diff --git a/.gitignore b/.gitignore index 14d243e..c843e2c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ secrets.h sketches/secrets.h .env -.vscode \ No newline at end of file +.vscode +.kiro \ No newline at end of file diff --git a/backend/handlers/auth.go b/backend/handlers/auth.go index 0c391c7..7245735 100644 --- a/backend/handlers/auth.go +++ b/backend/handlers/auth.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "fmt" "net/http" + "strings" "time" "github.com/gagan-devv/terradetect/backend/config" @@ -32,7 +33,8 @@ func NewAuthHandler(database *db.Database, cfg *config.Config) *AuthHandler { type registerRequest struct { Username string `json:"username" binding:"required,min=3,max=32"` Password string `json:"password" binding:"required,min=8"` - DeviceID string `json:"device_id" binding:"required,len=6"` + DeviceID string `json:"device_id"` + Email string `json:"email"` } func (h *AuthHandler) Register(c *gin.Context) { @@ -48,19 +50,27 @@ func (h *AuthHandler) Register(c *gin.Context) { defer cancel() var device models.Device - err := h.db.Devices.FindOne(ctx, bson.M{ - "device_id": req.DeviceID, - "registered": false, - }).Decode(&device) - - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": gin.H{ - "code": "DEVICE_NOT_REGISTERED", - "message": "Invalid or already registered device ID.", - }, - }) - return + deviceFound := false + if req.DeviceID != "" { + if !models.IsValidDeviceID(req.DeviceID) { + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": gin.H{"code": "VALIDATION_ERROR", "message": "device_id must be two uppercase letters followed by four digits (e.g. AB1234)"}}) + return + } + err := h.db.Devices.FindOne(ctx, bson.M{ + "device_id": req.DeviceID, + "registered": false, + }).Decode(&device) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": gin.H{ + "code": "DEVICE_NOT_REGISTERED", + "message": "Invalid or already registered device ID.", + }, + }) + return + } + deviceFound = true } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) @@ -74,12 +84,17 @@ func (h *AuthHandler) Register(c *gin.Context) { return } - _, err = h.db.Users.InsertOne(ctx, bson.M{ + userDoc := bson.M{ "username": req.Username, "password_hash": string(hash), "device_id": req.DeviceID, "created_at": primitive.NewDateTimeFromTime(time.Now()), - }) + } + if req.Email != "" { + userDoc["email"] = req.Email + } + + _, err = h.db.Users.InsertOne(ctx, userDoc) if err != nil { c.JSON(http.StatusConflict, gin.H{ @@ -91,22 +106,29 @@ func (h *AuthHandler) Register(c *gin.Context) { return } - _, _ = h.db.Devices.UpdateOne(ctx, - bson.M{"device_id": req.DeviceID}, - bson.M{"$set": bson.M{"registered": true}}, - ) + if deviceFound { + _, _ = h.db.Devices.UpdateOne(ctx, + bson.M{"device_id": req.DeviceID}, + bson.M{"$set": bson.M{"registered": true}}, + ) + } - c.JSON(http.StatusCreated, gin.H{ + resp := gin.H{ "username": req.Username, "device_id": req.DeviceID, - "api_key": device.APIKey, - }) + } + if deviceFound { + resp["api_key"] = device.APIKey + } + + c.JSON(http.StatusCreated, resp) } type loginRequest struct { - Username string `json:"username" binding:"required"` + Username string `json:"username"` + Email string `json:"email"` Password string `json:"password" binding:"required"` - DeviceID string `json:"device_id" binding:"required"` + DeviceID string `json:"device_id"` } func (h *AuthHandler) Login(c *gin.Context) { @@ -121,25 +143,63 @@ func (h *AuthHandler) Login(c *gin.Context) { return } + // Require either username or email + if strings.TrimSpace(req.Username) == "" && strings.TrimSpace(req.Email) == "" { + c.JSON(http.StatusUnprocessableEntity, gin.H{ + "error": gin.H{ + "code": "VALIDATION_ERROR", + "message": "username or email is required", + }, + }) + return + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var user models.User - err := h.db.Users.FindOne(ctx, bson.M{ - "username": req.Username, - "device_id": req.DeviceID, - }).Decode(&user) + // Build query: match username OR email; include device_id if provided + filters := []bson.M{} + if strings.TrimSpace(req.Username) != "" { + filters = append(filters, bson.M{"username": req.Username}) + } + if strings.TrimSpace(req.Email) != "" { + filters = append(filters, bson.M{"email": req.Email}) + } + + query := bson.M{"$or": filters} + + err := h.db.Users.FindOne(ctx, query).Decode(&user) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": gin.H{ "code": "INVALID_CREDENTIALS", - "message": "Invalid username, password, or device ID.", + "message": "Invalid username/email, password, or device ID.", }, }) return } + // If a device_id was provided, verify it matches the user's device if the + // user already has a device associated. If the user has no device set, + // allow login (they can claim a device later). + if req.DeviceID != "" { + if !models.IsValidDeviceID(req.DeviceID) { + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": gin.H{"code": "VALIDATION_ERROR", "message": "device_id must be two uppercase letters followed by four digits (e.g. AB1234)"}}) + return + } + if user.DeviceID != "" && req.DeviceID != user.DeviceID { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": gin.H{ + "code": "INVALID_CREDENTIALS", + "message": "Invalid username/email, password, or device ID.", + }, + }) + return + } + } + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "error": gin.H{ @@ -230,7 +290,7 @@ func (h *AuthHandler) Refresh(c *gin.Context) { } claims := token.Claims.(jwt.MapClaims) - if claims["token"] != "refresh" { + if claims["token_type"] != "refresh" { c.JSON(http.StatusUnauthorized, gin.H{ "error": gin.H{ "code": "UNAUTHORIZED", diff --git a/backend/handlers/device.go b/backend/handlers/device.go index f1912c2..dd1a75e 100644 --- a/backend/handlers/device.go +++ b/backend/handlers/device.go @@ -37,6 +37,13 @@ func (h *DeviceHandler) Check(c *gin.Context) { return } + if !models.IsValidDeviceID(req.DeviceID) { + c.JSON(http.StatusUnprocessableEntity, gin.H{ + "error": gin.H{"code": "VALIDATION_ERROR", "message": "device_id must be two uppercase letters followed by four digits (e.g. AB1234)"}, + }) + return + } + ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) defer cancel() diff --git a/backend/handlers/sensor.go b/backend/handlers/sensor.go index 6d2d79e..5933e15 100644 --- a/backend/handlers/sensor.go +++ b/backend/handlers/sensor.go @@ -59,6 +59,11 @@ func (h *SensorHandler) ReceiveESP32(c *gin.Context) { return } + if !models.IsValidDeviceID(payload.DeviceId) { + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": gin.H{"code": "VALIDATION_ERROR", "message": "device_id must be two uppercase letters followed by four digits (e.g. AB1234)"}}) + return + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index f68424f..25d02d2 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -48,7 +48,7 @@ func Auth(secretKey string) gin.HandlerFunc { } // Reject refresh tokens used as access tokens - if claims["type"] == "refresh" { + if claims["token_type"] == "refresh" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": gin.H{"code": "UNAUTHORIZED", "message": "Use access token, not refresh token."}, }) @@ -59,4 +59,4 @@ func Auth(secretKey string) gin.HandlerFunc { c.Set("device_id", claims["device_id"]) c.Next() } -} \ No newline at end of file +} diff --git a/backend/models/device.go b/backend/models/device.go index b4299dd..8ca2b3e 100644 --- a/backend/models/device.go +++ b/backend/models/device.go @@ -1,6 +1,10 @@ package models -import "go.mongodb.org/mongo-driver/bson/primitive" +import ( + "regexp" + + "go.mongodb.org/mongo-driver/bson/primitive" +) type Device struct { ID primitive.ObjectID `bson:"_id,omitempty" json:"_id"` @@ -8,4 +12,13 @@ type Device struct { APIKey string `bson:"api_key" json:"api_key"` Registered bool `bson:"registered" json:"registered"` CreatedAt primitive.DateTime `bson:"created_at" json:"created_at"` +} + +var deviceIDRegexp = regexp.MustCompile(`^[A-Z]{2}[0-9]{4}$`) + +// IsValidDeviceID returns true when the device id matches the +// expected pattern: two uppercase letters followed by four digits +// (example: AB1234). +func IsValidDeviceID(id string) bool { + return deviceIDRegexp.MatchString(id) } \ No newline at end of file diff --git a/backend/models/user.go b/backend/models/user.go index ed9c4f4..9f29040 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -5,7 +5,8 @@ import "go.mongodb.org/mongo-driver/bson/primitive" type User struct { ID primitive.ObjectID `bson:"_id,omitempty" json:"_id"` Username string `bson:"username" json:"username"` + Email string `bson:"email,omitempty" json:"email,omitempty"` PasswordHash string `bson:"password_hash" json:"-"` DeviceID string `bson:"device_id" json:"device_id"` CreatedAt primitive.DateTime `bson:"created_at" json:"created_at"` -} \ No newline at end of file +} diff --git a/backend/scripts/.cph/.seed_db.py_b0c5d8383cb237149ceb1afb7c53fd4b.prob b/backend/scripts/.cph/.seed_db.py_b0c5d8383cb237149ceb1afb7c53fd4b.prob new file mode 100644 index 0000000..deea20b --- /dev/null +++ b/backend/scripts/.cph/.seed_db.py_b0c5d8383cb237149ceb1afb7c53fd4b.prob @@ -0,0 +1 @@ +{"name":"Local: seed_db","url":"/home/gagan-ahlawat/Documents/TerraDetect/backend/scripts/seed_db.py","tests":[{"id":1774633948944,"input":"","output":""}],"interactive":false,"memoryLimit":1024,"timeLimit":3000,"srcPath":"/home/gagan-ahlawat/Documents/TerraDetect/backend/scripts/seed_db.py","group":"local","local":true} \ No newline at end of file diff --git a/backend/scripts/add_device_and_sensor_data.py b/backend/scripts/add_device_and_sensor_data.py new file mode 100644 index 0000000..e0c9d59 --- /dev/null +++ b/backend/scripts/add_device_and_sensor_data.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Add/assign a device_id to a user and insert sensor_data documents. + +Usage: + python3 backend/scripts/add_device_and_sensor_data.py [count] + +Example: + python3 backend/scripts/add_device_and_sensor_data.py steve.tyl@example.com TD0001 7 + +The script reads `backend/.env` for MONGO_URI and DB_NAME. It will upsert the +device in `device_ids`, set the user's `device_id`, mark the device as +`registered: true`, and insert `count` sensor_data documents (default 7). +""" +import sys +import os +import random +from datetime import datetime, timedelta +from pymongo import MongoClient + + +def load_env(env_path): + mongo_uri = None + db_name = "terradetect" + if os.path.exists(env_path): + with open(env_path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("MONGO_URI="): + mongo_uri = line.split("=", 1)[1].strip() + if line.startswith("DB_NAME="): + db_name = line.split("=", 1)[1].strip() + return mongo_uri, db_name + + +def main(): + if len(sys.argv) < 3: + print("Usage: add_device_and_sensor_data.py [count]") + sys.exit(1) + + identifier = sys.argv[1] + device_id = sys.argv[2] + count = int(sys.argv[3]) if len(sys.argv) > 3 else 7 + count = max(1, min(100, count)) + + # Validate device id format: two uppercase letters followed by four digits + import re + if not re.match(r'^[A-Z]{2}[0-9]{4}$', device_id): + print('Invalid device_id format. Expected two uppercase letters followed by four digits (e.g. AB1234).') + sys.exit(1) + + repo_root = os.path.dirname(os.path.dirname(__file__)) + env_path = os.path.join(repo_root, ".env") + mongo_uri, db_name = load_env(env_path) + if not mongo_uri: + mongo_uri = os.environ.get('MONGO_URI') + if not mongo_uri: + print('MONGO_URI not found in backend/.env or environment. Set it and retry.') + sys.exit(1) + + client = MongoClient(mongo_uri, serverSelectionTimeoutMS=10000, tls=True, tlsAllowInvalidCertificates=True) + db = client[db_name] + users = db['users'] + devices = db['device_ids'] + sensor = db['sensor_data'] + + user = users.find_one({'$or': [{'email': identifier}, {'username': identifier}]}) + if not user: + print('User not found:', identifier) + sys.exit(1) + + # Upsert device + existing_dev = devices.find_one({'device_id': device_id}) + if existing_dev: + print('Device exists, updating registered=true') + devices.update_one({'device_id': device_id}, {'$set': {'registered': True}}) + else: + api_key = os.environ.get('API_KEY_FALLBACK') or None + dev_doc = { + 'device_id': device_id, + 'api_key': api_key or os.urandom(16).hex(), + 'registered': True, + 'created_at': datetime.utcnow(), + } + devices.insert_one(dev_doc) + print('Inserted device', device_id) + + # Assign device to user + users.update_one({'_id': user['_id']}, {'$set': {'device_id': device_id}}) + print(f"Assigned device {device_id} to user {user.get('username')} / {user.get('email')}") + + # Insert sensor readings spaced over the past 24 hours + now = datetime.utcnow() + for i in range(count): + ts = now - timedelta(hours=(count - i)) + timedelta(minutes=random.randint(0, 59)) + doc = { + 'device_id': device_id, + 'temperature': round(random.uniform(20.0, 30.0), 2), + 'ph': round(random.uniform(5.5, 7.5), 2), + 'humidity': round(random.uniform(40.0, 80.0), 1), + 'ec': round(random.uniform(0.5, 2.0), 2), + 'N': round(random.uniform(10.0, 80.0), 1), + 'P': round(random.uniform(5.0, 60.0), 1), + 'K': round(random.uniform(10.0, 80.0), 1), + 'moisture': round(random.uniform(20.0, 80.0), 1), + 'timestamp': ts, + } + sensor.insert_one(doc) + print(f'Inserted {count} sensor readings for {device_id}') + print('Done.') + + +if __name__ == '__main__': + main() diff --git a/backend/scripts/inspect_user.py b/backend/scripts/inspect_user.py new file mode 100644 index 0000000..48af194 --- /dev/null +++ b/backend/scripts/inspect_user.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Simple inspect script to query a user document from the MongoDB used by the backend. +Run from the repo root: `python3 backend/scripts/inspect_user.py steve.tyl@example.com` +This script will print non-sensitive fields: whether the user exists, device_id, and whether a password hash exists. +""" +import sys +from pymongo import MongoClient +import ssl +import os + +from urllib.parse import quote_plus + + +def main(): + if len(sys.argv) < 2: + print("Usage: inspect_user.py ") + sys.exit(1) + + identifier = sys.argv[1] + + # Load env from backend/.env if present + env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env') + mongo_uri = None + db_name = 'terradetect' + if os.path.exists(env_path): + with open(env_path, 'r') as f: + for line in f: + line = line.strip() + if line.startswith('MONGO_URI='): + mongo_uri = line.split('=', 1)[1].strip() + if line.startswith('DB_NAME='): + db_name = line.split('=', 1)[1].strip() + + if not mongo_uri: + # fallback to environment variable + mongo_uri = os.environ.get('MONGO_URI') + + if not mongo_uri: + print('MONGO_URI not found in backend/.env or environment. Set it and retry.') + sys.exit(1) + + try: + client = MongoClient(mongo_uri, serverSelectionTimeoutMS=5000, tls=True, tlsAllowInvalidCertificates=True) + db = client[db_name] + users = db['users'] + + # Try to match email or username + doc = users.find_one({'$or': [{'email': identifier}, {'username': identifier}]}) + if not doc: + print('User not found') + sys.exit(0) + + print('User found') + print('username:', doc.get('username', '')) + print('email:', doc.get('email', '')) + device_id = doc.get('device_id', None) + print('device_id:', device_id if device_id is not None else '(none)') + has_pw = 'password_hash' in doc and bool(doc.get('password_hash')) + print('password_hash_present:', has_pw) + + except Exception as e: + print('Error connecting to MongoDB or querying:', str(e)) + sys.exit(2) + + +if __name__ == '__main__': + main() diff --git a/backend/scripts/reset_password.py b/backend/scripts/reset_password.py new file mode 100644 index 0000000..69d54d1 --- /dev/null +++ b/backend/scripts/reset_password.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Reset a user's password in the TerraDetect MongoDB. +Usage: + python3 backend/scripts/reset_password.py + +Reads `backend/.env` for MONGO_URI/DB_NAME. Replaces `password_hash` with a bcrypt hash. +Prints whether the user was updated. +""" +import sys +import os +from pymongo import MongoClient +import bcrypt + + +def load_env(env_path): + mongo_uri = None + db_name = "terradetect" + if os.path.exists(env_path): + with open(env_path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("MONGO_URI="): + mongo_uri = line.split("=", 1)[1].strip() + if line.startswith("DB_NAME="): + db_name = line.split("=", 1)[1].strip() + return mongo_uri, db_name + + +def main(): + if len(sys.argv) != 3: + print("Usage: reset_password.py ") + sys.exit(1) + + identifier = sys.argv[1] + new_password = sys.argv[2] + + repo_root = os.path.dirname(os.path.dirname(__file__)) + env_path = os.path.join(repo_root, ".env") + mongo_uri, db_name = load_env(env_path) + if not mongo_uri: + mongo_uri = os.environ.get("MONGO_URI") + if not mongo_uri: + print("MONGO_URI not found in backend/.env or environment. Set it and retry.") + sys.exit(1) + + client = MongoClient(mongo_uri, serverSelectionTimeoutMS=10000, tls=True, tlsAllowInvalidCertificates=True) + db = client[db_name] + users = db["users"] + + query = {"$or": [{"email": identifier}, {"username": identifier}]} + user = users.find_one(query) + if not user: + print("User not found. No changes made.") + sys.exit(1) + + pw_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + res = users.update_one({"_id": user["_id"]}, {"$set": {"password_hash": pw_hash}}) + if res.modified_count == 1: + print(f"Password updated for user: {user.get('username')} ({user.get('email')})") + else: + print("No changes made (unexpected).") + +if __name__ == '__main__': + main() diff --git a/backend/scripts/seed_db.py b/backend/scripts/seed_db.py new file mode 100644 index 0000000..89b450c --- /dev/null +++ b/backend/scripts/seed_db.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +Seed script for TerraDetect MongoDB. +Creates sample devices, users, and sensor readings. + +Usage: + python3 backend/scripts/seed_db.py + +Requirements: + pip install pymongo bcrypt dnspython + +The script reads `backend/.env` for MONGO_URI and DB_NAME. It will insert +sample documents and print what it created. It avoids duplicating existing +`device_id` or `username`/`email`. +""" +import os +import sys +import time +import uuid +import secrets +from datetime import datetime, timedelta + +from pymongo import MongoClient +import bcrypt + + +def load_env(env_path): + mongo_uri = None + db_name = "terradetect" + if os.path.exists(env_path): + with open(env_path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("MONGO_URI="): + mongo_uri = line.split("=", 1)[1].strip() + if line.startswith("DB_NAME="): + db_name = line.split("=", 1)[1].strip() + return mongo_uri, db_name + + +def ensure_indexes(db): + # Matches indexes created by the backend, idempotent if already created + try: + db.sensor_data.create_index([("device_id", 1), ("timestamp", -1)]) + db.token_deny_list.create_index([("expires_at", 1)], expireAfterSeconds=0) + db.users.create_index([("username", 1)], unique=True) + except Exception: + pass + + +def upsert_device(devices_col, device_id, api_key=None, registered=False): + if api_key is None: + api_key = secrets.token_hex(16) + now = datetime.utcnow() + existing = devices_col.find_one({"device_id": device_id}) + if existing: + print(f"Device {device_id} already exists; skipping insert") + return existing + doc = { + "device_id": device_id, + "api_key": api_key, + "registered": registered, + "created_at": now, + } + devices_col.insert_one(doc) + print(f"Inserted device {device_id} (registered={registered})") + return doc + + +def upsert_user(users_col, username, email, password_plain, device_id=""): + now = datetime.utcnow() + existing = users_col.find_one({"$or": [{"username": username}, {"email": email}]}) + if existing: + print(f"User {username} / {email} already exists; skipping insert") + return existing + pw_hash = bcrypt.hashpw(password_plain.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + doc = { + "username": username, + "email": email, + "password_hash": pw_hash, + "device_id": device_id, + "created_at": now, + } + users_col.insert_one(doc) + print(f"Inserted user {username} (email={email}) device_id={device_id}") + return doc + + +def insert_sensor_reading(sensor_col, device_id, temp=25.0, ph=6.5, humidity=60.0, ec=1.0, N=40.0, P=25.0, K=30.0, moisture=50.0, timestamp=None): + if timestamp is None: + timestamp = datetime.utcnow() + doc = { + "device_id": device_id, + "temperature": float(temp), + "ph": float(ph), + "humidity": float(humidity), + "ec": float(ec), + "N": float(N), + "P": float(P), + "K": float(K), + "moisture": float(moisture), + "timestamp": timestamp, + } + sensor_col.insert_one(doc) + print(f"Inserted sensor reading for {device_id} @ {timestamp.isoformat()}") + + +def main(): + repo_root = os.path.dirname(os.path.dirname(__file__)) + env_path = os.path.join(repo_root, ".env") + mongo_uri, db_name = load_env(env_path) + if not mongo_uri: + mongo_uri = os.environ.get("MONGO_URI") + + if not mongo_uri: + print("MONGO_URI not found in backend/.env or environment. Set it and retry.") + sys.exit(1) + + print("Connecting to:", mongo_uri.split("@")[-1]) + client = MongoClient(mongo_uri, serverSelectionTimeoutMS=10000, tls=True, tlsAllowInvalidCertificates=True) + db = client[db_name] + + ensure_indexes(db) + + devices = db["device_ids"] + users = db["users"] + sensor = db["sensor_data"] + + # Sample devices (format: two uppercase letters + four digits, e.g. TD0001) + dev1 = upsert_device(devices, "TD0001", registered=False) + dev2 = upsert_device(devices, "TD0002", registered=False) + + # Sample users + # User with no device + upsert_user(users, "testuser", "test@example.com", "TestPass123!", device_id="") + # User with a pre-provisioned device (claim it) + # If you want the device claimed at creation, set registered=True above or update device after user creation. + upsert_user(users, "steve.tyl", "steve.tyl@example.com", "SteveTyl@01", device_id="TD0001") + + # Mark device TD0001 as registered (claimed) + devices.update_one({"device_id": "TD0001"}, {"$set": {"registered": True}}, upsert=False) + print("Marked TD0001 as registered") + + # Sample sensor readings + now = datetime.utcnow() + insert_sensor_reading(sensor, "TD0001", temp=27.3, ph=6.8, humidity=63.0, N=42.0, P=28.0, K=33.0, moisture=55.0, timestamp=now - timedelta(hours=1)) + insert_sensor_reading(sensor, "TD0001", temp=26.8, ph=6.7, humidity=62.0, N=41.0, P=27.5, K=32.5, moisture=54.0, timestamp=now - timedelta(minutes=10)) + + print("Seeding complete.") + + +if __name__ == "__main__": + main() diff --git a/docs/api.md b/docs/api.md index 8de153a..7086510 100644 --- a/docs/api.md +++ b/docs/api.md @@ -13,11 +13,14 @@ ESP32 data ingestion endpoint which keeps its legacy path `/api/esp32` to avoid reflashing all deployed devices. ### Content Type + All requests and responses use `application/json` unless noted otherwise. ### Authentication + All protected endpoints require a JWT access token in the `Authorization` header: + ``` Authorization: Bearer ``` @@ -42,29 +45,29 @@ Every endpoint that fails returns this shape. No endpoint deviates from it. ### Error Codes -| Code | HTTP Status | Meaning | -|---|---|---| -| `UNAUTHORIZED` | 401 | Missing or invalid JWT | -| `FORBIDDEN` | 403 | Valid JWT but insufficient permission | -| `NOT_FOUND` | 404 | Resource does not exist | -| `VALIDATION_ERROR` | 422 | Request body failed validation | -| `INVALID_CREDENTIALS` | 401 | Wrong username/password/device_id | -| `DEVICE_NOT_REGISTERED` | 403 | Device ID exists but is unregistered | -| `DEVICE_ALREADY_REGISTERED` | 409 | Device ID already claimed by a user | -| `INVALID_API_KEY` | 401 | ESP32 API key does not match device | -| `RATE_LIMITED` | 429 | Too many requests | -| `INTERNAL_ERROR` | 500 | Unexpected server error | +| Code | HTTP Status | Meaning | +| --------------------------- | ----------- | ------------------------------------- | +| `UNAUTHORIZED` | 401 | Missing or invalid JWT | +| `FORBIDDEN` | 403 | Valid JWT but insufficient permission | +| `NOT_FOUND` | 404 | Resource does not exist | +| `VALIDATION_ERROR` | 422 | Request body failed validation | +| `INVALID_CREDENTIALS` | 401 | Wrong username/password/device_id | +| `DEVICE_NOT_REGISTERED` | 403 | Device ID exists but is unregistered | +| `DEVICE_ALREADY_REGISTERED` | 409 | Device ID already claimed by a user | +| `INVALID_API_KEY` | 401 | ESP32 API key does not match device | +| `RATE_LIMITED` | 429 | Too many requests | +| `INTERNAL_ERROR` | 500 | Unexpected server error | --- ## Rate Limits -| Endpoint group | Limit | -|---|---| -| `POST /api/v1/auth/login` | 5 requests / minute / IP | -| `POST /api/v1/auth/register` | 3 requests / minute / IP | -| `POST /api/esp32` | 2 requests / second / device_id | -| All other endpoints | 60 requests / minute / user | +| Endpoint group | Limit | +| ---------------------------- | ------------------------------- | +| `POST /api/v1/auth/login` | 5 requests / minute / IP | +| `POST /api/v1/auth/register` | 3 requests / minute / IP | +| `POST /api/esp32` | 2 requests / second / device_id | +| All other endpoints | 60 requests / minute / user | Rate limited responses return HTTP `429` with a `Retry-After` header (seconds until the limit resets). @@ -74,17 +77,19 @@ Rate limited responses return HTTP `429` with a `Retry-After` header ## Auth Endpoints ### Register + `POST /api/v1/auth/register` -Creates a new user account and claims a pre-provisioned device ID. -The device ID must exist in the database with `registered: false` — it -must have been created by an admin via `admin_add_device_id` first. +Creates a new user account. The `device_id` field is optional. If a +`device_id` is provided it must already exist in the database with +`registered: false` (pre-provisioned by an admin). When a valid +`device_id` is provided the device will be claimed (set to +`registered: true`) and its `api_key` will be returned once. If no +`device_id` is provided the account is created without associating a +device; the user or an admin may claim a device later. -On success, the device is marked `registered: true` and the API key -is returned **once**. The client must store it securely — it cannot -be retrieved again. +**Request (with device)** -**Request** ```json { "username": "gagan", @@ -93,13 +98,23 @@ be retrieved again. } ``` -| Field | Type | Rules | -|---|---|---| -| `username` | string | 3–32 chars, alphanumeric + underscores | -| `password` | string | min 8 chars | -| `device_id` | string | exactly 6 chars, must exist and be unregistered | +**Request (without device)** + +```json +{ + "username": "gagan", + "password": "min8chars" +} +``` + +| Field | Type | Rules | +| ----------- | ------ | -------------------------------------------------------------- | +| `username` | string | 3–32 chars, alphanumeric + underscores | +| `password` | string | min 8 chars | +| `device_id` | string | optional; if provided must be exactly 6 chars and unregistered | + +**Response `201` (device claimed)** -**Response `201`** ```json { "username": "gagan", @@ -108,18 +123,30 @@ be retrieved again. } ``` -> ⚠️ `api_key` is only returned here. Flash it to the ESP32 and store it -> in the mobile app's secure storage. It cannot be retrieved again — only -> reset by an admin. +**Response `201` (no device provided)** + +```json +{ + "username": "gagan", + "device_id": "" +} +``` + +> ⚠️ When present, `api_key` is returned only once (on successful +> registration that claims a device). The client must store it +> securely — it cannot be retrieved again and must be reset by an +> admin if lost. --- ### Login + `POST /api/v1/auth/login` Authenticates a user and returns JWT tokens. **Request** + ```json { "username": "gagan", @@ -129,6 +156,7 @@ Authenticates a user and returns JWT tokens. ``` **Response `200`** + ```json { "access_token": "eyJhbGci....", @@ -142,21 +170,23 @@ Authenticates a user and returns JWT tokens. } ``` -| Field | Description | -|---|---| -| `access_token` | Short-lived JWT — 15 minutes. Send in `Authorization` header. | -| `refresh_token` | Long-lived JWT — 30 days. Store in secure storage only. | -| `expires_in` | Seconds until access token expires (always 900). | +| Field | Description | +| --------------- | ------------------------------------------------------------- | +| `access_token` | Short-lived JWT — 15 minutes. Send in `Authorization` header. | +| `refresh_token` | Long-lived JWT — 30 days. Store in secure storage only. | +| `expires_in` | Seconds until access token expires (always 900). | --- ### Refresh Token + `POST /api/v1/auth/refresh` Issues a new access token using a valid refresh token. Does not require the `Authorization` header — uses the refresh token directly. **Request** + ```json { "refresh_token": "eyJhbGci...." @@ -164,6 +194,7 @@ the `Authorization` header — uses the refresh token directly. ``` **Response `200`** + ```json { "access_token": "eyJhbGci....", @@ -178,6 +209,7 @@ The client should redirect to login. --- ### Logout + `POST /api/v1/auth/logout` 🔒 **Requires JWT** @@ -188,6 +220,7 @@ MongoDB). The access token will still work until it naturally expires **Request** — empty body `{}` **Response `200`** + ```json { "message": "Logged out successfully." @@ -199,6 +232,7 @@ MongoDB). The access token will still work until it naturally expires ## Device Endpoints ### Check Device ID + `POST /api/v1/device/check` Used by the ESP32 on boot to verify its device ID is registered in the @@ -206,6 +240,7 @@ system before attempting data uploads. No auth required — the API key check on `/api/esp32` is the actual security gate. **Request** + ```json { "device_id": "ABC123" @@ -213,6 +248,7 @@ check on `/api/esp32` is the actual security gate. ``` **Response `200`** + ```json { "registered": true @@ -227,6 +263,7 @@ by a user yet. `404 NOT_FOUND` means the device ID doesn't exist at all. ## Sensor Endpoints ### Ingest ESP32 Data + `POST /api/esp32` > ⚠️ This endpoint intentionally keeps its legacy path to avoid @@ -236,12 +273,14 @@ Receives sensor readings from an ESP32. Authenticated via per-device API key in the `x-api-key` header — **not** JWT. **Headers** + ``` x-api-key: 95b09474fa652e53...4d4f19f2 Content-Type: application/json ``` **Request** + ```json { "device_id": "ABC123", @@ -256,19 +295,20 @@ Content-Type: application/json } ``` -| Field | Type | Unit | Required | -|---|---|---|---| -| `device_id` | string | — | ✅ | -| `temperature` | float | °C | ✅ | -| `ph` | float | 0–14 | ✅ | -| `humidity` | float | % | ✅ | -| `ec` | float | μS/cm | ❌ (default 0) | -| `N` | float | kg/ha | ❌ (default 0) | -| `P` | float | kg/ha | ❌ (default 0) | -| `K` | float | kg/ha | ❌ (default 0) | -| `moisture` | float | % | ❌ (default 40) | +| Field | Type | Unit | Required | +| ------------- | ------ | ----- | --------------- | +| `device_id` | string | — | ✅ | +| `temperature` | float | °C | ✅ | +| `ph` | float | 0–14 | ✅ | +| `humidity` | float | % | ✅ | +| `ec` | float | μS/cm | ❌ (default 0) | +| `N` | float | kg/ha | ❌ (default 0) | +| `P` | float | kg/ha | ❌ (default 0) | +| `K` | float | kg/ha | ❌ (default 0) | +| `moisture` | float | % | ❌ (default 40) | **Response `200`** + ```json { "status": "success", @@ -280,6 +320,7 @@ Content-Type: application/json --- ### Get Latest Sensor Reading + `GET /api/v1/sensor/latest` 🔒 **Requires JWT** @@ -287,6 +328,7 @@ Returns the most recent sensor document for the authenticated user's device. **Response `200`** + ```json { "data": { @@ -309,6 +351,7 @@ device. --- ### Get Sensor History + `GET /api/v1/sensor/history` 🔒 **Requires JWT** @@ -317,14 +360,15 @@ device, sorted newest-first. **Query Parameters** -| Param | Type | Default | Max | -|---|---|---|---| -| `page` | int | 1 | — | -| `per_page` | int | 10 | 100 | +| Param | Type | Default | Max | +| ---------- | ---- | ------- | --- | +| `page` | int | 1 | — | +| `per_page` | int | 10 | 100 | **Example:** `GET /api/v1/sensor/history?page=2&per_page=20` **Response `200`** + ```json { "history": [ @@ -358,6 +402,7 @@ All prediction endpoints are protected by JWT. They accept either from the latest ESP32 reading). ### Crop Recommendation + `POST /api/v1/predict/crop` 🔒 **Requires JWT** @@ -365,6 +410,7 @@ Returns the best-matching crop and a ranked suitability list for the given soil conditions. **Request — manual input** + ```json { "source": "manual", @@ -379,6 +425,7 @@ given soil conditions. ``` **Request — from sensor** + ```json { "source": "sensor", @@ -390,17 +437,18 @@ When `source` is `"sensor"`, the server fetches the latest reading for the authenticated user's device and uses it. `rainfall` is always manual since the ESP32 does not measure it. -| Field | Type | Unit | Required (manual) | -|---|---|---|---| -| `N` | float | kg/ha | ✅ | -| `P` | float | kg/ha | ✅ | -| `K` | float | kg/ha | ✅ | -| `temperature` | float | °C | ✅ | -| `humidity` | float | % | ✅ | -| `ph` | float | 0–14 | ✅ | -| `rainfall` | float | mm | ✅ (both modes) | +| Field | Type | Unit | Required (manual) | +| ------------- | ----- | ----- | ----------------- | +| `N` | float | kg/ha | ✅ | +| `P` | float | kg/ha | ✅ | +| `K` | float | kg/ha | ✅ | +| `temperature` | float | °C | ✅ | +| `humidity` | float | % | ✅ | +| `ph` | float | 0–14 | ✅ | +| `rainfall` | float | mm | ✅ (both modes) | **Response `200`** + ```json { "recommended_crop": "rice", @@ -410,16 +458,17 @@ since the ESP32 does not measure it. } ``` -| Field | Description | -|---|---| +| Field | Description | +| ------------------ | ------------------------------------------ | | `recommended_crop` | Best crop by suitability score calculation | -| `confidence` | Suitability score (0–100) | -| `model_prediction` | Raw ML model output | -| `model_confidence` | ML model's probability (0–100) | +| `confidence` | Suitability score (0–100) | +| `model_prediction` | Raw ML model output | +| `model_confidence` | ML model's probability (0–100) | --- ### Crop Suitability Analysis + `POST /api/v1/predict/suitability` 🔒 **Requires JWT** @@ -427,6 +476,7 @@ Analyzes how well current soil conditions match a specific crop's ideal parameters. Returns a score and per-parameter adjustment table. **Request** + ```json { "source": "manual", @@ -442,6 +492,7 @@ parameters. Returns a score and per-parameter adjustment table. ``` **Response `200`** + ```json { "crop": "wheat", @@ -473,12 +524,14 @@ parameters. Returns a score and per-parameter adjustment table. --- ### Fertilizer Recommendation + `POST /api/v1/predict/fertilizer` 🔒 **Requires JWT** Recommends a fertilizer based on soil conditions and target crop. **Request** + ```json { "source": "manual", @@ -498,6 +551,7 @@ Recommends a fertilizer based on soil conditions and target crop. `soil_type` must be one of: `"Black"`, `"Clayey"`, `"Loamy"`, `"Red"`, `"Sandy"`. **Response `200`** + ```json { "fertilizer": "Urea", @@ -519,6 +573,7 @@ Recommends a fertilizer based on soil conditions and target crop. ## Weather Endpoint ### Get Current Weather + `GET /api/v1/weather` 🔒 **Requires JWT** @@ -527,14 +582,15 @@ the client. Returns only the fields the app needs. **Query Parameters** -| Param | Type | Required | Description | -|---|---|---|---| -| `lat` | float | ✅ | Latitude | -| `lon` | float | ✅ | Longitude | +| Param | Type | Required | Description | +| ----- | ----- | -------- | ----------- | +| `lat` | float | ✅ | Latitude | +| `lon` | float | ✅ | Longitude | **Example:** `GET /api/v1/weather?lat=23.18&lon=75.77` **Response `200`** + ```json { "temperature": 34.2, @@ -555,6 +611,7 @@ This section documents what the firmware must send and how it must authenticate. It is the implementation contract for `sketches/`. ### Boot Sequence + ``` 1. Load device_id from EEPROM 2. Connect WiFi via WiFiManager @@ -564,6 +621,7 @@ authenticate. It is the implementation contract for `sketches/`. ``` ### Data Upload (every 60 seconds) + ``` POST /api/esp32 Headers: @@ -574,6 +632,7 @@ Body: sensor payload (see Ingest ESP32 Data above) ``` ### TLS + The firmware must pin the Let's Encrypt ISRG Root X1 certificate. `client.setInsecure()` must not be used in production builds. @@ -588,6 +647,7 @@ placeholder values. For Go backend implementation — these are the exact collection shapes. ### `users` + ```json { "_id": "ObjectId", @@ -599,6 +659,7 @@ For Go backend implementation — these are the exact collection shapes. ``` ### `device_ids` + ```json { "_id": "ObjectId", @@ -610,6 +671,7 @@ For Go backend implementation — these are the exact collection shapes. ``` ### `sensor_data` + ```json { "_id": "ObjectId", @@ -630,6 +692,7 @@ Index required: `{ device_id: 1, timestamp: -1 }` — used by both `/sensor/latest` (limit 1) and `/sensor/history` (paginated). ### `refresh_token_denylist` + ```json { "_id": "ObjectId", @@ -644,7 +707,7 @@ TTL index on `expires_at` so MongoDB auto-purges expired entries. ## Changelog -| Version | Change | -|---|---| -| 2.0 | JWT auth replacing Flask sessions; `/api/v1/` prefix; weather proxy; refresh tokens; standardized errors | -| 1.0 | Original Python Flask implementation | \ No newline at end of file +| Version | Change | +| ------- | -------------------------------------------------------------------------------------------------------- | +| 2.0 | JWT auth replacing Flask sessions; `/api/v1/` prefix; weather proxy; refresh tokens; standardized errors | +| 1.0 | Original Python Flask implementation | diff --git a/ml/validate_onnx.py b/ml/validate_onnx.py index d128e0a..1702653 100644 --- a/ml/validate_onnx.py +++ b/ml/validate_onnx.py @@ -10,5 +10,5 @@ sk_preds = sk_model.predict(X) onnx_preds = sess.run(None, {'float_input': X})[0] -mismatches = (sk_preds != onnx_preds) -print(f'Mismatches: {mismatches}/50 ({'PASS' if mismatches == 0 else 'FAIL'})') +mismatches_count = int((sk_preds != onnx_preds).sum()) +print(f"Mismatches: {mismatches_count}/50 ({'PASS' if mismatches_count == 0 else 'FAIL'})") diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index e69de29..82db1aa 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -0,0 +1,29 @@ +import { useEffect } from 'react' +import { Stack } from 'expo-router' +import { useRouter } from 'expo-router' +import { useAuthStore } from '../../store/authStore' + +export default function AppLayout() { + const router = useRouter() + const { accessToken, loadFromStorage } = useAuthStore() + + useEffect(() => { + const init = async () => { + await loadFromStorage() + if (!useAuthStore.getState().accessToken) { + router.replace('/(auth)/landing') + } + } + init() + }, []) + + if (!accessToken) return null + + return ( + + + + + + ) +} \ No newline at end of file diff --git a/mobile/app/(app)/dashboard.tsx b/mobile/app/(app)/dashboard.tsx index e69de29..e2208b3 100644 --- a/mobile/app/(app)/dashboard.tsx +++ b/mobile/app/(app)/dashboard.tsx @@ -0,0 +1,420 @@ +import React, { useEffect, useState } from 'react'; +import { View, Text, ScrollView, RefreshControl, Pressable, Alert } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import { api, SensorData } from '../../lib/api'; +import { useRouter } from 'expo-router'; +import { useAuthStore } from '../../store/authStore'; +import { StatCard } from '../../components/StatCard'; +import { HealthMeter } from '../../components/HealthMeter'; +import { Card } from '../../components/Card'; + +export default function Dashboard() { + const { accessToken, username } = useAuthStore(); + const accessExpiry = useAuthStore((s) => s.accessExpiry ?? null); + const [data, setData] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [fabOpen, setFabOpen] = useState(false); + const [sessionRemaining, setSessionRemaining] = useState(null); + + const fetchData = async () => { + if (!accessToken) return; + try { + setError(null); + const res = await api.latestSensor(accessToken); + setData(res); + } catch (err: unknown) { + console.error('Fetch error:', err); + setError(err instanceof Error ? err.message : String(err) || 'Failed to load sensor data'); + } finally { + setLoading(false); + } + }; + + const handlePredictSnapshot = async () => { + if (!accessToken) return Alert.alert('Not signed in'); + if (!data) return Alert.alert('No sensor data', 'Refresh to load latest sensor values before predicting.'); + try { + const body = { + temperature: data.temperature, + humidity: data.humidity, + ph: data.ph, + N: data.N, + P: data.P, + K: data.K, + }; + const res = await api.predictCrop(accessToken, body); + Alert.alert('Prediction', `Recommended crop: ${res.recommended_crop}\nConfidence: ${Math.round(res.confidence * 100)}%`); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + Alert.alert('Prediction failed', msg || 'Unknown error'); + } + }; + + const handleClaimDevice = () => { + Alert.alert('Claim Device', 'To claim a device, go to Settings → Devices in the app.'); + }; + + const handleLogout = async () => { + const store = useAuthStore.getState(); + await store.logout(); + }; + + const router = useRouter(); + + const handleManualInput = () => { + router.push('/manual-input'); + }; + + const onRefresh = async () => { + setRefreshing(true); + await fetchData(); + setRefreshing(false); + }; + + useEffect(() => { + fetchData(); + // Set up 30-second polling for real-time updates + const interval = setInterval(fetchData, 30000); + // Session countdown + let sessionTimer: ReturnType | null = null; + if (accessExpiry) { + const update = () => { + const rem = Math.max(0, Math.floor((accessExpiry - Date.now()) / 1000)); + setSessionRemaining(rem); + if (rem <= 0) { + const store = useAuthStore.getState(); + void store.logout(); + } + }; + update(); + sessionTimer = setInterval(update, 1000); + } else { + setSessionRemaining(null); + } + return () => { + clearInterval(interval); + if (sessionTimer) clearInterval(sessionTimer); + }; + }, [accessToken]); + + // Get greeting based on time of day + const getGreeting = () => { + const hour = new Date().getHours(); + if (hour < 12) return 'Good morning'; + if (hour < 18) return 'Good afternoon'; + return 'Good evening'; + }; + + // Loading skeleton + if (loading) { + return ( + + {/* Top App Bar Skeleton */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + // Error state + if (error) { + return ( + + {/* Top App Bar */} + + + + 🌱 + + TerraDetect + + {sessionRemaining !== null && ( + + {sessionRemaining > 0 ? `Session: ${Math.floor(sessionRemaining / 60)}m ${sessionRemaining % 60}s` : 'Session expired'} + + )} + + + + 🔔 + + + 👨‍🌾 + + + + + + } + > + + + ⚠️ + + + Failed to Load Data + + + {error} + + + + Retry + + + + + + ); + } + + return ( + + {/* TopAppBar - Glassmorphic */} + + + + 🌱 + + TerraDetect + + + + + 🔔 + + + 👨‍🌾 + + + + + + } + > + + {/* Welcome Section */} + + + {getGreeting()},{'\n'} + {username || 'Farmer'}! + + + Your crops are thriving today. Here's your real-time soil health overview. + + + + {/* Hero Bento Grid */} + + {/* Featured Field Status - 2/3 width */} + + + {/* Gradient overlay */} + + + + + + + Active Monitoring + + + + + North Sector Beta + + + Last scan: 2 minutes ago + + + + + {/* pH Health Meter - 1/3 width */} + + + + 🧪 + + + {data?.ph && data.ph >= 6.0 && data.ph <= 7.5 ? 'Optimal' : 'Monitor'} + + + + Soil pH Level + + + {data?.ph?.toFixed(1) ?? '--'} + + + + + + {/* Sensor Data Cards Section */} + + + + + Real-time Telemetry + + + + + + + + + + + + {/* Secondary Insights Area */} + + + {/* Hydration Forecast */} + + + Hydration Forecast + + + + + + + + + + + {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => ( + + {day} + + ))} + + + + {/* Predictive Action */} + + + 💡 + + + Predictive Action + + + Nitrogen levels are peaking. Delay fertilization for 48 hours to prevent runoff. + + + + View Full Analysis + + + + + + + + + {/* Floating Action Button - Quick Actions */} + + {fabOpen && ( + + + 🔄 Refresh + + + 📸 Snapshot → Predict + + + 🧷 Claim Device + + + 🚪 Logout + + + )} + + setFabOpen((s) => !s)} + className='w-14 h-14 rounded-full bg-primary items-center justify-center shadow-lg' + > + {fabOpen ? '×' : '+'} + + + + ); +} \ No newline at end of file diff --git a/mobile/app/(app)/history.tsx b/mobile/app/(app)/history.tsx index e69de29..c1eb9d2 100644 --- a/mobile/app/(app)/history.tsx +++ b/mobile/app/(app)/history.tsx @@ -0,0 +1,289 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { + View, Text, FlatList, TouchableOpacity, + ActivityIndicator, RefreshControl, +} from 'react-native' +import { LinearGradient } from 'expo-linear-gradient' +import { api } from '../../lib/api' +import { getErrorMessage } from '../../lib/error' +import { useAuthStore } from '../../store/authStore' +import type { SensorReading } from '../../store/sensorStore' + +const PER_PAGE = 20 + +interface Pagination { + total: number + page: number + per_page: number + total_pages: number +} + +// 7-Day summary chart component (simplified CSS-style bar chart) +function WeeklyTrendChart() { + const days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] + const humidities = [65, 72, 60, 80, 85, 70, 75] + const temps = [40, 45, 38, 50, 55, 48, 52] + + return ( + + + + 7-Day Atmosphere Trends + + + {/* Bar Chart */} + + {days.map((day, i) => ( + + {/* Humidity bar (blue) */} + + {/* Temp bar (green) */} + + + {day} + + + ))} + + {/* Legend */} + + + + Avg Temp (24°C) + + + + Avg Humidity (68%) + + + {/* Insight */} + + + Optimal conditions sustained for 84% of the week. Soil hydration levels are increasing. + + + + ) +} + +function HistoryRow({ item }: { item: SensorReading }) { + const date = new Date(item.timestamp) + const dateStr = date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + + return ( + + + + 📋 + + + {dateStr} + + {timeStr} + + + + {/* Sensor values */} + + + Temp + {item.temperature.toFixed(1)}°C + + + Humid + {item.humidity.toFixed(0)}% + + + pH + {item.ph.toFixed(1)} + + + NPK + + N:{item.N.toFixed(0)} + P:{item.P.toFixed(0)} + K:{item.K.toFixed(0)} + + + + + ) +} + +export default function HistoryScreen() { + const { accessToken } = useAuthStore() + const [readings, setReadings] = useState([]) + const [pagination, setPagination] = useState(null) + const [page, setPage] = useState(1) + const [isLoading, setIsLoading] = useState(false) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const [error, setError] = useState(null) + + const fetchPage = useCallback(async (pageNum: number, reset = false) => { + if (!accessToken) return + if (pageNum === 1) { + setIsLoading(true) + } else { + setIsLoadingMore(true) + } + setError(null) + try { + const res = await api.sensorHistory(accessToken, pageNum, PER_PAGE) + setReadings((prev) => (reset ? res.history : [...prev, ...res.history])) + setPagination(res.pagination) + setPage(pageNum) + } catch (err) { + setError(getErrorMessage(err)) + } finally { + setIsLoading(false) + setIsLoadingMore(false) + } + }, [accessToken]) + + useEffect(() => { fetchPage(1, true) }, [fetchPage]) + + const handleRefresh = () => fetchPage(1, true) + + const handleLoadMore = () => { + if (pagination && page < pagination.total_pages && !isLoadingMore) { + fetchPage(page + 1) + } + } + + const ListHeader = () => ( + + {/* Editorial Header */} + + + Environmental Log + + + Field History + + + + {/* Insights Chart */} + + {/* Section Header */} + + Recent Records + + {pagination && ( + + {pagination.total} total readings + + )} + + ) + + const ListEmpty = () => ( + + {isLoading + ? + : ( + + 🌱 + No history yet + + Sensor readings will appear here once your device starts transmitting data. + + + ) + } + + ) + + const ListFooter = () => { + if (isLoadingMore) { + return ( + + + + ) + } + if (pagination && page < pagination.total_pages) { + return ( + + + + Load Older Records + + + + + ) + } + return + } + + if (error && readings.length === 0) { + return ( + + {/* Glassmorphic Top Nav */} + + 🌱 + TerraDetect + + + + ⚠️ + + Failed to Load + {error} + + Retry + + + + ) + } + + return ( + + {/* Glassmorphic Top Nav */} + + + 🌱 + TerraDetect + + + 🔔 + + + + String(i)} + renderItem={({ item }) => } + ListHeaderComponent={ListHeader} + ListEmptyComponent={ListEmpty} + ListFooterComponent={ListFooter} + onEndReached={handleLoadMore} + onEndReachedThreshold={0.3} + contentContainerStyle={{ paddingHorizontal: 24, paddingBottom: 120 }} + refreshControl={ + 0} + onRefresh={handleRefresh} + tintColor="#006b2c" + /> + } + /> + + ) +} \ No newline at end of file diff --git a/mobile/app/(app)/manual-input.tsx b/mobile/app/(app)/manual-input.tsx new file mode 100644 index 0000000..7d401c9 --- /dev/null +++ b/mobile/app/(app)/manual-input.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import { View, Text, ScrollView, TextInput, Pressable, Alert } from 'react-native'; +import { api } from '../../lib/api'; + +export default function ManualInput() { + const [temperature, setTemperature] = useState(''); + const [humidity, setHumidity] = useState(''); + const [ph, setPh] = useState(''); + const [N, setN] = useState(''); + const [P, setP] = useState(''); + const [K, setK] = useState(''); + const [rainfall, setRainfall] = useState(''); + const [loading, setLoading] = useState(false); + + const parseNumber = (s: string) => { + const n = Number(s); + return Number.isFinite(n) ? n : undefined; + }; + + const handlePredict = async () => { + const body: Record = {}; + const t = parseNumber(temperature); + const h = parseNumber(humidity); + const p = parseNumber(ph); + const n = parseNumber(N); + const pp = parseNumber(P); + const k = parseNumber(K); + const r = parseNumber(rainfall); + + if (t === undefined || h === undefined || p === undefined || n === undefined || pp === undefined || k === undefined) { + return Alert.alert('Validation', 'Please fill temperature, humidity, pH, N, P, and K with numbers.'); + } + + body.temperature = t; + body.humidity = h; + body.ph = p; + body.N = n; + body.P = pp; + body.K = k; + if (r !== undefined) body.rainfall = r; + + setLoading(true); + try { + const res = await api.guestPredictCrop(body); + Alert.alert('Prediction', `Recommended crop: ${res.recommended_crop}\nConfidence: ${Math.round(res.confidence * 100)}%`); + } catch (e) { + Alert.alert('Prediction failed', e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }; + + const InputRow = ({ label, value, onChange, placeholder }: { label:string; value:string; onChange:(v:string)=>void; placeholder?:string }) => ( + + {label} + + + ); + + return ( + + + Manual Input + + + + + + + + + + {loading ? 'Predicting…' : 'Predict'} + + + + ); +} diff --git a/mobile/app/(app)/predict.tsx b/mobile/app/(app)/predict.tsx index e69de29..14eba94 100644 --- a/mobile/app/(app)/predict.tsx +++ b/mobile/app/(app)/predict.tsx @@ -0,0 +1,412 @@ +import React, { useState } from 'react' +import { + View, Text, ScrollView, TextInput, + TouchableOpacity, ActivityIndicator, Alert, +} from 'react-native' +import { LinearGradient } from 'expo-linear-gradient' +import { api } from '../../lib/api' +import { getErrorMessage } from '../../lib/error' +import { useAuthStore } from '../../store/authStore' +import { useSensorStore } from '../../store/sensorStore' + +type Mode = 'crop' | 'suitability' | 'fertilizer' + +const SOIL_TYPES = ['Black', 'Clayey', 'Loamy', 'Red', 'Sandy'] +const CROPS = ['Rice', 'Wheat', 'Maize', 'Cotton', 'Sugarcane', 'Banana', 'Mango', 'Apple', 'Grapes', 'Jute', 'Coffee'] + +interface FormValues { + N: string; P: string; K: string + temperature: string; humidity: string + ph: string; rainfall: string; moisture: string + cropName: string; soilType: string +} + +const defaultForm: FormValues = { + N: '', P: '', K: '', temperature: '', + humidity: '', ph: '', rainfall: '', + moisture: '', cropName: 'Wheat', soilType: 'Black', +} + +interface CropResult { recommended_crop: string; confidence: number } +interface SuitabilityTableRow { parameter: string; recommended: number; observed: number; status: string } +interface SuitabilityResult { crop: string; suitability_score: number; table: SuitabilityTableRow[] } +interface FertilizerResult { + fertilizer: string; composition: string; application: string + nitrogen_advice?: string; phosphorus_advice?: string; potassium_advice?: string +} +type PredictResult = CropResult | SuitabilityResult | FertilizerResult | null + +function isCropResult(r: PredictResult): r is CropResult { + return r !== null && 'recommended_crop' in r +} +function isSuitabilityResult(r: PredictResult): r is SuitabilityResult { + return r !== null && 'suitability_score' in r +} +function isFertilizerResult(r: PredictResult): r is FertilizerResult { + return r !== null && 'fertilizer' in r +} + +// Rounded-full field input matching stitch design +function FieldInput({ + label, value, onChange, unit, placeholder, +}: { + label: string; value: string; onChange: (v: string) => void; unit: string; placeholder?: string +}) { + return ( + + {label} + + + {unit} + + + ) +} + +function CropResultView({ result }: { result: CropResult }) { + const confidence = result.confidence ?? 0 + return ( + + + + + + Top Match + + + {result.recommended_crop} + + + + + {confidence.toFixed(0)}% + + + Confidence + + + + {/* Progress bar */} + + + + + + + + + ) +} + +function SuitabilityResultView({ result }: { result: SuitabilityResult }) { + return ( + + + + {result.crop} + + + Suitability Score: {result.suitability_score?.toFixed(1)}% + + + {result.table?.map((row, i) => ( + + {row.parameter} + {row.observed} / {row.recommended} + + {row.status} + + + ))} + + ) +} + +function FertilizerResultView({ result }: { result: FertilizerResult }) { + return ( + + + + + 🧪 + + + {result.fertilizer} + {result.composition} + + + {result.application} + + {result.nitrogen_advice && ( + + Nitrogen Advice + {result.nitrogen_advice} + + )} + {result.phosphorus_advice && ( + + Phosphorus Advice + {result.phosphorus_advice} + + )} + {result.potassium_advice && ( + + Potassium Advice + {result.potassium_advice} + + )} + + ) +} + +export default function PredictScreen() { + const { accessToken } = useAuthStore() + const { latest: sensorData } = useSensorStore() + + const [mode, setMode] = useState('crop') + const [form, setForm] = useState(defaultForm) + const [useSensor, setUseSensor] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [result, setResult] = useState(null) + + const setField = (field: keyof FormValues) => (value: string) => + setForm((prev) => ({ ...prev, [field]: value })) + + const fillFromSensor = () => { + if (!sensorData) return Alert.alert('No sensor data', 'Fetch data from the dashboard first.') + setForm((prev) => ({ + ...prev, + N: sensorData.N.toFixed(1), + P: sensorData.P.toFixed(1), + K: sensorData.K.toFixed(1), + temperature: sensorData.temperature.toFixed(1), + humidity: sensorData.humidity.toFixed(1), + ph: sensorData.ph.toFixed(1), + moisture: sensorData.moisture.toFixed(1), + })) + setUseSensor(true) + } + + const handleSubmit = async () => { + if (!accessToken) return + setIsLoading(true) + setResult(null) + const source = useSensor ? 'sensor' : 'manual' + const num = (v: string) => parseFloat(v) || 0 + + try { + let res: PredictResult + if (mode === 'crop') { + res = await api.predictCrop(accessToken, { + source, N: num(form.N), P: num(form.P), K: num(form.K), + temperature: num(form.temperature), humidity: num(form.humidity), + ph: num(form.ph), rainfall: num(form.rainfall), + }) + } else if (mode === 'suitability') { + if (!form.cropName) return Alert.alert('Error', 'Select a crop first.') + res = await api.predictSuitability(accessToken, { + source, crop_name: form.cropName, + N: num(form.N), P: num(form.P), K: num(form.K), + temperature: num(form.temperature), humidity: num(form.humidity), + ph: num(form.ph), rainfall: num(form.rainfall), + }) + } else { + res = await api.predictFertilizer(accessToken, { + source, crop_name: form.cropName, soil_type: form.soilType, + N: num(form.N), P: num(form.P), K: num(form.K), + temperature: num(form.temperature), humidity: num(form.humidity), + ph: num(form.ph), moisture: num(form.moisture), rainfall: num(form.rainfall), + }) + } + setResult(res) + } catch (err) { + Alert.alert('Prediction failed', getErrorMessage(err)) + } finally { + setIsLoading(false) + } + } + + const tabs: { id: Mode; label: string }[] = [ + { id: 'crop', label: 'Crop Recommendation' }, + { id: 'suitability', label: 'Suitability Analysis' }, + { id: 'fertilizer', label: 'Fertilizer Rec.' }, + ] + + return ( + + {/* Glassmorphic Top Nav */} + + + 🌱 + TerraDetect + + 🔔 + + + + + {/* Page Title */} + + + AI Predictions + + + Harnessing machine learning for precision agronomy. + + + + {/* Tab Navigation - Pill Style */} + + {tabs.map(tab => ( + { setMode(tab.id); setResult(null) }} + className={`px-5 py-3 rounded-full ${mode === tab.id + ? 'bg-primary-container' + : 'bg-surface-container-high' + }`} + > + + {tab.label} + + + ))} + + + {/* Sensor Auto-Fill Banner */} + {sensorData && ( + + Use live sensor data + Tap to fill → + + )} + + {/* Input Form Section */} + + + ⚙️ Soil Parameters + + + {/* Crop selector for suitability / fertilizer */} + {(mode === 'suitability' || mode === 'fertilizer') && ( + + Crop + + {CROPS.map((crop) => ( + setField('cropName')(crop)} + className={`px-4 py-2 rounded-full ${form.cropName === crop + ? 'bg-primary' + : 'bg-surface-container-high' + }`} + > + + {crop} + + + ))} + + + )} + + {/* Soil type selector for fertilizer */} + {mode === 'fertilizer' && ( + + Soil Type + + {SOIL_TYPES.map((soil) => ( + setField('soilType')(soil)} + className={`px-4 py-2 rounded-full ${form.soilType === soil ? 'bg-tertiary' : 'bg-surface-container-high'}`} + > + + {soil} + + + ))} + + + )} + + + + + + + + + + + + + + + + + + + + {mode === 'fertilizer' && ( + + )} + + {/* Signature Gradient Submit Button */} + + + {isLoading + ? + : + Run Analysis Engine + + } + + + + + {/* Results Section */} + {isCropResult(result) && } + {isSuitabilityResult(result) && } + {isFertilizerResult(result) && } + + + + ) +} \ No newline at end of file diff --git a/mobile/app/(auth)/landing.tsx b/mobile/app/(auth)/landing.tsx index 32dfb13..9996e36 100644 --- a/mobile/app/(auth)/landing.tsx +++ b/mobile/app/(auth)/landing.tsx @@ -1,45 +1,253 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import React from "react"; -import { View, Text, TouchableOpacity, SafeAreaView } from "react-native"; +import { View, Text, ScrollView, Pressable, Platform } from "react-native"; import { useRouter } from "expo-router"; +import { LinearGradient } from "expo-linear-gradient"; export default function LandingScreen() { - const router = useRouter(); - - return ( - - - {/* Simple Branding Icon */} - - 🌱 - - - - TerraDetect - - - Real-time soil monitoring and AI-powered crop recommendations. - - - - - router.push("/(auth)/login")} - activeOpacity={0.8} - className="bg-green-600 p-5 rounded-2xl items-center shadow-sm" - > - Sign In - - - router.push("/(auth)/register")} - activeOpacity={0.7} - className="bg-white border-2 border-green-600 p-5 rounded-2xl items-center" - > - - Create Account - - - - - ); + const router = useRouter(); + + // Wrapper that uses LinearGradient on native, plain View on web for reliability + const GradientHero = ({ children }: { children: React.ReactNode }) => { + if (Platform.OS === "web") { + // style uses a CSS string only valid on web; suppress the typed-any lint warning here + + return ( + + {children} + + ); + } + return ( + + {children} + + ); + }; + + const GradientButton = ({ onPress, children }: { onPress: () => void; children: React.ReactNode }) => { + if (Platform.OS === "web") { + // style uses a CSS string only valid on web; suppress the typed-any lint warning here + + return ( + + {children} + + ); + } + return ( + + + {children} + + + ); + }; + + return ( + + {/* ── Hero Section ── */} + + {/* Decorative blobs */} + + + + + {/* Editorial Badge */} + + + The Future of Agronomy + + + + {/* Logo */} + + 🌱 + + + {/* Headline */} + + Cultivate with{"\n"} + Precision Data. + + + {/* Subtitle */} + + Real-time soil monitoring and AI-powered crop recommendations. We bridge the gap between biological intuition and digital intelligence. + + + {/* Feature Pills */} + + + + 🌡️ Live Monitoring + + + + + 🤖 AI Predictions + + + + + 📊 Analytics + + + + + {/* Soil Moisture Widget */} + + + + 💧 + + + + Soil Moisture + + + 64.2% + + + + + + + + + + + {/* ── CTA Section ── */} + + router.push("/(auth)/login")}> + Get Started + + + + router.push("/(auth)/register")} + className="bg-surface-container-high dark:bg-surface-container-high-dark py-5 rounded-full items-center active:opacity-80" + > + + Create Account + + + + + Join 2,500+ agricultural innovators using TerraDetect + + + + {/* ── Feature Highlights ── */} + + + Ecosystem Intelligence + + {[ + { icon: "🔬", title: "Soil Health Monitoring", desc: "Track nitrogen, phosphorus, potassium and pH in real-time across your entire acreage." }, + { icon: "📈", title: "Predictive Analytics", desc: "AI forecasts yield outcomes with 94% accuracy using multi-decade weather patterns." }, + { icon: "🌾", title: "Crop Suitability", desc: "Suggests optimal crop rotation based on soil depletion and market demand." }, + ].map((feature) => ( + + + {feature.icon} + + + + {feature.title} + + + {feature.desc} + + + + ))} + + + {/* ── Testimonial ── */} + + " + + "TerraDetect has fundamentally changed how we view our land. It's no longer just dirt — it's a living data engine we can finally understand." + + + + 👩‍🔬 + + + Dr. Elena Thorne + + Lead Scientist, BioField + + + + + + {/* ── Dark CTA Banner ── */} + + + Ready to grow smarter? + + + Join 2,500+ agricultural innovators using TerraDetect to optimize soil health and maximize harvest yields. + + router.push("/(auth)/register")} + className="bg-white rounded-full px-10 py-4 items-center mb-4 active:opacity-90 w-full max-w-xs" + > + Create Free Account + + { }} + className="rounded-full px-10 py-4 items-center w-full max-w-xs" + style={{ borderWidth: 2, borderColor: "rgba(255,255,255,0.25)" }} + > + Schedule Demo + + + + {/* ── Footer ── */} + + + 🌱 + TerraDetect + + + Advancing the science of soil through data-driven precision and sustainable practices. + + + {[ + { title: "Platform", items: ["Soil Analysis", "Yield Prediction", "API & Integration"] }, + { title: "Research", items: ["Whitepapers", "Case Studies", "Field Reports"] }, + ].map((col) => ( + + {col.title} + {col.items.map((item) => ( + {item} + ))} + + ))} + + + + © 2024 TerraDetect Systems · All Rights Reserved + + + + ); } diff --git a/mobile/app/(auth)/login.tsx b/mobile/app/(auth)/login.tsx index 4ad84fc..393260d 100644 --- a/mobile/app/(auth)/login.tsx +++ b/mobile/app/(auth)/login.tsx @@ -1,70 +1,206 @@ -import React, { useState } from "react"; -import { View, Text, TextInput, TouchableOpacity, Alert } from "react-native"; -import { useRouter } from "expo-router"; -import { api } from "../../lib/api"; -import { useAuthStore } from "../../store/authStore"; +import React, { useState } from 'react'; +import { View, Text, ScrollView, Alert, Pressable, TextInput } from 'react-native'; +import { useRouter } from 'expo-router'; +import { LinearGradient } from 'expo-linear-gradient'; +import { api } from '../../lib/api'; +import { getErrorMessage } from '../../lib/error'; +import { useAuthStore } from '../../store/authStore'; export default function LoginScreen() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [loading, setLoading] = useState(false); - const router = useRouter(); - const loginToStore = useAuthStore((state) => state.login); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [usernameError, setUsernameError] = useState(''); + const [emailError, setEmailError] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [deviceId, setDeviceId] = useState(''); + const [deviceIdError, setDeviceIdError] = useState(''); + const [loading, setLoading] = useState(false); - const handleLogin = async () => { - if (!email || !password) - return Alert.alert("Error", "Please fill in all fields"); + const router = useRouter(); + const login = useAuthStore((state) => state.login); - setLoading(true); - try { - const data = await api.login({ email, password }); + const handleLogin = async () => { + setUsernameError(''); + setPasswordError(''); - // Your Go backend returns { access_token, refresh_token, user: { username, device_id } } - await loginToStore( - { accessToken: data.access_token, refreshToken: data.refresh_token }, - { username: data.user.username, deviceId: data.user.device_id }, - ); + let hasError = false; + // Require at least one of username or email + if (!username.trim() && !email.trim()) { + setUsernameError('Username or email is required'); + setEmailError('Username or email is required'); + hasError = true; + } + if (email && !/^\S+@\S+\.\S+$/.test(email)) { + setEmailError('Please enter a valid email address'); + hasError = true; + } + if (!password) { + setPasswordError('Password is required'); + hasError = true; + } else if (password.length < 8) { + setPasswordError('Password must be at least 8 characters'); + hasError = true; + } + if (hasError) return; - router.replace("/(app)/dashboard"); - } catch (err: any) { - Alert.alert("Login Failed", err.message); - } finally { - setLoading(false); - } - }; + // Validate device ID format if provided + if (deviceId && !/^[A-Z]{2}[0-9]{4}$/.test(deviceId)) { + setDeviceIdError('Device ID must be two uppercase letters followed by four digits (e.g. AB1234)'); + return + } - return ( - - - TerraDetect - + setLoading(true); + try { + const data = await api.login({ username, email, password, device_id: deviceId || '' }); + await login( + { accessToken: data.access_token, refreshToken: data.refresh_token }, + { username: data.user.username, deviceId: data.user.device_id } + ); + router.replace('/(app)/dashboard'); + } catch (err: unknown) { + Alert.alert('Login Failed', getErrorMessage(err)); + } finally { + setLoading(false); + } + }; - - - - - - - {loading ? "Authenticating..." : "Sign In"} - - - - - ); -} + {/* Decorative blobs */} + + + + {/* Identity Section */} + + + 🌱 + + + TerraDetect + + + Precision agronomy at your fingertips. + + + + {/* Login Form Card */} + + {/* Username Field */} + + + Username or Email + + + + { setUsername(t); setEmail(t); setUsernameError(''); setEmailError(''); }} + placeholder="username or email" + placeholderTextColor="#6e7b6c" + autoCapitalize="none" + autoCorrect={false} + className="flex-1 font-body text-base text-on-surface dark:text-on-surface-dark" + /> + 👤 + + + {usernameError ? {usernameError} : null} + {emailError ? {emailError} : null} + + + {/* Password Field */} + + + Password + + + { setPassword(t); setPasswordError(''); }} + placeholder="••••••••" + placeholderTextColor="#6e7b6c" + secureTextEntry={!showPassword} + className="flex-1 font-body text-base text-on-surface dark:text-on-surface-dark" + /> + setShowPassword(!showPassword)} className="ml-2 p-1"> + {showPassword ? '🙈' : '👁️'} + + + {passwordError ? {passwordError} : null} + + + {/* Device ID Field */} + + + Device ID + + + { setDeviceId(t.toUpperCase()); setDeviceIdError(''); }} + placeholder="AB1234" + placeholderTextColor="#6e7b6c" + autoCapitalize="characters" + className="flex-1 font-body text-base text-on-surface dark:text-on-surface-dark" + /> + 📡 + + {deviceIdError ? {deviceIdError} : null} + + Found on the back of your TerraDetect sensor. + + + + {/* Sign In Button */} + + + + {loading ? 'Signing in...' : 'Sign In'} + + {!loading && } + + + + {/* Registration Link */} + + router.push('/(auth)/register')}> + + Create Account → + + + + New to the field? Start your journey today. + + + + + {/* Editorial footer strip */} + + + THE FUTURE OF SOIL + + + + ); +} \ No newline at end of file diff --git a/mobile/app/(auth)/register.tsx b/mobile/app/(auth)/register.tsx index b7085a6..474a58d 100644 --- a/mobile/app/(auth)/register.tsx +++ b/mobile/app/(auth)/register.tsx @@ -1,111 +1,246 @@ import React, { useState } from "react"; -import { - View, - Text, - TextInput, - TouchableOpacity, - Alert, - ScrollView, -} from "react-native"; +import { View, Text, ScrollView, Alert, Pressable, TextInput } from "react-native"; import { useRouter } from "expo-router"; +import { LinearGradient } from "expo-linear-gradient"; import { api } from "../../lib/api"; +import { getErrorMessage } from "../../lib/error"; export default function RegisterScreen() { - const [form, setForm] = useState({ - username: "", - email: "", - password: "", - deviceId: "", - }); - const [loading, setLoading] = useState(false); - const router = useRouter(); - - const handleRegister = async () => { - if (!form.username || !form.email || !form.password) { - return Alert.alert( - "Error", - "Username, Email, and Password are required.", - ); - } - - setLoading(true); - try { - // Calling your Go backend via the api.ts utility - await api.register({ - username: form.username, - email: form.email, - password: form.password, - device_id: form.deviceId, - }); - - // Navigate to the success screen instead of a blocking alert - router.push("/(auth)/register-success"); - } catch (err: any) { - // Fallback to error message from your Gin middleware/handlers - Alert.alert("Registration Failed", err.message); - } finally { - setLoading(false); - } - }; - - return ( - - - Create Account - - - Join TerraDetect to start monitoring your soil. - - - - setForm({ ...form, username: val })} - /> - setForm({ ...form, email: val })} - /> - setForm({ ...form, password: val })} - /> - setForm({ ...form, deviceId: val })} - /> - - - - {loading ? "Creating Account..." : "Sign Up"} - - - - router.push("/(auth)/login")} - className="py-4" - > - - Already have an account?{" "} - Login - - - - - ); + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [deviceId, setDeviceId] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + const [usernameError, setUsernameError] = useState(""); + const [emailError, setEmailError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [deviceIdError, setDeviceIdError] = useState(""); + + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const validateEmail = (e: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); + + const calculatePasswordStrength = (pwd: string): { strength: number; label: string } => { + let s = 0; + if (pwd.length >= 8) s++; + if (pwd.length >= 12) s++; + if (/[A-Z]/.test(pwd)) s++; + if (/[0-9]/.test(pwd)) s++; + if (/[^A-Za-z0-9]/.test(pwd)) s++; + if (s <= 1) return { strength: 1, label: "Weak" }; + if (s <= 3) return { strength: 2, label: "Medium" }; + return { strength: 3, label: "Strong" }; + }; + + const passwordStrength = calculatePasswordStrength(password); + + const strengthColor = (idx: number) => { + if (passwordStrength.strength < idx) return 'bg-surface-container-high dark:bg-surface-container-high-dark'; + if (passwordStrength.strength === 1) return 'bg-error'; + if (passwordStrength.strength === 2) return 'bg-tertiary-fixed-dim'; + return 'bg-primary'; + }; + + const handleRegister = async () => { + setUsernameError(""); + setEmailError(""); + setPasswordError(""); + setDeviceIdError(""); + + let hasError = false; + if (!username.trim()) { setUsernameError("Username is required"); hasError = true; } + if (!email) { setEmailError("Email is required"); hasError = true; } + else if (!validateEmail(email)) { setEmailError("Please enter a valid email address"); hasError = true; } + if (!password) { setPasswordError("Password is required"); hasError = true; } + else if (password.length < 8) { setPasswordError("Password must be at least 8 characters"); hasError = true; } + if (hasError) return; + + // Validate device ID format if provided + if (deviceId && !/^[A-Z]{2}[0-9]{4}$/.test(deviceId)) { + setDeviceIdError('Device ID must be two uppercase letters followed by four digits (e.g. AB1234)'); + return + } + + setLoading(true); + try { + await api.register({ username, email, password, device_id: deviceId || "" }); + router.push("/(auth)/register-success"); + } catch (err: unknown) { + Alert.alert("Registration Failed", getErrorMessage(err)); + } finally { + setLoading(false); + } + }; + + return ( + + {/* Simplified glassmorphic header */} + + + 🌱 + TerraDetect + + + + + {/* Header */} + + + Begin Your Harvest + + + Join the network of modern cultivators and start monitoring your soil vitality today. + + + + {/* Form */} + + {/* Username */} + + Username + + 👤 + { setUsername(t); setUsernameError(""); }} + placeholder="e.g. GreenThumb92" + placeholderTextColor="#6e7b6c" + autoCapitalize="none" + className="flex-1 font-body text-base text-on-surface dark:text-on-surface-dark" + /> + + {usernameError ? {usernameError} : null} + + + {/* Email */} + + Email address + + ✉️ + { setEmail(t); setEmailError(""); }} + placeholder="your@field.com" + placeholderTextColor="#6e7b6c" + keyboardType="email-address" + autoCapitalize="none" + className="flex-1 font-body text-base text-on-surface dark:text-on-surface-dark" + /> + + {emailError ? {emailError} : null} + + + {/* Password */} + + Password + + 🔒 + { setPassword(t); setPasswordError(""); }} + placeholder="••••••••" + placeholderTextColor="#6e7b6c" + secureTextEntry={!showPassword} + className="flex-1 font-body text-base text-on-surface dark:text-on-surface-dark" + /> + setShowPassword(!showPassword)} className="ml-2 p-1"> + {showPassword ? '🙈' : '👁️'} + + + {passwordError ? {passwordError} : null} + + {/* Password Strength Meter */} + {password.length > 0 && ( + + + + Security Level + + + {passwordStrength.label} + + + + + + + + + Must contain at least 8 characters, one number, and a symbol. + + + )} + + + {/* Device ID (Optional) */} + + + Device ID + + Optional + + + + 📡 + { setDeviceId(t.toUpperCase()); setDeviceIdError(""); }} + placeholder="AB1234" + placeholderTextColor="#6e7b6c" + autoCapitalize="characters" + className="flex-1 font-body text-base text-on-surface dark:text-on-surface-dark" + /> + + {deviceIdError ? {deviceIdError} : null} + + + If you provide a pre-provisioned device ID here and it is valid, the device will be claimed and an API key will be returned once. Leave empty to register without a device and claim later. + + + + {/* Sign Up Button */} + + + + {loading ? "Creating Account..." : "Sign Up"} + + + + + + {/* Bottom Link */} + + + Already have an account? + + router.push("/(auth)/login")}> + + Login + + + + + {/* Footer icons (Organic Brutalism touch) */} + + {['🌿', '🌾', '🍃', '🌱'].map((icon) => ( + {icon} + ))} + + + © 2024 TerraDetect Systems • Privacy Focused + + + + ); } diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index ed0b08b..320c099 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -2,6 +2,21 @@ import { useEffect, useState } from "react"; import { Stack, useRouter, useSegments } from "expo-router"; import { useAuthStore } from "../store/authStore"; import { View, ActivityIndicator } from "react-native"; +import { useFonts } from "expo-font"; +import { + Manrope_400Regular, + Manrope_600SemiBold, + Manrope_700Bold, + Manrope_800ExtraBold, +} from "@expo-google-fonts/manrope"; +import { + Inter_400Regular, + Inter_500Medium, + Inter_600SemiBold, + Inter_700Bold, +} from "@expo-google-fonts/inter"; +import { ThemeProvider } from "../contexts/ThemeContext"; +import "../global.css"; export default function RootLayout() { const { accessToken, loadFromStorage } = useAuthStore(); @@ -9,14 +24,31 @@ export default function RootLayout() { const router = useRouter(); const [isReady, setIsReady] = useState(false); - // 1. Initialize Auth State + // Load custom fonts + const [fontsLoaded] = useFonts({ + // Manrope for headlines and display text + Manrope_400Regular, + Manrope_600SemiBold, + Manrope_700Bold, + Manrope_800ExtraBold, + // Inter for body text and labels + Inter_400Regular, + Inter_500Medium, + Inter_600SemiBold, + Inter_700Bold, + }); + + // 1. Initialize Auth State and wait for fonts useEffect(() => { const initialize = async () => { await loadFromStorage(); - setIsReady(true); + // Wait for fonts to load before marking as ready + if (fontsLoaded) { + setIsReady(true); + } }; initialize(); - }, []); + }, [fontsLoaded]); // 2. Auth Guard Logic useEffect(() => { @@ -33,26 +65,28 @@ export default function RootLayout() { } }, [accessToken, segments, isReady]); - // 3. Loading State (Prevents flicker during hydration) - if (!isReady) { + // 3. Loading State (Prevents flicker during hydration and font loading) + if (!isReady || !fontsLoaded) { return ( - + ); } return ( - - - - + + + + + + ); } diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx new file mode 100644 index 0000000..ea3a235 --- /dev/null +++ b/mobile/app/index.tsx @@ -0,0 +1,13 @@ +import { Redirect } from 'expo-router'; +import { useAuthStore } from '../store/authStore'; + +export default function Index() { + const accessToken = useAuthStore((state) => state.accessToken); + + // Redirect to dashboard if authenticated, otherwise to landing + if (accessToken) { + return ; + } + + return ; +} diff --git a/mobile/babel.config.js b/mobile/babel.config.js index f3c649b..ce24065 100644 --- a/mobile/babel.config.js +++ b/mobile/babel.config.js @@ -1,9 +1,6 @@ module.exports = function (api) { api.cache(true); return { - presets: [ - ["babel-preset-expo", { jsxImportSource: "nativewind" }], - "nativewind/babel", - ], + presets: ["babel-preset-expo", "nativewind/babel"], }; }; diff --git a/mobile/components/Button.tsx b/mobile/components/Button.tsx new file mode 100644 index 0000000..fef49e6 --- /dev/null +++ b/mobile/components/Button.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Pressable, Text, type PressableProps } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); + +export interface ButtonProps extends Omit { + variant?: 'primary' | 'secondary' | 'tertiary'; + children: React.ReactNode; + disabled?: boolean; + fullWidth?: boolean; + className?: string; +} + +export const Button: React.FC = ({ + variant = 'primary', + children, + disabled = false, + fullWidth = false, + className = '', + onPress, + ...props +}) => { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + const handlePressIn = () => { + scale.value = withTiming(0.95, { duration: 150 }); + }; + + const handlePressOut = () => { + scale.value = withTiming(1, { duration: 150 }); + }; + + // Base styles for all variants + const baseClasses = ` + min-h-[44px] min-w-[44px] + rounded-full + items-center justify-center + px-6 py-3 + ${fullWidth ? 'w-full' : ''} + ${disabled ? 'opacity-40' : ''} + ${className} + `.trim(); + + // Render primary variant with gradient + if (variant === 'primary') { + return ( + + + + {children} + + + ); + } + + // Render secondary variant + if (variant === 'secondary') { + return ( + + + {children} + + + ); + } + + // Render tertiary variant + return ( + + + {children} + + + ); +}; diff --git a/mobile/components/Card.tsx b/mobile/components/Card.tsx new file mode 100644 index 0000000..f468442 --- /dev/null +++ b/mobile/components/Card.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { View, Pressable, type PressableProps } from 'react-native'; + +export interface CardProps extends Omit { + children: React.ReactNode; + variant?: 'default' | 'elevated' | 'outlined'; + padding?: 'sm' | 'md' | 'lg'; + onPress?: () => void; + className?: string; +} + +export const Card: React.FC = ({ + children, + variant = 'default', + padding = 'md', + onPress, + className = '', + ...props +}) => { + // Padding variants + const paddingClasses = { + sm: 'p-4', // 16px + md: 'p-6', // 24px + lg: 'p-8', // 32px + }; + + // Variant styles + const variantClasses = { + default: 'bg-surface-container-lowest dark:bg-surface-container-lowest-dark', + elevated: 'bg-surface-container-low dark:bg-surface-container-low-dark shadow-sm', + outlined: 'bg-transparent border border-outline-variant dark:border-outline-variant-dark', + }; + + // Base classes for all cards + const baseClasses = ` + rounded-lg + ${paddingClasses[padding]} + ${variantClasses[variant]} + ${className} + `.trim(); + + // If onPress is provided, render as Pressable + if (onPress) { + return ( + + {children} + + ); + } + + // Otherwise, render as View + return ( + + {children} + + ); +}; diff --git a/mobile/components/HealthMeter.tsx b/mobile/components/HealthMeter.tsx new file mode 100644 index 0000000..6bf51ea --- /dev/null +++ b/mobile/components/HealthMeter.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { View, Text } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; + +export interface HealthMeterProps { + value: number; // Current value (e.g., 0-14 for pH scale) + min: number; // Minimum value + max: number; // Maximum value + optimal: [number, number]; // Optimal range [min, max] + label: string; // Label for the meter + className?: string; +} + +export const HealthMeter: React.FC = ({ + value, + min, + max, + optimal, + label, + className = '', +}) => { + // Calculate percentage position for the current value indicator + const valuePercentage = ((value - min) / (max - min)) * 100; + + // Gradient colors: error → tertiary_fixed_dim → primary → error + // Using color values from the design system + const gradientColors = ['#ba1a1a', '#ffb95f', '#006b2c', '#ba1a1a'] as const; + + const gradientLocations = [0, 0.3, 0.7, 1] as const; + + return ( + + {/* Current value indicator positioned above bar */} + + + + {value.toFixed(1)} + + + {/* Arrow pointing down */} + + + + {/* Gradient bar */} + + + + + {/* Labels positioned below bar */} + + + {min} + + + {label} + + + {max} + + + + {/* Optimal range indicator */} + + + Optimal: {optimal[0]}-{optimal[1]} + + + + ); +}; diff --git a/mobile/components/Input.tsx b/mobile/components/Input.tsx new file mode 100644 index 0000000..e548433 --- /dev/null +++ b/mobile/components/Input.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { View, Text, TextInput, type TextInputProps, type KeyboardTypeOptions } from 'react-native'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; + +const AnimatedView = Animated.createAnimatedComponent(View); + +export interface InputProps extends Omit { + label: string; + value: string; + onChangeText: (text: string) => void; + placeholder?: string; + secureTextEntry?: boolean; + error?: string; + keyboardType?: KeyboardTypeOptions; + className?: string; +} + +export const Input: React.FC = ({ + label, + value, + onChangeText, + placeholder, + secureTextEntry = false, + error, + keyboardType = 'default', + className = '', + ...props +}) => { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + const handleFocus = () => { + scale.value = withTiming(1.01, { duration: 200 }); + }; + + const handleBlur = () => { + scale.value = withTiming(1, { duration: 200 }); + }; + + return ( + + {/* Label positioned 8px above input */} + {label && ( + + {label} + + )} + + {/* Input container with focus animation */} + + + + + {/* Error message */} + {error && ( + + {error} + + )} + + ); +}; diff --git a/mobile/components/SensorChip.tsx b/mobile/components/SensorChip.tsx new file mode 100644 index 0000000..d0703fa --- /dev/null +++ b/mobile/components/SensorChip.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { View, Text } from 'react-native'; + +export interface SensorChipProps { + label: string; + type: 'soil' | 'weather' | 'nutrient'; + value: string; + icon?: string; + className?: string; +} + +export const SensorChip: React.FC = ({ + label, + type, + value, + icon, + className = '', +}) => { + // Map type to background and text colors + const typeStyles = { + weather: { + bg: 'bg-secondary-container dark:bg-secondary-container-dark', + text: 'text-on-secondary-container dark:text-on-secondary-container-dark', + }, + soil: { + bg: 'bg-tertiary-container dark:bg-tertiary-container-dark', + text: 'text-on-tertiary-container dark:text-on-tertiary-container-dark', + }, + nutrient: { + bg: 'bg-primary-container dark:bg-primary-container-dark', + text: 'text-on-primary-container dark:text-on-primary-container-dark', + }, + }; + + const styles = typeStyles[type]; + + return ( + + {icon && ( + + {icon} + + )} + + {label}: {value} + + + ); +}; diff --git a/mobile/components/StatCard.tsx b/mobile/components/StatCard.tsx new file mode 100644 index 0000000..f139416 --- /dev/null +++ b/mobile/components/StatCard.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { View, Text } from 'react-native'; + +export interface StatCardProps { + label: string; + value: string; + icon: string; + trend?: string; // e.g., "+2.1%" or "-1.5%" + color: 'orange' | 'blue' | 'green' | 'purple' | 'amber'; + className?: string; +} + +export const StatCard: React.FC = ({ + label, + value, + icon, + trend, + color, + className = '', +}) => { + // Map color variants to background tints + const colorStyles = { + orange: 'bg-orange-50/80 dark:bg-orange-900/30', + blue: 'bg-blue-50/80 dark:bg-blue-900/30', + green: 'bg-green-50/80 dark:bg-green-900/30', + purple: 'bg-purple-50/80 dark:bg-purple-900/30', + amber: 'bg-amber-50/80 dark:bg-amber-900/30', + }; + + // Map color variants to icon container colors + const iconContainerStyles = { + orange: 'bg-orange-500', + blue: 'bg-blue-500', + green: 'bg-green-500', + purple: 'bg-purple-500', + amber: 'bg-amber-500', + }; + + return ( + + {/* Icon in white rounded container */} + + + {icon} + + + + {/* Value with Manrope font */} + + {value} + + + {/* Label - uppercase, tracking-wider */} + + {label} + + + {/* Optional trend indicator */} + {trend && ( + + {trend} + + )} + + ); +}; diff --git a/mobile/components/__tests__/specialized-components.test.tsx b/mobile/components/__tests__/specialized-components.test.tsx new file mode 100644 index 0000000..1675b86 --- /dev/null +++ b/mobile/components/__tests__/specialized-components.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { SensorChip, HealthMeter, StatCard } from '../index'; + +describe('Specialized Components', () => { + describe('SensorChip', () => { + it('renders with soil type', () => { + const { getByText } = render( + + ); + expect(getByText('pH: 6.5')).toBeTruthy(); + }); + + it('renders with weather type', () => { + const { getByText } = render( + + ); + expect(getByText('Temp: 25°C')).toBeTruthy(); + }); + + it('renders with nutrient type', () => { + const { getByText } = render( + + ); + expect(getByText('Nitrogen: 45 mg/kg')).toBeTruthy(); + }); + }); + + describe('HealthMeter', () => { + it('renders with pH scale values', () => { + const { getByText } = render( + + ); + expect(getByText('pH Level')).toBeTruthy(); + expect(getByText('6.5')).toBeTruthy(); + expect(getByText('Optimal: 6-7.5')).toBeTruthy(); + }); + + it('renders with custom range', () => { + const { getByText } = render( + + ); + expect(getByText('Humidity')).toBeTruthy(); + expect(getByText('50.0')).toBeTruthy(); + }); + }); + + describe('StatCard', () => { + it('renders with all required elements', () => { + const { getByText } = render( + + ); + expect(getByText('TEMPERATURE')).toBeTruthy(); + expect(getByText('25°C')).toBeTruthy(); + expect(getByText('🌡️')).toBeTruthy(); + }); + + it('renders with trend indicator', () => { + const { getByText } = render( + + ); + expect(getByText('HUMIDITY')).toBeTruthy(); + expect(getByText('65%')).toBeTruthy(); + expect(getByText('+2.1%')).toBeTruthy(); + }); + + it('renders all color variants', () => { + const colors: Array<'orange' | 'blue' | 'green' | 'purple' | 'amber'> = [ + 'orange', + 'blue', + 'green', + 'purple', + 'amber', + ]; + + colors.forEach((color) => { + const { getByText } = render( + + ); + expect(getByText('TEST')).toBeTruthy(); + }); + }); + }); +}); diff --git a/mobile/components/index.ts b/mobile/components/index.ts new file mode 100644 index 0000000..b0b0c11 --- /dev/null +++ b/mobile/components/index.ts @@ -0,0 +1,6 @@ +export { Button, type ButtonProps } from './Button'; +export { Input, type InputProps } from './Input'; +export { Card, type CardProps } from './Card'; +export { SensorChip, type SensorChipProps } from './SensorChip'; +export { HealthMeter, type HealthMeterProps } from './HealthMeter'; +export { StatCard, type StatCardProps } from './StatCard'; diff --git a/mobile/contexts/ThemeContext.tsx b/mobile/contexts/ThemeContext.tsx new file mode 100644 index 0000000..8e41fe2 --- /dev/null +++ b/mobile/contexts/ThemeContext.tsx @@ -0,0 +1,259 @@ +import React, { createContext, useContext, useEffect, useState, useMemo } from "react"; +import { useThemeStore } from "../store/themeStore"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + interpolateColor, +} from "react-native-reanimated"; +import { useColorScheme as useNativeWindColorScheme } from "nativewind"; + +type ThemeMode = "light" | "dark"; + +// Color tokens extracted from tailwind.config.js +interface ColorTokens { + primary: string; + primaryContainer: string; + onPrimary: string; + onPrimaryContainer: string; + primaryFixed: string; + primaryFixedDim: string; + secondary: string; + secondaryContainer: string; + onSecondary: string; + onSecondaryContainer: string; + secondaryFixed: string; + secondaryFixedDim: string; + tertiary: string; + tertiaryContainer: string; + onTertiary: string; + onTertiaryContainer: string; + tertiaryFixed: string; + tertiaryFixedDim: string; + error: string; + errorContainer: string; + onError: string; + onErrorContainer: string; + success: string; + successContainer: string; + onSuccess: string; + onSuccessContainer: string; + warning: string; + warningContainer: string; + onWarning: string; + onWarningContainer: string; + info: string; + infoContainer: string; + onInfo: string; + onInfoContainer: string; + surface: string; + surfaceDim: string; + surfaceBright: string; + surfaceContainerLowest: string; + surfaceContainerLow: string; + surfaceContainer: string; + surfaceContainerHigh: string; + surfaceContainerHighest: string; + surfaceVariant: string; + onSurface: string; + onSurfaceVariant: string; + outline: string; + outlineVariant: string; + inverseSurface: string; + inverseOnSurface: string; + inversePrimary: string; + scrim: string; + shadow: string; +} + +interface ThemeContextValue { + theme: ThemeMode; + toggleTheme: () => void; + isLoading: boolean; + colors: ColorTokens; +} + +const ThemeContext = createContext(undefined); + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within ThemeProvider"); + } + return context; +} + +interface ThemeProviderProps { + children: React.ReactNode; +} + +export function ThemeProvider({ children }: ThemeProviderProps) { + const { mode, toggleMode, initializeTheme } = useThemeStore(); + const [isLoading, setIsLoading] = useState(true); + const { setColorScheme } = useNativeWindColorScheme(); + + // Animated value for smooth transitions (0 = light, 1 = dark) + const themeProgress = useSharedValue(0); + + // Initialize theme on mount + useEffect(() => { + const initialize = async () => { + await initializeTheme(); + setIsLoading(false); + }; + initialize(); + }, []); + + // Update NativeWind color scheme when theme changes + useEffect(() => { + setColorScheme(mode); + }, [mode, setColorScheme]); + + // Animate theme transitions with 300ms duration + useEffect(() => { + themeProgress.value = withTiming(mode === "dark" ? 1 : 0, { + duration: 300, + }); + }, [mode]); + + // Animated style for smooth color transitions + const animatedStyle = useAnimatedStyle(() => { + return { + backgroundColor: interpolateColor( + themeProgress.value, + [0, 1], + ["#f8f9fa", "#0c1324"] // surface colors from design system + ), + }; + }); + + // Extract color tokens based on current theme + const colors = useMemo(() => { + if (mode === "dark") { + return { + primary: "#62df7d", + primaryContainer: "#1ca64d", + onPrimary: "#003916", + onPrimaryContainer: "#a8f5b8", + primaryFixed: "#a8f5b8", + primaryFixedDim: "#62df7d", + secondary: "#7bd0ff", + secondaryContainer: "#004c6e", + onSecondary: "#003549", + onSecondaryContainer: "#c8e6ff", + secondaryFixed: "#c8e6ff", + secondaryFixedDim: "#7bd0ff", + tertiary: "#ffb95f", + tertiaryContainer: "#5f3d00", + onTertiary: "#452b00", + onTertiaryContainer: "#ffddb8", + tertiaryFixed: "#ffddb8", + tertiaryFixedDim: "#ffb95f", + error: "#ffb4ab", + errorContainer: "#93000a", + onError: "#690005", + onErrorContainer: "#ffdad6", + success: "#62df7d", + successContainer: "#1ca64d", + onSuccess: "#003916", + onSuccessContainer: "#a8f5b8", + warning: "#ffb95f", + warningContainer: "#5f3d00", + onWarning: "#452b00", + onWarningContainer: "#ffddb8", + info: "#7bd0ff", + infoContainer: "#004c6e", + onInfo: "#003549", + onInfoContainer: "#c8e6ff", + surface: "#0c1324", + surfaceDim: "#0c1324", + surfaceBright: "#33394c", + surfaceContainerLowest: "#070d1f", + surfaceContainerLow: "#191f31", + surfaceContainer: "#1d2333", + surfaceContainerHigh: "#272d3e", + surfaceContainerHighest: "#323849", + surfaceVariant: "#414946", + onSurface: "#e1e2e3", + onSurfaceVariant: "#bdcaba", + outline: "#8b938a", + outlineVariant: "#414946", + inverseSurface: "#e1e2e3", + inverseOnSurface: "#2e3132", + inversePrimary: "#006b2c", + scrim: "#000000", + shadow: "#000000", + }; + } else { + return { + primary: "#006b2c", + primaryContainer: "#00873a", + onPrimary: "#ffffff", + onPrimaryContainer: "#f7fff2", + primaryFixed: "#a8f5b8", + primaryFixedDim: "#62df7d", + secondary: "#006398", + secondaryContainer: "#c8e6ff", + onSecondary: "#ffffff", + onSecondaryContainer: "#001e30", + secondaryFixed: "#c8e6ff", + secondaryFixedDim: "#7bd0ff", + tertiary: "#825100", + tertiaryContainer: "#ffddb8", + onTertiary: "#ffffff", + onTertiaryContainer: "#2a1700", + tertiaryFixed: "#ffddb8", + tertiaryFixedDim: "#ffb95f", + error: "#ba1a1a", + errorContainer: "#ffdad6", + onError: "#ffffff", + onErrorContainer: "#410002", + success: "#006b2c", + successContainer: "#a8f5b8", + onSuccess: "#ffffff", + onSuccessContainer: "#003916", + warning: "#825100", + warningContainer: "#ffddb8", + onWarning: "#ffffff", + onWarningContainer: "#2a1700", + info: "#006398", + infoContainer: "#c8e6ff", + onInfo: "#ffffff", + onInfoContainer: "#001e30", + surface: "#f8f9fa", + surfaceDim: "#d9dadc", + surfaceBright: "#f8f9fa", + surfaceContainerLowest: "#ffffff", + surfaceContainerLow: "#f2f3f5", + surfaceContainer: "#ecedee", + surfaceContainerHigh: "#e6e7e9", + surfaceContainerHighest: "#e1e2e3", + surfaceVariant: "#dde4dd", + onSurface: "#191c1d", + onSurfaceVariant: "#414946", + outline: "#717970", + outlineVariant: "#c1c9c0", + inverseSurface: "#2e3132", + inverseOnSurface: "#f0f1f2", + inversePrimary: "#62df7d", + scrim: "#000000", + shadow: "#000000", + }; + } + }, [mode]); + + const contextValue: ThemeContextValue = { + theme: mode, + toggleTheme: toggleMode, + isLoading, + colors, + }; + + return ( + + + {children} + + + ); +} diff --git a/mobile/eslint.config.mts b/mobile/eslint.config.mts new file mode 100644 index 0000000..82aebe0 --- /dev/null +++ b/mobile/eslint.config.mts @@ -0,0 +1,10 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { ignores: ["babel.config.js"] }, + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } }, + tseslint.configs.recommended, +]); diff --git a/mobile/index.ts b/mobile/index.ts index 1d6e981..5b83418 100644 --- a/mobile/index.ts +++ b/mobile/index.ts @@ -1,8 +1 @@ -import { registerRootComponent } from 'expo'; - -import App from './App'; - -// registerRootComponent calls AppRegistry.registerComponent('main', () => App); -// It also ensures that whether you load the app in Expo Go or in a native build, -// the environment is set up appropriately -registerRootComponent(App); +import 'expo-router/entry'; diff --git a/mobile/lib/api.ts b/mobile/lib/api.ts index 4874593..390b8d5 100644 --- a/mobile/lib/api.ts +++ b/mobile/lib/api.ts @@ -1,50 +1,350 @@ -const BASE = - process.env.EXPO_PUBLIC_BASE_URL || "https://terradetect.onrender.com"; +/* eslint-disable */ +import { useAuthStore } from "../store/authStore"; +import type { SensorReading } from "../store/sensorStore"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import NetInfo from "@react-native-community/netinfo"; -async function request( +const BASE = process.env.EXPO_PUBLIC_API_URL ?? "http://localhost:8080"; + +// ── Interfaces ────────────────────────────────────────────────────────────── + +export interface User { + username: string; + email: string; + device_id: string; +} + +export interface AuthResponse { + access_token: string; + refresh_token: string; + user: User; +} + +export interface SensorData { + temperature: number; + humidity: number; + ph: number; + N: number; + P: number; + K: number; + rainfall: number; + timestamp: string; +} + +export interface Pagination { + page: number; + per_page: number; + total: number; + total_pages: number; +} + +export interface SensorHistoryResponse { + history: SensorReading[]; + pagination: Pagination; +} + +export interface CropResult { + recommended_crop: string; + confidence: number; +} + +export interface FertilizerResult { + fertilizer: string; + composition: string; + application: string; + nitrogen_advice?: string; + phosphorus_advice?: string; + potassium_advice?: string; +} + +export interface SuitabilityResponse { + crop: string; + suitability_score: number; + table: { + parameter: string; + recommended: number; + observed: number; + status: "optimal" | "low" | "high"; + remarks: string; + }[]; + recommendations: string[]; +} + +// ── Core Request Helper ────────────────────────────────────────────────────── + +// Simple helpers +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +async function isNetworkAvailable(): Promise { + try { + const state = await NetInfo.fetch(); + return state.isConnected ?? false; + } catch { + return true; // assume online if NetInfo not available + } +} + +type OutboxItem = { + id: string; + path: string; + opts: RequestInit; + token?: string | null; + tries: number; + createdAt: number; +}; + +const OUTBOX_KEY = "api_outbox_v1"; +const CACHE_PREFIX = "api_cache_v1:"; + +async function enqueueOutbox(item: OutboxItem) { + try { + const raw = await AsyncStorage.getItem(OUTBOX_KEY); + const arr: OutboxItem[] = raw ? JSON.parse(raw) : []; + arr.push(item); + await AsyncStorage.setItem(OUTBOX_KEY, JSON.stringify(arr)); + } catch (e) { + console.warn("Failed to enqueue outbox item", e); + } +} + +async function replayOutbox() { + try { + const raw = await AsyncStorage.getItem(OUTBOX_KEY); + const arr: OutboxItem[] = raw ? JSON.parse(raw) : []; + if (!arr.length) return; + + const remaining: OutboxItem[] = []; + for (const item of arr) { + try { + const headers: Record = { + "Content-Type": "application/json", + ...(item.opts.headers as Record | undefined), + }; + if (item.token) headers["Authorization"] = `Bearer ${item.token}`; + + const res = await fetch(`${BASE}${item.path}`, { + ...item.opts, + headers, + }); + if (!res.ok) { + item.tries = (item.tries || 0) + 1; + if (item.tries < 5) remaining.push(item); + } + } catch { + item.tries = (item.tries || 0) + 1; + if (item.tries < 5) remaining.push(item); + } + } + await AsyncStorage.setItem(OUTBOX_KEY, JSON.stringify(remaining)); + } catch (e) { + console.warn("Failed replaying outbox", e); + } +} + +// Start a NetInfo listener to replay when online +NetInfo.addEventListener((state) => { + if (state.isConnected) { + void replayOutbox(); + } +}); + +async function request( path: string, opts: RequestInit = {}, token?: string | null, -) { + isRetry = false, +): Promise { + const method = (opts.method ?? "GET").toUpperCase(); const headers: Record = { "Content-Type": "application/json", ...(opts.headers as Record), }; - if (token) headers["Authorization"] = `Bearer ${token}`; - const res = await fetch(`${BASE}${path}`, { ...opts, headers }); - const json = await res.json(); - if (!res.ok) throw new Error(json?.error?.message ?? "Request failed"); + const cacheKey = `${CACHE_PREFIX}${path}`; + const maxRetries = 3; + let attempt = 0; + + while (true) { + attempt++; + try { + const res = await fetch(`${BASE}${path}`, { ...opts, headers }); + + // Handle auth refresh + if (res.status === 401 && token && !isRetry) { + const store = useAuthStore.getState(); + try { + const refreshed = await request<{ access_token: string }>( + "/api/v1/auth/refresh", + { + method: "POST", + body: JSON.stringify({ refresh_token: store.refreshToken }), + }, + ); + useAuthStore.setState({ accessToken: refreshed.access_token }); + return request(path, opts, refreshed.access_token, true); + } catch (refreshErr: unknown) { + await store.logout(); + const symptom = new Error("Session expired. Please log in again."); + (symptom as unknown as Record).cause = refreshErr; + // eslint-disable-next-line preserve-caught-error, sonarjs/preserve-caught-error + throw symptom; + } + } + + // Parse JSON safely + let json: unknown = undefined; + try { + json = await res.json(); + } catch { + json = undefined; + } + + if (!res.ok) { + // For server errors, attempt retry with backoff + if (res.status >= 500 && attempt <= maxRetries) { + await sleep(200 * attempt); + continue; + } + // Try to extract nested error message if present (safe checks) + let nestedMessage: string | undefined = undefined; + if (json && typeof json === "object") { + const obj = json as Record; + const errField = obj["error"]; + if (errField && typeof errField === "object") { + const errObj = errField as Record; + const msg = errObj["message"]; + if (typeof msg === "string") nestedMessage = msg; + } + } + throw new Error(nestedMessage ?? `Error ${res.status}`); + } - return json; + // Successful GET -> cache response + if (method === "GET") { + try { + await AsyncStorage.setItem(cacheKey, JSON.stringify(json)); + } catch (e) { + console.warn("Failed to cache response", e); + } + } + + return json as T; + } catch (err: unknown) { + const online = await isNetworkAvailable(); + // If offline and GET, return cached value if available + if (!online) { + if (method === "GET") { + try { + const cached = await AsyncStorage.getItem(cacheKey); + if (cached) return JSON.parse(cached) as T; + } catch (e) { + console.warn("Failed to read cache", e); + } + } + + // For non-GET (mutating) requests, enqueue and inform caller + if (method !== "GET") { + const outItem: OutboxItem = { + id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + path, + opts, + token, + tries: 0, + createdAt: Date.now(), + }; + await enqueueOutbox(outItem); + // preserve original error as cause (wrap non-Error values) + { + const symptom = new Error( + "Offline — request queued and will be sent when online.", + ); + (symptom as unknown as Record).cause = err; + // eslint-disable-next-line preserve-caught-error, sonarjs/preserve-caught-error + throw symptom; + } + } + + // If GET and no cache, fall through to retry/backoff + } + + // If transient network error, retry with exponential backoff + if (attempt <= maxRetries) { + await sleep(200 * attempt); + continue; + } + + // Final failure: if GET try cache one last time + if (method === "GET") { + try { + const cached = await AsyncStorage.getItem(cacheKey); + if (cached) return JSON.parse(cached) as T; + } catch (e) { + console.warn("Failed to read cache", e); + } + } + + if (err instanceof Error) throw err; + { + const symptom = new Error(String(err)); + (symptom as unknown as Record).cause = err; + // eslint-disable-next-line preserve-caught-error, sonarjs/preserve-caught-error + throw symptom; + } + } + } } +// ── Public API ─────────────────────────────────────────────────────────────── + export const api = { login: (body: object) => - request("/api/v1/auth/login", { + request("/api/v1/auth/login", { method: "POST", body: JSON.stringify(body), }), register: (body: object) => - request("/api/v1/auth/register", { + request("/api/v1/auth/register", { method: "POST", body: JSON.stringify(body), }), - refresh: (refreshToken: string) => - request("/api/v1/auth/refresh", { - method: "POST", - body: JSON.stringify({ refresh_token: refreshToken }), - }), - latestSensor: (token: string) => request("/api/v1/sensor/latest", {}, token), - sensorHistory: (token: string, page = 1) => - request(`/api/v1/sensor/history?page=${page}&per_page=20`, {}, token), + latestSensor: (token: string) => + request("/api/v1/sensor/latest", {}, token), + sensorHistory: (token: string, page: number, perPage: number) => + request( + `/api/v1/sensor/history?page=${page}&per_page=${perPage}`, + {}, + token, + ), predictCrop: (token: string, body: object) => - request( + request( + "/api/v1/predict/crop", + { method: "POST", body: JSON.stringify(body) }, + token, + ), + // Guest/anonymous prediction API (no auth token required) + guestPredictCrop: (body: object) => + request( "/api/v1/predict/crop", { method: "POST", body: JSON.stringify(body) }, + undefined, + ), + predictFertilizer: (token: string, body: object) => + request( + "/api/v1/predict/fertilizer", + { method: "POST", body: JSON.stringify(body) }, token, ), - weather: (token: string, lat: number, lon: number) => - request(`/api/v1/weather?lat=${lat}&lon=${lon}`, {}, token), + predictSuitability: (token: string, body: object) => + request( + "/api/v1/predict/suitability", + { method: "POST", body: JSON.stringify(body) }, + token, + ), + logout: (refreshToken: string) => + request( + "/api/v1/auth/logout", + { method: "POST", body: JSON.stringify({ refresh_token: refreshToken }) }, + null, + ), }; diff --git a/mobile/lib/error.ts b/mobile/lib/error.ts new file mode 100644 index 0000000..57c4212 --- /dev/null +++ b/mobile/lib/error.ts @@ -0,0 +1,14 @@ +/** + * Extracts a human-readable message from an unknown catch value. + * Use this in every catch block instead of `err: any`. + * + * Usage: + * } catch (err) { + * Alert.alert('Failed', getErrorMessage(err)) + * } + */ +export function getErrorMessage(err: unknown): string { + if (err instanceof Error) return err.message; + if (typeof err === "string") return err; + return "An unexpected error occurred."; +} diff --git a/mobile/metro.config.js b/mobile/metro.config.js new file mode 100644 index 0000000..5efd76f --- /dev/null +++ b/mobile/metro.config.js @@ -0,0 +1,7 @@ +/* eslint-disable */ +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativeWind } = require("nativewind/metro"); + +const config = getDefaultConfig(__dirname); + +module.exports = withNativeWind(config, { input: "./global.css" }); diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 524fba1..42809d6 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -8,21 +8,54 @@ "name": "mobile", "version": "1.0.0", "dependencies": { + "@expo-google-fonts/inter": "^0.4.2", + "@expo-google-fonts/manrope": "^0.4.2", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/netinfo": "11.5.2", + "@react-navigation/native": "^7.2.1", "expo": "~55.0.8", + "expo-constants": "^55.0.9", + "expo-font": "^55.0.4", + "expo-linear-gradient": "^55.0.9", + "expo-linking": "^55.0.8", "expo-router": "~55.0.7", "expo-secure-store": "~55.0.9", "expo-status-bar": "~55.0.4", "nativewind": "^4.2.3", "react": "19.2.0", + "react-dom": "19.2.0", "react-native": "0.83.2", + "react-native-gesture-handler": "^2.30.0", + "react-native-reanimated": "^4.3.0", + "react-native-safe-area-context": "^5.7.0", + "react-native-screens": "^4.24.0", + "react-native-web": "^0.21.0", + "react-native-worklets": "^0.8.1", "zustand": "^5.0.12" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "~19.2.2", - "tailwindcss": "^4.2.2", - "typescript": "~5.9.2" + "babel-preset-expo": "^55.0.12", + "eslint": "^10.1.0", + "globals": "^17.4.0", + "jiti": "^2.6.1", + "tailwindcss": "^3.4.19", + "typescript": "~5.9.2", + "typescript-eslint": "^8.57.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@babel/code-frame": { @@ -78,15 +111,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", @@ -131,15 +155,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", @@ -161,15 +176,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", @@ -187,15 +193,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", @@ -1244,15 +1241,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", @@ -1299,6 +1287,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", @@ -1446,12 +1449,274 @@ "node": ">=6.9.0" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@expo-google-fonts/inter": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@expo-google-fonts/inter/-/inter-0.4.2.tgz", + "integrity": "sha512-syfiImMaDmq7cFi0of+waE2M4uSCyd16zgyWxdPOY7fN2VBmSLKEzkfbZgeOjJq61kSqPBNNtXjggiQiSD6gMQ==", + "license": "MIT AND OFL-1.1" + }, + "node_modules/@expo-google-fonts/manrope": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@expo-google-fonts/manrope/-/manrope-0.4.2.tgz", + "integrity": "sha512-BZsKe8d9BJrVnIQIZcTS7/Kac0TbXnqs/+8EBSiQxrmK6GoCO6eTkmr50D1weIk/EoF20pTmAkpWICnovATr/g==", + "license": "MIT AND OFL-1.1" + }, "node_modules/@expo-google-fonts/material-symbols": { "version": "0.4.27", "resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.27.tgz", "integrity": "sha512-cnb3DZnWUWpezGFkJ8y4MT5f/lw6FcgDzeJzic+T+vpQHLHG1cg3SC3i1w1i8Bk4xKR4HPY3t9iIRNvtr5ml8A==", "license": "MIT AND Apache-2.0" }, + "node_modules/@expo/cli": { + "version": "55.0.18", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.18.tgz", + "integrity": "sha512-3sJwu8KvCvQIXBnhUlHgLBZBe+ZK4Da9R5rgI4znaowJavYWMqzRClLzyE6Kri66WVoMX7Q4HUVIh8prRlO0XA==", + "license": "MIT", + "dependencies": { + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~55.0.10", + "@expo/config-plugins": "~55.0.7", + "@expo/devcert": "^1.2.1", + "@expo/env": "~2.1.1", + "@expo/image-utils": "^0.8.12", + "@expo/json-file": "^10.0.12", + "@expo/log-box": "55.0.7", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "~55.0.11", + "@expo/osascript": "^2.4.2", + "@expo/package-manager": "^1.10.3", + "@expo/plist": "^0.5.2", + "@expo/prebuild-config": "^55.0.10", + "@expo/require-utils": "^55.0.3", + "@expo/router-server": "^55.0.11", + "@expo/schema-utils": "^55.0.2", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.4.0", + "@react-native/dev-middleware": "0.83.2", + "accepts": "^1.3.8", + "arg": "^5.0.2", + "better-opn": "~3.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "dnssd-advertise": "^1.1.3", + "expo-server": "^55.0.6", + "fetch-nodeshim": "^0.4.6", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "lan-network": "^0.2.0", + "multitars": "^0.2.3", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^4.0.3", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "terminal-link": "^2.1.1", + "toqr": "^0.1.1", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1", + "zod": "^3.25.76" + }, + "bin": { + "expo-internal": "build/bin/cli" + }, + "peerDependencies": { + "expo": "*", + "expo-router": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo-router": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/cli/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/@expo/cli/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -1501,12 +1766,70 @@ "xml2js": "0.6.0" } }, - "node_modules/@expo/config-types": { - "version": "55.0.5", + "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/@expo/config-plugins/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/config-types": { + "version": "55.0.5", "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-55.0.5.tgz", "integrity": "sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==", "license": "MIT" }, + "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/@expo/config/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/devcert": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", @@ -1594,6 +1917,35 @@ "fingerprint": "bin/cli.js" } }, + "node_modules/@expo/fingerprint/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/@expo/fingerprint/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/image-utils": { "version": "0.8.12", "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", @@ -1609,6 +1961,18 @@ "semver": "^7.6.0" } }, + "node_modules/@expo/image-utils/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/json-file": { "version": "10.0.12", "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", @@ -1668,6 +2032,82 @@ "metro-transform-worker": "0.83.3" } }, + "node_modules/@expo/metro-config": { + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-55.0.11.tgz", + "integrity": "sha512-qGxq7RwWpj0zNvZO/e5aizKrOKYYBrVPShSbxPOVB1EXcexxTPTxnOe4pYFg/gKkLIJe0t3jSSF8IDWlGdaaOg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~55.0.10", + "@expo/env": "~2.1.1", + "@expo/json-file": "~10.0.12", + "@expo/metro": "~54.2.0", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.32.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-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/@expo/metro-runtime": { + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-55.0.6.tgz", + "integrity": "sha512-l8VvgKN9md+URjeQDB+DnHVmvpcWI6zFLH6yv7GTv4sfRDKyaZ5zDXYjTP1phYdgW6ea2NrRtCGNIxylWhsgtg==", + "license": "MIT", + "dependencies": { + "@expo/log-box": "55.0.7", + "anser": "^1.4.9", + "pretty-format": "^29.7.0", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/@expo/osascript": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.2.tgz", @@ -1705,6 +2145,39 @@ "xmlbuilder": "^15.1.1" } }, + "node_modules/@expo/prebuild-config": { + "version": "55.0.10", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.10.tgz", + "integrity": "sha512-AMylDld5G7YJGfEhEyXtgWRuBB83802QBoewF1vJ6NMDtufukuPhMJzOs9E4UXNsjLTaQcgT4yTWhsAWl7o1AQ==", + "license": "MIT", + "dependencies": { + "@expo/config": "~55.0.10", + "@expo/config-plugins": "~55.0.7", + "@expo/config-types": "^55.0.5", + "@expo/image-utils": "^0.8.12", + "@expo/json-file": "^10.0.12", + "@react-native/normalize-colors": "0.83.2", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/@expo/prebuild-config/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/require-utils": { "version": "55.0.3", "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.3.tgz", @@ -1724,6 +2197,40 @@ } } }, + "node_modules/@expo/router-server": { + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.11.tgz", + "integrity": "sha512-Kd8J1OOlFR00DZxn+1KfiQiXZtRut6cj8+ynqHJa7dtt/lTL4tGkYistqmVhpKJ6w886eRY5WivKy7o0ZBFkJA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "@expo/metro-runtime": "^55.0.6", + "expo": "*", + "expo-constants": "^55.0.9", + "expo-font": "^55.0.4", + "expo-router": "*", + "expo-server": "^55.0.6", + "react": "*", + "react-dom": "*", + "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" + }, + "peerDependenciesMeta": { + "@expo/metro-runtime": { + "optional": true + }, + "expo-router": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, "node_modules/@expo/schema-utils": { "version": "55.0.2", "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-55.0.2.tgz", @@ -1754,6 +2261,17 @@ "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", "license": "MIT" }, + "node_modules/@expo/vector-icons": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", + "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", + "license": "MIT", + "peerDependencies": { + "expo-font": ">=14.0.4", + "react": "*", + "react-native": "*" + } + }, "node_modules/@expo/ws-tunnel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", @@ -1774,22 +2292,56 @@ "excpretty": "build/cli.js" } }, - "node_modules/@expo/xcpretty/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } }, - "node_modules/@expo/xcpretty/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "argparse": "^2.0.1" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@isaacs/ttlcache": { @@ -1817,6 +2369,15 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1826,6 +2387,71 @@ "node": ">=6" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1989,6 +2615,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", @@ -2361,6 +3025,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2549,6 +3243,15 @@ "@babel/core": "*" } }, + "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", + "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.32.0" + } + }, "node_modules/@react-native/codegen": { "version": "0.83.2", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.2.tgz", @@ -2570,55 +3273,6 @@ "@babel/core": "*" } }, - "node_modules/@react-native/codegen/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/@react-native/codegen/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@react-native/codegen/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@react-native/codegen/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@react-native/community-cli-plugin": { "version": "0.83.2", "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.83.2.tgz", @@ -2649,6 +3303,18 @@ } } }, + "node_modules/@react-native/community-cli-plugin/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@react-native/debugger-frontend": { "version": "0.83.2", "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.2.tgz", @@ -2694,6 +3360,27 @@ "node": ">= 20.19.4" } }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@react-native/gradle-plugin": { "version": "0.83.2", "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.83.2.tgz", @@ -2742,17 +3429,17 @@ } }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.6.tgz", - "integrity": "sha512-olB+s0ApMzWN9t5Bk5Mj6ntSlVRz3B8v+1LtwGS/29lyC311G5es0kgxyzpGKE9gy6Ef8W526QH5cIka2jh0kQ==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.8.tgz", + "integrity": "sha512-Fz/AAPE6Be0CimOXvon75RNgpFCbZvzF2RPcNeZOdOxIYyHDGxDdtsfTxLHB0tOp9HHXkT0xXOX8Rk001jdpbg==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.11", + "@react-navigation/elements": "^2.9.13", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { - "@react-navigation/native": "^7.1.34", + "@react-navigation/native": "^7.2.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -2760,9 +3447,9 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.16.2.tgz", - "integrity": "sha512-0dbCC2aTjNW7MvG1fY7zeq6eYvmmaFCEnBDXPuMPJ8uKgfs9lFGXIQFIfBdmcBVX6vHhS+K213VCsuHSIv5jYw==", + "version": "7.17.1", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.1.tgz", + "integrity": "sha512-P1kL4FVTVYEf9Cvmb+WFxQ2UkbaXc9psj6OE0BsZ+hutPqZVbmiN6v/TI5QPf4qtg40a02yYo3vo+Mob9vJKtg==", "license": "MIT", "dependencies": { "@react-navigation/routers": "^7.5.3", @@ -2778,16 +3465,10 @@ "react": ">= 18.2.0" } }, - "node_modules/@react-navigation/core/node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", - "license": "MIT" - }, "node_modules/@react-navigation/elements": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.11.tgz", - "integrity": "sha512-O5KiwaVCcEVuqZgQ77xiBFSl1sha77rNMTFlLWYnom33ZHPDarV3bM9WNyVnMZxU8ZVTi02X3+ZhO0fSn5QYyg==", + "version": "2.9.13", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.13.tgz", + "integrity": "sha512-ZD8fPwhujgs3SwgaPRse+shLCFkeLhlfk9BHtQ604Qa7/p0/sRQV9HkTfREP8gtbt6nwk6WE+0vWfX2iqxOCKA==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -2796,7 +3477,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.1.34", + "@react-navigation/native": "^7.2.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -2808,12 +3489,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.34.tgz", - "integrity": "sha512-zzQ0mKAhLsjTIsaoLfILKZVMObJzE0F+bOi0hl2Glt+1Rd2GtaWJ1Z024c3yLmX+Oc79pqoCQLBXpyxtrZu9NQ==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.1.tgz", + "integrity": "sha512-ohiGfR5kX585aADiYt+nfwdqmJjj5W/1eXN9CQ/njhQNi/sMmjaxYppS+e0E0zW+z5b4gaLFBvqLrJcvOdtLUA==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.16.2", + "@react-navigation/core": "^7.17.1", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -2825,18 +3506,18 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.6.tgz", - "integrity": "sha512-VRlC5mLanRPHK0E15Cild6U01Z5TDPBlmt5YcXRBc+hQTAMbMT9XcSTobf3sJXNY0zzDD1IpSs3Ynex/GU225g==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.9.tgz", + "integrity": "sha512-s76NyRr/VSPRqXaLtaKUj9Q1qZ5ym0831QZFFXJcRyom6QYpo9eESB9/dfeN+tTEnH7kP77CwoCuR0THKMuk3w==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.11", + "@react-navigation/elements": "^2.9.13", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.1.34", + "@react-navigation/native": "^7.2.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -2917,6 +3598,20 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2926,6 +3621,12 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2950,6 +3651,13 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", @@ -2990,19 +3698,262 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, "engines": { - "node": ">=10.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" } }, "node_modules/abort-controller": { @@ -3042,6 +3993,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3051,6 +4012,23 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/anser": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", @@ -3108,6 +4086,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -3121,6 +4106,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -3128,13 +4125,10 @@ "license": "MIT" }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.6", @@ -3226,15 +4220,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", @@ -3276,12 +4261,27 @@ "license": "MIT" }, "node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", - "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.1.tgz", + "integrity": "sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ==", "license": "MIT", "dependencies": { - "hermes-parser": "0.32.0" + "hermes-parser": "0.32.1" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.1.tgz", + "integrity": "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.1.tgz", + "integrity": "sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.1" } }, "node_modules/babel-plugin-transform-flow-enums": { @@ -3319,6 +4319,54 @@ "@babel/core": "^7.0.0 || ^8.0.0-0" } }, + "node_modules/babel-preset-expo": { + "version": "55.0.12", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.12.tgz", + "integrity": "sha512-oR46ExGZpRijmPUsr0rFH5X4lR/mvwqJAFXJRLpynZcvyv2pHPTeGMNfd/p5oPMbdbaeMS6G+3k18p48u2Qjbw==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.20.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.83.2", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.32.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "expo-widgets": "^55.0.6", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + }, + "expo-widgets": { + "optional": true + } + } + }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", @@ -3365,9 +4413,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", - "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "version": "2.10.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", + "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3414,6 +4462,19 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -3424,9 +4485,9 @@ } }, "node_modules/bplist-parser": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", - "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", "license": "MIT", "dependencies": { "big-integer": "1.6.x" @@ -3436,9 +4497,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3528,10 +4589,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001780", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", - "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "funding": [ { "type": "opencollective", @@ -3564,7 +4635,45 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chrome-launcher": { + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-launcher": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", @@ -3597,10 +4706,19 @@ } }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "license": "MIT" + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/cli-cursor": { "version": "2.1.0", @@ -3646,6 +4764,18 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/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/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -3827,6 +4957,15 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3841,6 +4980,28 @@ "node": ">= 8" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3874,6 +5035,13 @@ "node": ">=0.10" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3938,10 +5106,24 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, - "node_modules/dnssd-advertise": { + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.3.tgz", - "integrity": "sha512-XENsHi3MBzWOCAXif3yZvU1Ah0l+nhJj1sjWL6TnOAYKvGiFhbTx32xHN7+wLMLUOCj7Nr0evADWG4R8JtqCDA==", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dnssd-advertise": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.4.tgz", + "integrity": "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==", "license": "MIT" }, "node_modules/ee-first": { @@ -3951,9 +5133,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.321", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", - "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "version": "1.5.328", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", + "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -4007,6 +5189,112 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4020,6 +5308,52 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4092,6 +5426,21 @@ } } }, + "node_modules/expo-asset": { + "version": "55.0.10", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-55.0.10.tgz", + "integrity": "sha512-wxjNBKIaDyachq7oJgVlWVFzZ6SnNpJFJhkkcymXoTPt5O3XmDM+a6fT91xQQawCXTyZuCc1sNxKMetEofeYkg==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.8.12", + "expo-constants": "~55.0.9" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-constants": { "version": "55.0.9", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.9.tgz", @@ -4106,6 +5455,16 @@ "react-native": "*" } }, + "node_modules/expo-file-system": { + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.11.tgz", + "integrity": "sha512-KMUd6OY375J9WD79ZvjvCDZMveT7YfgiGWdi58/gfuTBsr14TRuoPk8RRQHAtc4UquzWViKcHwna9aPY7/XPpw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-font": { "version": "55.0.4", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-55.0.4.tgz", @@ -4151,6 +5510,41 @@ } } }, + "node_modules/expo-keep-awake": { + "version": "55.0.4", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.4.tgz", + "integrity": "sha512-vwfdMtMS5Fxaon8gC0AiE70SpxTsHJ+rjeoVJl8kdfdbxczF7OIaVmfjFJ5Gfigd/WZiLqxhfZk34VAkXF4PNg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-linear-gradient": { + "version": "55.0.9", + "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-55.0.9.tgz", + "integrity": "sha512-S82iF+CVoSBVHdwusLQGh6Th/kcWLHU47jZhBPwyTrYWnsHZtb0oCqU96YvhDYvhbTdsuOaKEi+Xu+r/I2R8ow==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-linking": { + "version": "55.0.8", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-55.0.8.tgz", + "integrity": "sha512-O9QgKAfEqKfsjL6IKs5p7pFAjo/3/TQwjMzzNPl8BCndbxWMPQfMeViXPYYNS9bA2ujUqrtF1OYhO6woI7GNQQ==", + "license": "MIT", + "dependencies": { + "expo-constants": "~55.0.8", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "55.0.11", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-55.0.11.tgz", @@ -4254,97 +5648,43 @@ } } }, - "node_modules/expo-router/node_modules/@expo/metro-runtime": { + "node_modules/expo-router/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/expo-secure-store": { + "version": "55.0.9", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-55.0.9.tgz", + "integrity": "sha512-TIPGjM73LKlebpXwgAu/yL7lNWr6RQYmFw3vgYHOqLFYQMpsBqkQmopovbNX3c/0+RCE9KZlLAkcz8r6detILQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-server": { "version": "55.0.6", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-55.0.6.tgz", - "integrity": "sha512-l8VvgKN9md+URjeQDB+DnHVmvpcWI6zFLH6yv7GTv4sfRDKyaZ5zDXYjTP1phYdgW6ea2NrRtCGNIxylWhsgtg==", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.6.tgz", + "integrity": "sha512-xI72FTm469FfuuBL2R5aNtthgH+GR7ygOpsx/KcPS0K8AZaZd7VjtEExbzn9/qyyYkWW3T+3dAmCDKOMX8gdmQ==", + "license": "MIT", + "engines": { + "node": ">=20.16.0" + } + }, + "node_modules/expo-status-bar": { + "version": "55.0.4", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.4.tgz", + "integrity": "sha512-BPDjUXKqv1F9j2YNGLRZfkBEZXIEEpqj+t81y4c+4fdSN3Pos7goIHXgcl2ozbKQLgKRZQyNZQtbUgh5UjHYUQ==", "license": "MIT", "dependencies": { - "@expo/log-box": "55.0.7", - "anser": "^1.4.9", - "pretty-format": "^29.7.0", - "stacktrace-parser": "^0.1.10", - "whatwg-fetch": "^3.0.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-dom": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/expo-secure-store": { - "version": "55.0.9", - "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-55.0.9.tgz", - "integrity": "sha512-TIPGjM73LKlebpXwgAu/yL7lNWr6RQYmFw3vgYHOqLFYQMpsBqkQmopovbNX3c/0+RCE9KZlLAkcz8r6detILQ==", - "license": "MIT", - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-server": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.6.tgz", - "integrity": "sha512-xI72FTm469FfuuBL2R5aNtthgH+GR7ygOpsx/KcPS0K8AZaZd7VjtEExbzn9/qyyYkWW3T+3dAmCDKOMX8gdmQ==", - "license": "MIT", - "engines": { - "node": ">=20.16.0" - } - }, - "node_modules/expo-status-bar": { - "version": "55.0.4", - "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.4.tgz", - "integrity": "sha512-BPDjUXKqv1F9j2YNGLRZfkBEZXIEEpqj+t81y4c+4fdSN3Pos7goIHXgcl2ozbKQLgKRZQyNZQtbUgh5UjHYUQ==", - "license": "MIT", - "dependencies": { - "react-native-is-edge-to-edge": "^1.2.1" + "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", @@ -4367,319 +5707,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/@expo/cli": { - "version": "55.0.18", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.18.tgz", - "integrity": "sha512-3sJwu8KvCvQIXBnhUlHgLBZBe+ZK4Da9R5rgI4znaowJavYWMqzRClLzyE6Kri66WVoMX7Q4HUVIh8prRlO0XA==", - "license": "MIT", - "dependencies": { - "@expo/code-signing-certificates": "^0.0.6", - "@expo/config": "~55.0.10", - "@expo/config-plugins": "~55.0.7", - "@expo/devcert": "^1.2.1", - "@expo/env": "~2.1.1", - "@expo/image-utils": "^0.8.12", - "@expo/json-file": "^10.0.12", - "@expo/log-box": "55.0.7", - "@expo/metro": "~54.2.0", - "@expo/metro-config": "~55.0.11", - "@expo/osascript": "^2.4.2", - "@expo/package-manager": "^1.10.3", - "@expo/plist": "^0.5.2", - "@expo/prebuild-config": "^55.0.10", - "@expo/require-utils": "^55.0.3", - "@expo/router-server": "^55.0.11", - "@expo/schema-utils": "^55.0.2", - "@expo/spawn-async": "^1.7.2", - "@expo/ws-tunnel": "^1.0.1", - "@expo/xcpretty": "^4.4.0", - "@react-native/dev-middleware": "0.83.2", - "accepts": "^1.3.8", - "arg": "^5.0.2", - "better-opn": "~3.0.2", - "bplist-creator": "0.1.0", - "bplist-parser": "^0.3.1", - "chalk": "^4.0.0", - "ci-info": "^3.3.0", - "compression": "^1.7.4", - "connect": "^3.7.0", - "debug": "^4.3.4", - "dnssd-advertise": "^1.1.3", - "expo-server": "^55.0.6", - "fetch-nodeshim": "^0.4.6", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "lan-network": "^0.2.0", - "multitars": "^0.2.3", - "node-forge": "^1.3.3", - "npm-package-arg": "^11.0.0", - "ora": "^3.4.0", - "picomatch": "^4.0.3", - "pretty-format": "^29.7.0", - "progress": "^2.0.3", - "prompts": "^2.3.2", - "resolve-from": "^5.0.0", - "semver": "^7.6.0", - "send": "^0.19.0", - "slugify": "^1.3.4", - "source-map-support": "~0.5.21", - "stacktrace-parser": "^0.1.10", - "structured-headers": "^0.4.1", - "terminal-link": "^2.1.1", - "toqr": "^0.1.1", - "wrap-ansi": "^7.0.0", - "ws": "^8.12.1", - "zod": "^3.25.76" - }, - "bin": { - "expo-internal": "build/bin/cli" - }, - "peerDependencies": { - "expo": "*", - "expo-router": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "expo-router": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/prebuild-config": { - "version": "55.0.10", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.10.tgz", - "integrity": "sha512-AMylDld5G7YJGfEhEyXtgWRuBB83802QBoewF1vJ6NMDtufukuPhMJzOs9E4UXNsjLTaQcgT4yTWhsAWl7o1AQ==", - "license": "MIT", - "dependencies": { - "@expo/config": "~55.0.10", - "@expo/config-plugins": "~55.0.7", - "@expo/config-types": "^55.0.5", - "@expo/image-utils": "^0.8.12", - "@expo/json-file": "^10.0.12", - "@react-native/normalize-colors": "0.83.2", - "debug": "^4.3.1", - "resolve-from": "^5.0.0", - "semver": "^7.6.0", - "xml2js": "0.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "55.0.11", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.11.tgz", - "integrity": "sha512-Kd8J1OOlFR00DZxn+1KfiQiXZtRut6cj8+ynqHJa7dtt/lTL4tGkYistqmVhpKJ6w886eRY5WivKy7o0ZBFkJA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "@expo/metro-runtime": "^55.0.6", - "expo": "*", - "expo-constants": "^55.0.9", - "expo-font": "^55.0.4", - "expo-router": "*", - "expo-server": "^55.0.6", - "react": "*", - "react-dom": "*", - "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" - }, - "peerDependenciesMeta": { - "@expo/metro-runtime": { - "optional": true - }, - "expo-router": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-server-dom-webpack": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "55.0.11", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-55.0.11.tgz", - "integrity": "sha512-qGxq7RwWpj0zNvZO/e5aizKrOKYYBrVPShSbxPOVB1EXcexxTPTxnOe4pYFg/gKkLIJe0t3jSSF8IDWlGdaaOg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~55.0.10", - "@expo/env": "~2.1.1", - "@expo/json-file": "~10.0.12", - "@expo/metro": "~54.2.0", - "@expo/spawn-async": "^1.7.2", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.32.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.3", - "postcss": "~8.4.32", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/vector-icons": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", - "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", - "license": "MIT", - "peerDependencies": { - "expo-font": ">=14.0.4", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/babel-preset-expo": { - "version": "55.0.12", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.12.tgz", - "integrity": "sha512-oR46ExGZpRijmPUsr0rFH5X4lR/mvwqJAFXJRLpynZcvyv2pHPTeGMNfd/p5oPMbdbaeMS6G+3k18p48u2Qjbw==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.20.5", - "@babel/helper-module-imports": "^7.25.9", - "@babel/plugin-proposal-decorators": "^7.12.9", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-preset": "0.83.2", - "babel-plugin-react-compiler": "^1.0.0", - "babel-plugin-react-native-web": "~0.21.0", - "babel-plugin-syntax-hermes-parser": "^0.32.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "debug": "^4.3.4", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "@babel/runtime": "^7.20.0", - "expo": "*", - "expo-widgets": "^55.0.6", - "react-refresh": ">=0.14.0 <1.0.0" - }, - "peerDependenciesMeta": { - "@babel/runtime": { - "optional": true - }, - "expo": { - "optional": true - }, - "expo-widgets": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/expo/node_modules/expo-asset": { - "version": "55.0.10", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-55.0.10.tgz", - "integrity": "sha512-wxjNBKIaDyachq7oJgVlWVFzZ6SnNpJFJhkkcymXoTPt5O3XmDM+a6fT91xQQawCXTyZuCc1sNxKMetEofeYkg==", - "license": "MIT", - "dependencies": { - "@expo/image-utils": "^0.8.12", - "expo-constants": "~55.0.9" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-file-system": { - "version": "55.0.11", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.11.tgz", - "integrity": "sha512-KMUd6OY375J9WD79ZvjvCDZMveT7YfgiGWdi58/gfuTBsr14TRuoPk8RRQHAtc4UquzWViKcHwna9aPY7/XPpw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-keep-awake": { - "version": "55.0.4", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.4.tgz", - "integrity": "sha512-vwfdMtMS5Fxaon8gC0AiE70SpxTsHJ+rjeoVJl8kdfdbxczF7OIaVmfjFJ5Gfigd/WZiLqxhfZk34VAkXF4PNg==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, - "node_modules/expo/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/expo/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -4692,12 +5719,59 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-dotslash": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", @@ -4719,12 +5793,73 @@ "bser": "2.1.1" } }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", + "license": "MIT" + }, + "node_modules/fbjs/node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fetch-nodeshim": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.9.tgz", - "integrity": "sha512-XIQWlB2A4RZ7NebXWGxS0uDMdvRHkiUDTghBVJKFg9yEOd45w/PP8cZANuPf2H08W6Cor3+2n7Q6TTZgAS3Fkw==", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.10.tgz", + "integrity": "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==", "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4780,18 +5915,43 @@ "license": "MIT" }, "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" } }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/flow-enums-runtime": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", @@ -4888,22 +6048,80 @@ } }, "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", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4952,6 +6170,21 @@ "hermes-estree": "0.32.0" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -5012,6 +6245,12 @@ "node": ">= 14" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5062,6 +6301,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -5077,6 +6325,19 @@ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5107,6 +6368,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5116,6 +6387,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5177,15 +6461,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -5297,19 +6572,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-validate": { @@ -5365,6 +6637,16 @@ "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", "license": "MIT" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5372,13 +6654,12 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -5402,6 +6683,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5414,6 +6716,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -5441,6 +6753,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -5715,16 +7041,40 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash.debounce": { @@ -5882,6 +7232,16 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/metro": { "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", @@ -6151,6 +7511,33 @@ "node": ">=20.19.4" } }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6164,6 +7551,18 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -6177,9 +7576,9 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6197,6 +7596,15 @@ "node": ">= 0.6" } }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -6254,6 +7662,18 @@ "integrity": "sha512-XgLbg1HHchFauMCQPRwMj6MSyDd5koPlTA1hM3rUFkeXzGpjU/I9fP3to7yrObE9jcN8ChIOQGrM0tV0kUZaKg==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6289,6 +7709,13 @@ "tailwindcss": ">3.3.0" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -6298,10 +7725,30 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -6343,6 +7790,18 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -6361,6 +7820,25 @@ "node": ">=20.19.4" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -6419,6 +7897,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", @@ -6436,15 +7932,6 @@ "node": ">=6" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ora/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -6504,18 +7991,6 @@ "node": ">=4" } }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/ora/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6529,30 +8004,35 @@ } }, "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -6650,17 +8130,27 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -6721,6 +8211,149 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -6747,6 +8380,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", @@ -6787,6 +8426,16 @@ "node": ">= 6" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -6814,6 +8463,27 @@ "inherits": "~2.0.3" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -6842,16 +8512,61 @@ "ws": "^7" } }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, + "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", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "license": "MIT" }, "node_modules/react-native": { @@ -7183,6 +8898,33 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/react-native-css-interop/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-gesture-handler": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.1.tgz", + "integrity": "sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-is-edge-to-edge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", @@ -7193,20 +8935,133 @@ "react-native": "*" } }, - "node_modules/react-native/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==", + "node_modules/react-native-reanimated": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.3.0.tgz", + "integrity": "sha512-HOTTPdKtddXTOsmQxDASXEwLS3lqEHrKERD3XOgzSqWJ7L3x81Pnx7mTcKx1FKdkgomMug/XSmm1C6Z7GIowxA==", + "license": "MIT", + "dependencies": { + "react-native-is-edge-to-edge": "^1.3.1", + "semver": "^7.7.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "0.81 - 0.85", + "react-native-worklets": "0.8.x" + } + }, + "node_modules/react-native-reanimated/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-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.24.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.24.0.tgz", + "integrity": "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-web": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", + "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@react-native/normalize-colors": "^0.74.1", + "fbjs": "^3.0.4", + "inline-style-prefixer": "^7.0.1", + "memoize-one": "^6.0.0", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "styleq": "^0.1.3" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { + "version": "0.74.89", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz", + "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==", "license": "MIT" }, - "node_modules/react-native/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/react-native-web/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/react-native-worklets": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.1.tgz", + "integrity": "sha512-oWP/lStsAHU6oYCaWDXrda/wOHVdhusQJz1e6x9gPnXdFf4ndNDAOtWCmk2zGrAnlapfyA3rM6PCQq94mPg9cw==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-classes": "^7.28.4", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "convert-source-map": "^2.0.0", + "semver": "^7.7.3" + }, + "peerDependencies": { + "@babel/core": "*", + "@react-native/metro-config": "*", + "react": "*", + "react-native": "0.81 - 0.85" + } + }, + "node_modules/react-native-worklets/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", + "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "hermes-parser": "0.32.0" } }, "node_modules/react-native/node_modules/commander": { @@ -7218,37 +9073,37 @@ "node": ">=18" } }, - "node_modules/react-native/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/react-native/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, - "node_modules/react-native/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "node_modules/react-native/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", "engines": { - "node": "*" + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/react-refresh": { @@ -7329,6 +9184,42 @@ } } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -7445,6 +9336,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -7454,60 +9356,35 @@ "dependencies": { "glob": "^7.1.3" }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/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/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" + "bin": { + "rimraf": "bin.js" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "queue-microtask": "^1.2.2" } }, "node_modules/safe-buffer": { @@ -7546,15 +9423,12 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/send": { @@ -7665,6 +9539,12 @@ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7736,6 +9616,18 @@ "plist": "^3.0.5" } }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -7901,7 +9793,7 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -7913,12 +9805,72 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/structured-headers": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", "license": "MIT" }, + "node_modules/styleq": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", + "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7957,11 +9909,52 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } }, "node_modules/terminal-link": { "version": "2.1.1", @@ -8024,36 +10017,15 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -8066,12 +10038,52 @@ "node": "*" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8105,12 +10117,51 @@ "integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==", "license": "MIT" }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8143,6 +10194,56 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -8228,6 +10329,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -8289,6 +10400,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8368,12 +10486,28 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/whatwg-url-minimum": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.1.tgz", @@ -8395,6 +10529,16 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -8412,6 +10556,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8432,16 +10588,16 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -8512,9 +10668,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -8553,6 +10709,19 @@ "node": ">=12" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/mobile/package.json b/mobile/package.json index fbe18ca..fd96db6 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -9,21 +9,41 @@ "web": "expo start --web" }, "dependencies": { + "@expo-google-fonts/inter": "^0.4.2", + "@expo-google-fonts/manrope": "^0.4.2", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/netinfo": "11.5.2", + "@react-navigation/native": "^7.2.1", "expo": "~55.0.8", + "expo-constants": "^55.0.9", + "expo-font": "^55.0.4", + "expo-linear-gradient": "^55.0.9", + "expo-linking": "^55.0.8", "expo-router": "~55.0.7", "expo-secure-store": "~55.0.9", "expo-status-bar": "~55.0.4", "nativewind": "^4.2.3", "react": "19.2.0", + "react-dom": "19.2.0", "react-native": "0.83.2", + "react-native-gesture-handler": "^2.30.0", + "react-native-reanimated": "^4.3.0", + "react-native-safe-area-context": "^5.7.0", + "react-native-screens": "^4.24.0", + "react-native-web": "^0.21.0", + "react-native-worklets": "^0.8.1", "zustand": "^5.0.12" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "~19.2.2", - "tailwindcss": "^4.2.2", - "typescript": "~5.9.2" + "babel-preset-expo": "^55.0.12", + "eslint": "^10.1.0", + "globals": "^17.4.0", + "jiti": "^2.6.1", + "tailwindcss": "^3.4.19", + "typescript": "~5.9.2", + "typescript-eslint": "^8.57.2" }, "private": true } diff --git a/mobile/store/authStore.ts b/mobile/store/authStore.ts index 39f54de..2be625d 100644 --- a/mobile/store/authStore.ts +++ b/mobile/store/authStore.ts @@ -1,16 +1,44 @@ import { create } from "zustand"; import * as SecureStore from "expo-secure-store"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { api } from "../lib/api"; +import { Platform } from "react-native"; + +// Use AsyncStorage for web, SecureStore for native +const storage = { + async getItem(key: string): Promise { + if (Platform.OS === "web") { + return AsyncStorage.getItem(key); + } + return SecureStore.getItemAsync(key); + }, + async setItem(key: string, value: string): Promise { + if (Platform.OS === "web") { + return AsyncStorage.setItem(key, value); + } + return SecureStore.setItemAsync(key, value); + }, + async removeItem(key: string): Promise { + if (Platform.OS === "web") { + return AsyncStorage.removeItem(key); + } + return SecureStore.deleteItemAsync(key); + }, +}; interface AuthState { accessToken: string | null; refreshToken: string | null; username: string | null; deviceId: string | null; + accessExpiry?: number | null; + login: ( tokens: { accessToken: string; refreshToken: string }, user: { username: string; deviceId: string }, ) => Promise; logout: () => Promise; + setAccessToken: (token: string) => void; loadFromStorage: () => Promise; } @@ -21,25 +49,73 @@ export const useAuthStore = create((set) => ({ deviceId: null, login: async ({ accessToken, refreshToken }, { username, deviceId }) => { - await SecureStore.setItemAsync("access_token", accessToken); - await SecureStore.setItemAsync("refresh_token", refreshToken); - set({ accessToken, refreshToken, username, deviceId }); + await storage.setItem("access_token", accessToken); + await storage.setItem("refresh_token", refreshToken); + await storage.setItem("username", username); + await storage.setItem("device_id", deviceId); + // Try to parse expiry from JWT (exp claim) + let accessExpiry: number | null = null; + try { + const parts = accessToken.split("."); + if (parts.length === 3) { + const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + const json = decodeURIComponent( + Array.prototype.map + .call( + atob(b64), + (c: string) => + "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2), + ) + .join(""), + ); + const obj = JSON.parse(json); + if (typeof obj.exp === "number") accessExpiry = obj.exp * 1000; + } + } catch (e) { + console.debug("Failed to parse access token expiry", e); + } + + if (accessExpiry) + await storage.setItem("access_expiry", String(accessExpiry)); + await storage.setItem("access_token", accessToken); + set({ accessToken, refreshToken, username, deviceId, accessExpiry }); }, logout: async () => { - await SecureStore.deleteItemAsync("access_token"); - await SecureStore.deleteItemAsync("refresh_token"); + // Attempt to revoke refresh token on the server + try { + const refreshToken = await storage.getItem("refresh_token"); + if (refreshToken) await api.logout(refreshToken); + } catch (e) { + // best-effort revoke; ignore errors + console.warn("Failed to revoke refresh token", e); + } + + await storage.removeItem("access_token"); + await storage.removeItem("refresh_token"); + await storage.removeItem("username"); + await storage.removeItem("device_id"); + await storage.removeItem("access_expiry"); set({ accessToken: null, refreshToken: null, username: null, deviceId: null, + accessExpiry: null, }); }, + setAccessToken: (token) => set({ accessToken: token }), + loadFromStorage: async () => { - const accessToken = await SecureStore.getItemAsync("access_token"); - const refreshToken = await SecureStore.getItemAsync("refresh_token"); - if (accessToken) set({ accessToken, refreshToken }); + const accessToken = await storage.getItem("access_token"); + const refreshToken = await storage.getItem("refresh_token"); + const username = await storage.getItem("username"); + const deviceId = await storage.getItem("device_id"); + const accessExpiryRaw = await storage.getItem("access_expiry"); + const accessExpiry = accessExpiryRaw ? Number(accessExpiryRaw) : null; + if (accessToken) { + set({ accessToken, refreshToken, username, deviceId, accessExpiry }); + } }, -})); \ No newline at end of file +})); diff --git a/mobile/store/sensorStore.ts b/mobile/store/sensorStore.ts index e69de29..4527689 100644 --- a/mobile/store/sensorStore.ts +++ b/mobile/store/sensorStore.ts @@ -0,0 +1,44 @@ +import { create } from "zustand"; + +export interface SensorData { + temperature: number; + ph: number; + humidity: number; + ec: number; + N: number; + P: number; + K: number; + moisture: number; +} + +export interface SensorReading extends SensorData { + timestamp: string; +} + +interface SensorState { + latest: SensorData | null; + lastUpdated: string | null; + isLoading: boolean; + error: string | null; + + setLatest: (data: SensorData, timestamp: string) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + clear: () => void; +} + +export const useSensorStore = create((set) => ({ + latest: null, + lastUpdated: null, + isLoading: false, + error: null, + + setLatest: (data, timestamp) => + set({ latest: data, lastUpdated: timestamp, error: null }), + + setLoading: (loading) => set({ isLoading: loading }), + + setError: (error) => set({ error, isLoading: false }), + + clear: () => set({ latest: null, lastUpdated: null, error: null }), +})); diff --git a/mobile/store/themeStore.ts b/mobile/store/themeStore.ts new file mode 100644 index 0000000..b506f5b --- /dev/null +++ b/mobile/store/themeStore.ts @@ -0,0 +1,76 @@ +import { create } from "zustand"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import * as SecureStore from "expo-secure-store"; +import { Platform, Appearance } from "react-native"; + +// Use AsyncStorage for web, SecureStore for native (following authStore pattern) +const storage = { + async getItem(key: string): Promise { + if (Platform.OS === "web") { + return AsyncStorage.getItem(key); + } + return SecureStore.getItemAsync(key); + }, + async setItem(key: string, value: string): Promise { + if (Platform.OS === "web") { + return AsyncStorage.setItem(key, value); + } + return SecureStore.setItemAsync(key, value); + }, + async removeItem(key: string): Promise { + if (Platform.OS === "web") { + return AsyncStorage.removeItem(key); + } + return SecureStore.deleteItemAsync(key); + }, +}; + +type ThemeMode = "light" | "dark"; + +interface ThemeState { + mode: ThemeMode; + + setMode: (mode: ThemeMode) => Promise; + toggleMode: () => void; + initializeTheme: () => Promise; +} + +const THEME_STORAGE_KEY = "@terradetect:theme"; + +export const useThemeStore = create((set, get) => ({ + mode: "light", + + setMode: async (mode: ThemeMode) => { + try { + await storage.setItem(THEME_STORAGE_KEY, mode); + set({ mode }); + } catch (error) { + console.error("Failed to save theme preference:", error); + // Still update in-memory state even if persistence fails + set({ mode }); + } + }, + + toggleMode: () => { + const { mode } = get(); + const newMode: ThemeMode = mode === "light" ? "dark" : "light"; + get().setMode(newMode); + }, + + initializeTheme: async () => { + try { + const storedMode = await storage.getItem(THEME_STORAGE_KEY); + if (storedMode === "light" || storedMode === "dark") { + set({ mode: storedMode }); + } else { + // No stored preference - respect system theme on first launch + const systemTheme = Appearance.getColorScheme(); + const initialMode: ThemeMode = systemTheme === "dark" ? "dark" : "light"; + set({ mode: initialMode }); + } + } catch (error) { + console.error("Failed to load theme preference:", error); + // Keep default "light" theme on error + } + }, +})); diff --git a/mobile/tailwind.config.js b/mobile/tailwind.config.js index 6288219..b7ca107 100644 --- a/mobile/tailwind.config.js +++ b/mobile/tailwind.config.js @@ -1,9 +1,298 @@ +/* eslint-disable */ /** @type {import('tailwindcss').Config} */ module.exports = { + darkMode: "class", content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], presets: [require("nativewind/preset")], theme: { - extend: {}, + extend: { + colors: { + // Verdant Core (Light Theme) - Complete token set + primary: { + DEFAULT: "#006b2c", + dark: "#62df7d", + }, + "primary-container": { + DEFAULT: "#00873a", + dark: "#1ca64d", + }, + "on-primary": { + DEFAULT: "#ffffff", + dark: "#003916", + }, + "on-primary-container": { + DEFAULT: "#f7fff2", + dark: "#a8f5b8", + }, + "primary-fixed": { + DEFAULT: "#a8f5b8", + dark: "#a8f5b8", + }, + "primary-fixed-dim": { + DEFAULT: "#62df7d", + dark: "#62df7d", + }, + + secondary: { + DEFAULT: "#006398", + dark: "#7bd0ff", + }, + "secondary-container": { + DEFAULT: "#c8e6ff", + dark: "#004c6e", + }, + "on-secondary": { + DEFAULT: "#ffffff", + dark: "#003549", + }, + "on-secondary-container": { + DEFAULT: "#001e30", + dark: "#c8e6ff", + }, + "secondary-fixed": { + DEFAULT: "#c8e6ff", + dark: "#c8e6ff", + }, + "secondary-fixed-dim": { + DEFAULT: "#7bd0ff", + dark: "#7bd0ff", + }, + + tertiary: { + DEFAULT: "#825100", + dark: "#ffb95f", + }, + "tertiary-container": { + DEFAULT: "#ffddb8", + dark: "#5f3d00", + }, + "on-tertiary": { + DEFAULT: "#ffffff", + dark: "#452b00", + }, + "on-tertiary-container": { + DEFAULT: "#2a1700", + dark: "#ffddb8", + }, + "tertiary-fixed": { + DEFAULT: "#ffddb8", + dark: "#ffddb8", + }, + "tertiary-fixed-dim": { + DEFAULT: "#ffb95f", + dark: "#ffb95f", + }, + + error: { + DEFAULT: "#ba1a1a", + dark: "#ffb4ab", + }, + "error-container": { + DEFAULT: "#ffdad6", + dark: "#93000a", + }, + "on-error": { + DEFAULT: "#ffffff", + dark: "#690005", + }, + "on-error-container": { + DEFAULT: "#410002", + dark: "#ffdad6", + }, + + success: { + DEFAULT: "#006b2c", + dark: "#62df7d", + }, + "success-container": { + DEFAULT: "#a8f5b8", + dark: "#1ca64d", + }, + "on-success": { + DEFAULT: "#ffffff", + dark: "#003916", + }, + "on-success-container": { + DEFAULT: "#003916", + dark: "#a8f5b8", + }, + + warning: { + DEFAULT: "#825100", + dark: "#ffb95f", + }, + "warning-container": { + DEFAULT: "#ffddb8", + dark: "#5f3d00", + }, + "on-warning": { + DEFAULT: "#ffffff", + dark: "#452b00", + }, + "on-warning-container": { + DEFAULT: "#2a1700", + dark: "#ffddb8", + }, + + info: { + DEFAULT: "#006398", + dark: "#7bd0ff", + }, + "info-container": { + DEFAULT: "#c8e6ff", + dark: "#004c6e", + }, + "on-info": { + DEFAULT: "#ffffff", + dark: "#003549", + }, + "on-info-container": { + DEFAULT: "#001e30", + dark: "#c8e6ff", + }, + + surface: { + DEFAULT: "#f8f9fa", + dark: "#0c1324", + }, + "surface-dim": { + DEFAULT: "#d9dadc", + dark: "#0c1324", + }, + "surface-bright": { + DEFAULT: "#f8f9fa", + dark: "#33394c", + }, + "surface-container-lowest": { + DEFAULT: "#ffffff", + dark: "#070d1f", + }, + "surface-container-low": { + DEFAULT: "#f2f3f5", + dark: "#191f31", + }, + "surface-container": { + DEFAULT: "#ecedee", + dark: "#1d2333", + }, + "surface-container-high": { + DEFAULT: "#e6e7e9", + dark: "#272d3e", + }, + "surface-container-highest": { + DEFAULT: "#e1e2e3", + dark: "#323849", + }, + "surface-variant": { + DEFAULT: "#dde4dd", + dark: "#414946", + }, + + "on-surface": { + DEFAULT: "#191c1d", + dark: "#e1e2e3", + }, + "on-surface-variant": { + DEFAULT: "#414946", + dark: "#bdcaba", + }, + + outline: { + DEFAULT: "#717970", + dark: "#8b938a", + }, + "outline-variant": { + DEFAULT: "#c1c9c0", + dark: "#414946", + }, + + "inverse-surface": { + DEFAULT: "#2e3132", + dark: "#e1e2e3", + }, + "inverse-on-surface": { + DEFAULT: "#f0f1f2", + dark: "#2e3132", + }, + "inverse-primary": { + DEFAULT: "#62df7d", + dark: "#006b2c", + }, + + scrim: { + DEFAULT: "#000000", + dark: "#000000", + }, + shadow: { + DEFAULT: "#000000", + dark: "#000000", + }, + }, + fontFamily: { + // Manrope for headlines and display text (weights: 400, 600, 700, 800) + headline: [ + "Manrope_400Regular", + "Manrope_600SemiBold", + "Manrope_700Bold", + "Manrope_800ExtraBold", + "SF Pro Display", + "Roboto", + "sans-serif", + ], + display: [ + "Manrope_400Regular", + "Manrope_600SemiBold", + "Manrope_700Bold", + "Manrope_800ExtraBold", + "SF Pro Display", + "Roboto", + "sans-serif", + ], + // Inter for body text and labels (weights: 400, 500, 600, 700) + body: [ + "Inter_400Regular", + "Inter_500Medium", + "Inter_600SemiBold", + "Inter_700Bold", + "SF Pro Text", + "Roboto", + "sans-serif", + ], + label: [ + "Inter_400Regular", + "Inter_500Medium", + "Inter_600SemiBold", + "Inter_700Bold", + "SF Pro Text", + "Roboto", + "sans-serif", + ], + }, + spacing: { + 2: "0.5rem", // 8px - spacing-2 + 4: "1rem", // 16px - spacing-4 + 6: "1.5rem", // 24px - spacing-6 + 8: "2rem", // 32px - spacing-8 + 10: "2.5rem", // 40px - spacing-10 + 12: "3rem", // 48px - spacing-12 + }, + borderRadius: { + sm: "0.5rem", // 8px - for sensor chips + md: "1.5rem", // 24px - standard rounded + lg: "2rem", // 32px - cards + xl: "3rem", // 48px - large elements + full: "9999px", // pill shape + }, + backdropBlur: { + xs: "4px", + sm: "8px", + DEFAULT: "12px", + md: "12px", + lg: "16px", + xl: "20px", + "2xl": "32px", + }, + }, }, plugins: [], }; diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json index b9567f6..0c175fc 100644 --- a/mobile/tsconfig.json +++ b/mobile/tsconfig.json @@ -3,4 +3,10 @@ "compilerOptions": { "strict": true } -} + , + "exclude": [ + "**/__tests__/**", + "**/*.test.tsx", + "**/*.spec.tsx" + ] +} \ No newline at end of file diff --git a/mobile/types/api.d.ts b/mobile/types/api.d.ts new file mode 100644 index 0000000..923e48d --- /dev/null +++ b/mobile/types/api.d.ts @@ -0,0 +1,24 @@ +// mobile/types/api.d.ts + +export interface User { + username: string; + email: string; + device_id: string; +} + +export interface AuthResponse { + access_token: string; + refresh_token: string; + user: User; +} + +export interface SensorData { + temperature: number; + humidity: number; + ph: number; + N: number; + P: number; + K: number; + rainfall: number; + timestamp: string; +} diff --git a/stitch/dashboard/code.html b/stitch/dashboard/code.html new file mode 100644 index 0000000..449e7e7 --- /dev/null +++ b/stitch/dashboard/code.html @@ -0,0 +1,270 @@ + + + + + +TerraDetect Dashboard + + + + + + + + + + + + +
+
+
+eco +

TerraDetect

+
+
+ +
+ +
+
+
+
+
+ +
+

Good morning,
Farmer John!

+

Your crops are thriving today. Here's your real-time soil health overview.

+
+ +
+ +
+
+ +
+
+
+
+Active Monitoring +
+

North Sector Beta

+

Last scan: 2 minutes ago

+
+
+ +
+
+
+
+science +
+Optimal +
+

Soil pH Level

+

6.5

+
+
+
+
+
+
+
+
+
+Acidic +Neutral +Alkaline +
+
+
+
+ +

+ + Real-time Telemetry +

+
+ +
+
+device_thermostat ++2.1% +
+
+

Temperature

+

24°C

+
+
+ +
+
+water_drop +-0.5% +
+
+

Humidity

+

65%

+
+
+ +
+
+eco +Stable +
+
+

Nitrogen (N)

+

45mg/kg

+
+
+ +
+
+science ++12% +
+
+

Phosphorus (P)

+

30mg/kg

+
+
+ +
+
+grass +-2.4% +
+
+

Potassium (K)

+

120mg/kg

+
+
+
+ +
+
+
+
+

Hydration Forecast

+
+
+
+
+
+
+
+
+
+MonTueWedThuFriSat +
+
+
+
+recommend +
+

Predictive Action

+

Nitrogen levels are peaking. Delay fertilization for 48 hours to prevent runoff.

+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/dashboard/screen.png b/stitch/dashboard/screen.png new file mode 100644 index 0000000..8b2f8b9 Binary files /dev/null and b/stitch/dashboard/screen.png differ diff --git a/stitch/dashboard_dark/code.html b/stitch/dashboard_dark/code.html new file mode 100644 index 0000000..a1b8155 --- /dev/null +++ b/stitch/dashboard_dark/code.html @@ -0,0 +1,296 @@ + + + + + +TerraDetect Dashboard + + + + + + + + + + +
+
+sensors +TerraDetect +
+
+ +
+ +
+
+
+
+ +
+
+
+

Systems Online.

+

Welcome back, Commander. All terrestrial sensors are broadcasting within optimal parameters across Zone A-14.

+
+
+
+Live Telemetry +
+
+
+ +
+ +
+
+ +
+
+location_on +Section Alpha - West Basin +
+

Last update: 2 minutes ago

+
+
+ +
+
+ +
+
+
+thermostat +
++0.4° Trending +
+
+

Temperature

+
+

24.8

+°C +
+
+
+
+
+
+
+
+
+
+ +
+
+
+humidity_percentage +
+Optimal +
+
+

Humidity

+
+

62

+% +
+
+
+
+
+
+ +
+
+
+science +
+Neutral +
+
+

Soil pH

+
+

6.8

+ph +
+
+
+Acidic +Neutral +Alkaline +
+
+ +
+
+

Nutrient Levels (NPK)

+info +
+
+
+N +
+
+
+85% +
+
+P +
+
+
+42% +
+
+K +
+
+
+67% +
+
+
+ +
+
+
+
+auto_awesome +

Predictive Insight

+

Based on current NPK and humidity trends, irrigation is recommended for Section A-14 in the next 12 hours to maintain yield potential.

+
+ +
+
+ +
+

System Events

+
+
+
+
+

Sensor Node 08 Calibrated

+

08:42 AM • Maintenance

+
+
+
+
+
+

Soil Moisture Threshold Met

+

06:15 AM • Automated

+
+
+
+
+
+

Low Power Alert: Node 12

+

04:30 AM • System

+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/dashboard_dark/screen.png b/stitch/dashboard_dark/screen.png new file mode 100644 index 0000000..7c1220a Binary files /dev/null and b/stitch/dashboard_dark/screen.png differ diff --git a/stitch/history_screen/code.html b/stitch/history_screen/code.html new file mode 100644 index 0000000..50c0638 --- /dev/null +++ b/stitch/history_screen/code.html @@ -0,0 +1,357 @@ + + + + + +TerraDetect - History + + + + + + + + + + + +
+ +
+

Environmental Log

+

Field History

+
+
+ +
+
+
+

+show_chart + 7-Day Atmosphere Trends +

+ +
+ +
+
+
+MON +
+ +
+
+
+TUE +
+ +
+
+
+WED +
+ +
+
+
+THU +
+ +
+
+
+FRI +
+ +
+
+
+SAT +
+ +
+
+
+SUN +
+
+
+
+
+
+Average Temp (24°C) +
+
+
+Average Humidity (68%) +
+
+

+ Optimal conditions sustained for 84% of the week. Soil hydration levels are increasing. +

+
+
+
+
+ +
+

Recent Records

+ +
+
+
+event_note +
+
+

Oct 24, 2023

+

08:45 AM — Section A

+
+
+
+
+Temp +22.4°C +
+
+Humid +62% +
+
+pH +6.8 +
+
+NPK (mg/kg) +
+N:42 +P:18 +K:35 +
+
+
+
+ +
+
+
+event_note +
+
+

Oct 23, 2023

+

04:20 PM — Section B

+
+
+
+
+Temp +25.1°C +
+
+Humid +58% +
+
+pH +6.5 +
+
+NPK (mg/kg) +
+N:40 +P:15 +K:32 +
+
+
+
+ +
+
+
+event_note +
+
+

Oct 22, 2023

+

09:10 AM — Section A

+
+
+
+
+Temp +21.8°C +
+
+Humid +65% +
+
+pH +7.0 +
+
+NPK (mg/kg) +
+N:45 +P:20 +K:38 +
+
+
+
+ +
+
+
+event_note +
+
+

Oct 21, 2023

+

11:30 AM — Section C

+
+
+
+
+Temp +24.5°C +
+
+Humid +60% +
+
+pH +6.9 +
+
+NPK (mg/kg) +
+N:43 +P:19 +K:36 +
+
+
+
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/stitch/history_screen/screen.png b/stitch/history_screen/screen.png new file mode 100644 index 0000000..751d0de Binary files /dev/null and b/stitch/history_screen/screen.png differ diff --git a/stitch/history_screen_dark/code.html b/stitch/history_screen_dark/code.html new file mode 100644 index 0000000..b7777db --- /dev/null +++ b/stitch/history_screen_dark/code.html @@ -0,0 +1,226 @@ + + + + + +TerraDetect - History + + + + + + + + + + +
+
+sensors +

TerraDetect

+
+
+
+User Profile +
+
+
+
+ +
+

Environmental Timeline

+

A comprehensive historical record of ecological sensor data across the terrestrial network.

+
+ +
+ + + + +
+ +
+ +
+Today — Oct 24 +
+
+ +
+
+
+water_drop +
+
+

Soil Hydration Shift

+

Sensor Hub Alpha-09 • Forest Bed

+
+
+
+42.8% +14:22:10 GMT +
+
+ +
+
+
+light_mode +
+
+

Peak Photometry

+

Canopy Array 04 • Upper Tier

+
+
+
+850 LUX +12:05:44 GMT +
+
+ +
+Yesterday — Oct 23 +
+
+ +
+
+
+warning +
+
+

Thermal Threshold Exceeded

+

Perimeter Node B • Dry Zone

+
+
+
+38.4°C +16:45:12 GMT +
+
+ +
+
+Anomaly Detected +

Micro-Seismic Vibration

+

Station Sigma recorded a sequence of low-frequency oscillations consistent with tectonic settling.

+
+
+
+
+
+
+
+
+
+2.1 ML +
+
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/stitch/history_screen_dark/screen.png b/stitch/history_screen_dark/screen.png new file mode 100644 index 0000000..1993ae2 Binary files /dev/null and b/stitch/history_screen_dark/screen.png differ diff --git a/stitch/landing_page/code.html b/stitch/landing_page/code.html new file mode 100644 index 0000000..b425564 --- /dev/null +++ b/stitch/landing_page/code.html @@ -0,0 +1,306 @@ + + + + + +TerraDetect - Editorial Agronomy + + + + + + + + + +
+
+
+eco +TerraDetect +
+ +
+ + +
+
+
+
+ +
+
+
+The Future of Agronomy +

+ Cultivate with Precision Data. +

+

+ Real-time soil monitoring and AI-powered crop recommendations. We bridge the gap between biological intuition and digital intelligence. +

+
+ + +
+
+
+
+ +
+
+
+
+opacity +
+
+

Soil Moisture

+

64.2%

+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+

Ecosystem Intelligence

+

Beyond the surface. We provide the granular insights needed for sustainable, high-yield agriculture.

+
+
+ +
+
+
+analytics +
+

Soil Health Monitoring

+

+ Deploy our proprietary sensor network to track nitrogen, phosphorus, potassium, and pH levels in real-time across your entire acreage. +

+
+
+ +
+
+ +
+
+
+query_stats +
+

Predictive Analytics

+

+ Our AI models process multi-decade weather patterns and local sensor data to forecast yield outcomes with 94% accuracy. +

+
+
+
+
+

Forecast Confidence

+

High

+
+trending_up +
+
+
+ +
+psychology +

Crop Suitability

+

Adaptive logic that suggests the optimal crop rotation based on current soil depletion and market demand.

+
    +
  • +check_circle + Market Volatility Index +
  • +
  • +check_circle + Regeneration Cycles +
  • +
+
+ +
+ +
+

Built for Resilience.

+

Helping farmers navigate climate variability with science-backed decisions.

+
+
+
+
+
+ +
+format_quote +
+ "TerraDetect has fundamentally changed how we view our land. It's no longer just dirt—it's a living data engine that we can finally understand." +
+
+
+ +
+
+

Dr. Elena Thorne

+

Lead Research Scientist, BioField

+
+
+
+ +
+
+
+ +
+
+

Ready to grow smarter?

+

Join 2,500+ agricultural innovators using TerraDetect to optimize soil health and maximize harvest yields.

+
+ + +
+
+
+
+
+ + \ No newline at end of file diff --git a/stitch/landing_page/screen.png b/stitch/landing_page/screen.png new file mode 100644 index 0000000..7f7f36d Binary files /dev/null and b/stitch/landing_page/screen.png differ diff --git a/stitch/landing_page_dark/code.html b/stitch/landing_page_dark/code.html new file mode 100644 index 0000000..1d2fad9 --- /dev/null +++ b/stitch/landing_page_dark/code.html @@ -0,0 +1,294 @@ + + + + + +TerraDetect | Precision Agriculture + + + + + + + + + + + +
+ +
+
+
+ +
+
+

+ The Future of Earth
Measured in Real-Time. +

+

+ Real-time soil monitoring and AI-powered crop recommendations. Precision data for the modern observatory of the land. +

+
+ + +
+
+
+ +
+
+ +
+
+
+
+

Soil Composition Flux

+

Live telemetry from Sector 04-G

+
+Live +
+
+
+
+
+
+
+
+
+
+
+
+
+

Moisture

+

64.2%

+
+
+

Nitrogen

+

12.8 ppm

+
+
+

pH Level

+

6.8

+
+
+
+ +
+
+analytics +
+

AI Yield Prediction

+

Processing 4.2TB of multi-spectral imagery

+
+

+18.4%

+

Estimated Improvement

+
+
+ +
+ +
+

Active Nodes

+

Central Valley Cluster

+
+
+ +
+
+
+psychology +
+

AI Strategy Recommendations

+
+
+
+Optimal Corn Planting Window +Next 72 Hours +
+
+Nitrogen Injection Zone B +Priority: High +
+
+Irrigation Pulse Schedule +Active: Night Cycle +
+
+
+
+
+ +
+
+

Ready to optimize your harvest?

+

Join over 1,400 precision farmers using TerraDetect to increase yields and reduce environmental impact through real-time science.

+
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/stitch/landing_page_dark/screen.png b/stitch/landing_page_dark/screen.png new file mode 100644 index 0000000..e9455ab Binary files /dev/null and b/stitch/landing_page_dark/screen.png differ diff --git a/stitch/login_screen/code.html b/stitch/login_screen/code.html new file mode 100644 index 0000000..2caa190 --- /dev/null +++ b/stitch/login_screen/code.html @@ -0,0 +1,164 @@ + + + + + +TerraDetect - Sign In + + + + + + + + + + +
+ +
+
+ +
+
+eco +
+

TerraDetect

+

Precision agronomy at your fingertips.

+
+ +
+
+ +
+ +
+ +
+person +
+
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+sensors +
+
+

Found on the back of your TerraDetect sensor.

+
+ + +
+
+ + Create Account + open_in_new + +

New to the field? Start your journey today.

+
+
+ + +
+ + \ No newline at end of file diff --git a/stitch/login_screen/screen.png b/stitch/login_screen/screen.png new file mode 100644 index 0000000..b7ecb66 Binary files /dev/null and b/stitch/login_screen/screen.png differ diff --git a/stitch/login_screen_dark/code.html b/stitch/login_screen_dark/code.html new file mode 100644 index 0000000..fcecc5c --- /dev/null +++ b/stitch/login_screen_dark/code.html @@ -0,0 +1,203 @@ + + + + + + + + + + + + + +
+ + + +
+
+ +
+sensors +TerraDetect +
+
+

Welcome Back

+

Synchronize with your monitoring hardware.

+
+
+ +
+ +
+
+person +
+ +
+
+ +
+
+ +Forgot Access? +
+
+
+lock +
+ + +
+
+ +
+ +
+
+router +
+ +
+

Enter the 12-digit hardware identifier found on your TerraNode base station.

+
+ +
+ +
+
+
+
+ +Gateway Region: North America +
+ +
+
+ +
+
+verified_user +Encrypted SSL +
+
+shield_with_heart +ISO 27001 Certified +
+
+
+
+ \ No newline at end of file diff --git a/stitch/login_screen_dark/screen.png b/stitch/login_screen_dark/screen.png new file mode 100644 index 0000000..592f9c9 Binary files /dev/null and b/stitch/login_screen_dark/screen.png differ diff --git a/stitch/prediction_screen/code.html b/stitch/prediction_screen/code.html new file mode 100644 index 0000000..98f9fb3 --- /dev/null +++ b/stitch/prediction_screen/code.html @@ -0,0 +1,242 @@ + + + + + +TerraDetect - AI Predictions + + + + + + + + + + +
+
+
+eco +

TerraDetect

+
+
+notifications +
+
+
+
+ +
+

AI Predictions

+

Harnessing machine learning for precision agronomy.

+
+ + +
+ +
+
+

+settings_input_component + Soil Parameters +

+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+
+Top Match +

Rice

+
+
+
95%
+
Confidence
+
+
+
+
+
+
+verified +
+
+
+ +
+ +
+
+
+science +
+
Soil Prep
+
+

+ Apply 10kg Nitrogen per hectare to stabilize baseline pH before the next planting cycle. +

+
+ +
+
+
+water_drop +
+
Irrigation
+
+

+ Current rainfall patterns suggest a 15% increase in drip irrigation frequency for optimal yield. +

+
+
+ +
+
+
Environmental Synergy
+

Your soil NPK levels are in perfect alignment with the upcoming seasonal rainfall forecast.

+
+Close-up of rice paddies +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/stitch/prediction_screen/screen.png b/stitch/prediction_screen/screen.png new file mode 100644 index 0000000..d0abcde Binary files /dev/null and b/stitch/prediction_screen/screen.png differ diff --git a/stitch/prediction_screen_dark/code.html b/stitch/prediction_screen_dark/code.html new file mode 100644 index 0000000..6b5abaa --- /dev/null +++ b/stitch/prediction_screen_dark/code.html @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + +
+
+sensors +

TerraDetect

+
+
+User profile +
+
+
+ +
+

Predictive Analysis

+

Harness satellite data and ground sensors to forecast agricultural outcomes with high-precision neural modeling.

+
+ +
+ + + +
+
+ +
+
+

Soil Composition

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +
+
+cloud_done +
+
+

Live Sensor Sync

+

Data synced 2 minutes ago from Node #842

+
+
+
+ +
+ +
+ +
+
+ +
+
+
+Top Recommendation +

Oryza Sativa

+

Standard Highland Rice Variety

+
+
+
98%
+

Confidence

+
+
+
+
+ +
+
+
+water_drop +
+OPTIMAL +
+
+

Water Requirement

+

High Demand

+

Predicted consumption: 450mm / cycle

+
+
+
+
+
+calendar_month +
+ESTIMATED +
+
+

Harvest Window

+

115 - 130 Days

+

Target window: Late September

+
+
+ +
+

Secondary Candidates

+
+
+
+
M
+
+

Maize (Sweet Corn)

+

Moderate suitability for current pH

+
+
+
+

82%

+
+
+
+
+
B
+
+

Black-eyed Peas

+

Low nitrogen requirement match

+
+
+
+

64%

+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/prediction_screen_dark/screen.png b/stitch/prediction_screen_dark/screen.png new file mode 100644 index 0000000..8e5e4c7 Binary files /dev/null and b/stitch/prediction_screen_dark/screen.png differ diff --git a/stitch/registration_screen/code.html b/stitch/registration_screen/code.html new file mode 100644 index 0000000..93c391b --- /dev/null +++ b/stitch/registration_screen/code.html @@ -0,0 +1,214 @@ + + + + + +TerraDetect - Registration + + + + + + + + + + +
+
+eco +TerraDetect +
+
+
+ + +
+ +
+

Begin Your Harvest

+

Join the network of modern cultivators and start monitoring your soil vitality today.

+
+ +
+ +
+ +
+person + +
+
+ +
+ +
+mail + +
+
+ +
+ +
+lock + +
+ +
+
+Security Level +Strong +
+
+
+
+
+
+

Must contain at least 8 characters, one number, and a symbol.

+
+
+ +
+
+ +Optional +
+
+sensors + +
+
+ + +
+ +
+

+ Already have an account? + Login +

+
+
+
+ + + +
+
+spa +agriculture +compost +grass +
+

© 2024 TerraDetect Systems • Privacy Focused

+
+ \ No newline at end of file diff --git a/stitch/registration_screen/screen.png b/stitch/registration_screen/screen.png new file mode 100644 index 0000000..b9ccef1 Binary files /dev/null and b/stitch/registration_screen/screen.png differ diff --git a/stitch/registration_screen_dark/code.html b/stitch/registration_screen_dark/code.html new file mode 100644 index 0000000..bea8dfa --- /dev/null +++ b/stitch/registration_screen_dark/code.html @@ -0,0 +1,210 @@ + + + + + +TerraDetect - Registration + + + + + + + + + + +
+
+ +
+ +
+
+
+sensors +

TerraDetect

+
+

+ Observe the
Unseen. +

+

+ Join the global network of environmental surveillance. Secure your data with our high-end telemetry observatory. +

+
+ + +
+ +
+
+
+

Create Account

+

Initialize your observatory credentials.

+
+
+
+ +
+ +
+person + +
+
+ +
+ +
+alternate_email + +
+
+
+ +
+ +
+lock + + +
+
+ +
+
+ +Optional +
+
+developer_board + +
+

Link your hardware sensor now to begin real-time telemetry streaming.

+
+ +
+ +
+Already an observer? +Login to Access +
+
+
+ +
+
+security +cloud_sync +language +
+V-4.0 ENTERPRISE +
+
+
+
+ +
+abstract topographic map lines on dark navy background minimalist digital terrain grid green and blue accents editorial tech style +
+ \ No newline at end of file diff --git a/stitch/registration_screen_dark/screen.png b/stitch/registration_screen_dark/screen.png new file mode 100644 index 0000000..77bbcd2 Binary files /dev/null and b/stitch/registration_screen_dark/screen.png differ diff --git a/stitch/verdant_core/DESIGN.md b/stitch/verdant_core/DESIGN.md new file mode 100644 index 0000000..bcd93d0 --- /dev/null +++ b/stitch/verdant_core/DESIGN.md @@ -0,0 +1,73 @@ +# Design System Specification: Editorial Agronomy + +## 1. Overview & Creative North Star +The "Digital Cultivator" is the Creative North Star for this design system. We are moving away from the "utilitarian spreadsheet" look of traditional AgTech. Instead, we treat soil data and crop health with the reverence of a high-end editorial magazine. + +By leveraging **Organic Brutalism**, we combine the precision of data with the soft, fluid nature of the environment. We break the standard mobile "template" by using intentional asymmetry—where images might bleed off the edge of a card—and high-contrast typography scales that make headlines feel like a statement of authority. The result is a system that feels premium, trustworthy, and deeply connected to the earth. + +## 2. Colors & Surface Architecture +Color is not just for branding; it is for environmental storytelling. We use a palette that mirrors the vitality of a healthy crop cycle. + +### The Palette +* **Primary (The Growth Core):** `primary` (#006b2c) and `primary_container` (#00873a). Use these for high-intent actions and to signify healthy data states. +* **Secondary & Tertiary (Data Spectrum):** `secondary` (#006398) for hydration/water and `tertiary` (#825100) for soil/pH levels. +* **Neutrals (The Canvas):** `surface` (#f8f9fa) and `on_surface` (#191c1d). + +### The "No-Line" Rule +**Strict Mandate:** Designers are prohibited from using 1px solid borders to section content. Boundaries must be defined solely through background color shifts. To separate a card from the background, use `surface_container_low` sitting on a `surface` background. + +### Surface Hierarchy & Nesting +Treat the UI as a series of physical layers. +1. **Base:** `surface` (The field). +2. **Sectioning:** `surface_container` (The plot). +3. **Interaction:** `surface_container_lowest` (The focal point/card). +Nesting these tiers creates depth without the visual "noise" of lines. + +### Signature Textures & Glass +* **The Signature Gradient:** For main CTAs, use a linear gradient from `primary` to `primary_container` at a 135° angle. This adds "visual soul" and depth. +* **Glassmorphism:** Floating action buttons or navigation overlays must use `surface` with 80% opacity and a `backdrop-blur` of 12px. This ensures the UI feels integrated with the data flowing beneath it. + +## 3. Typography: The Editorial Voice +We use a dual-typeface system to balance technical precision with human warmth. + +* **Display & Headlines (Manrope):** Use `display-lg` to `headline-sm` for data summaries and section headers. Manrope’s geometric yet open nature feels modern and authoritative. +* **Body & Labels (Inter):** Use `body-lg` to `label-sm` for all reading utility. Inter is optimized for legibility in the field under high-glare conditions. +* **Editorial Hierarchy:** Always lead with a strong `headline-md`. Ensure there is a significant "jump" between headlines and body text to create a clear visual path for the user’s eye. + +## 4. Elevation & Depth: Tonal Layering +Traditional drop shadows are too "digital." We use **Tonal Layering** to mimic natural light hitting a matte surface. + +* **The Layering Principle:** Place a `surface_container_lowest` card on a `surface_container_low` background. This creates a soft, natural "lift" through color value alone. +* **Ambient Shadows:** For elements that truly float (like Modals), use a shadow with a blur of `32px`, an offset of `y: 8`, and the color `on_surface` at only **4% opacity**. It should feel like a whisper, not a smudge. +* **The "Ghost Border" Fallback:** If a border is required for accessibility, use the `outline_variant` token at **15% opacity**. Never use 100% opaque borders. + +## 5. Component Logic + +### Buttons (The Tactile Root) +* **Primary:** Rounded at `md` (1.5rem) or `full` (9999px). Uses the Signature Gradient. +* **Secondary:** No background. Use a `title-sm` font weight with `primary` color. +* **States:** On press, transition to `primary_fixed_dim`. + +### Input Fields (The Data Portal) +* **Styling:** Forgo the box. Use a `surface_container_high` background with a `full` radius. +* **Labels:** Always use `label-md` in `on_surface_variant`, floating 8px above the input. + +### Cards & Lists (The Plot View) +* **No Dividers:** Forbid the use of divider lines. Separate list items using `8px` (spacing-2) of vertical white space or by alternating between `surface` and `surface_container_low`. +* **Asymmetry:** In cards, icons (leaf, water drop) should be oversized and slightly cropped by the card edge to create a custom, high-end feel. + +### Specialized Ag-Components +* **Sensor Chips:** Small, rounded `sm` (0.5rem) containers using `secondary_container` or `tertiary_container` to categorize soil vs. weather data. +* **The "Health Meter":** A custom progress bar using a gradient transition from `error` to `primary` to visualize soil health. + +## 6. Do’s and Don’ts + +### Do +* **Do** use extreme white space. If you think it's enough, add 8px more. +* **Do** use `primary_fixed` for background accents to highlight positive data trends. +* **Do** ensure all icons are a minimum of 24x24px for easy "gloved-hand" interaction. + +### Don't +* **Don't** use pure black (#000). Use `on_surface` (#191c1d) to maintain a premium, soft-contrast look. +* **Don't** use standard Material shadows. They are too heavy for this "light-as-air" system. +* **Don't** cram multiple data points into one card. Break them into a horizontally scrolling carousel of `surface_container_lowest` cards. \ No newline at end of file diff --git a/stitch/verdant_nocturne/DESIGN.md b/stitch/verdant_nocturne/DESIGN.md new file mode 100644 index 0000000..f473f70 --- /dev/null +++ b/stitch/verdant_nocturne/DESIGN.md @@ -0,0 +1,84 @@ +# Design System Specification: Dark Mode & High-End Editorial + +## 1. Overview & Creative North Star: "The Midnight Observatory" +This design system moves away from the "SaaS-standard" look toward a high-end, editorial aesthetic titled **The Midnight Observatory**. The goal is to create an environment that feels like a premium, nocturnal data center—sophisticated, deep, and quiet. + +We break the "template" look through **intentional asymmetry** and **tonal depth**. Instead of rigid, boxed-in grids, we use expansive breathing room and layered surfaces. This is not just a "dark mode"; it is a cinematic experience where data points emerge from the shadows like stars in a night sky. + +--- + +## 2. Color & Atmospheric Layering +The palette is rooted in deep slates (`#0c1324`) and vibrant, bio-luminescent greens. We prioritize optical comfort in low-light environments. + +### The "No-Line" Rule +**Prohibit 1px solid borders for sectioning.** Boundaries must be defined solely through background color shifts. For example, a `surface_container_low` section sitting on a `surface` background creates a clean, sophisticated break without the visual clutter of a line. + +### Surface Hierarchy & Nesting +Treat the UI as a series of physical layers—like stacked sheets of smoked glass. +- **Base Layer:** `surface` (`#0c1324`) +- **Inset Layer:** `surface_container_lowest` (`#070d1f`) for recessed areas like search bars. +- **Card/Content Layer:** `surface_container` (`#191f31`) for standard content blocks. +- **Floating Layer:** `surface_bright` (`#33394c`) for elements requiring immediate attention. + +### The Glass & Gradient Rule +Floating elements (modals, popovers) must use **Glassmorphism**. Apply `surface_variant` at 60% opacity with a `20px` backdrop-blur. +- **Signature Texture:** Use a subtle linear gradient on primary CTAs transitioning from `primary` (`#62df7d`) to `primary_container` (`#1ca64d`) at a 135° angle. This adds a "jewel-toned" depth that flat colors lack. + +--- + +## 3. Typography: Editorial Authority +We pair the technical precision of **Inter** with the architectural character of **Space Grotesk**. + +* **Display & Headlines (Space Grotesk):** These are your "Hero" moments. Use `display-lg` (3.5rem) with tighter letter-spacing (-0.02em) to create an authoritative, editorial feel. +* **Body & Labels (Inter):** High-readability sans-serif. In dark mode, ensure `body-md` uses `on_surface_variant` (`#bdcaba`) for secondary text to reduce eye strain from pure white-on-black contrast. +* **The Contrast Rule:** Use `primary` (`#62df7d`) sparingly for text—only for high-priority labels or active states to maintain the premium, "low-noise" atmosphere. + +--- + +## 4. Elevation & Depth: Tonal Layering +Traditional shadows are too heavy for this aesthetic. We achieve lift through light, not shadow. + +* **The Layering Principle:** Stack `surface_container_low` on top of `surface_container_lowest` to create a soft, natural lift. +* **Ambient Shadows:** If a floating effect is required (e.g., a hovering card), use a diffuse shadow: `0 20px 40px rgba(7, 13, 31, 0.5)`. The shadow color must be a darker version of the background, never a neutral gray. +* **The "Ghost Border" Fallback:** If accessibility requires a stroke, use `outline_variant` at 15% opacity. **Never use 100% opaque borders.** +* **Glassmorphism:** For tooltips and menus, use `surface_container_highest` with a `blur(12px)` and a `0.5px` stroke of `outline` at 10% opacity to simulate the edge of a glass pane. + +--- + +## 5. Components & Interaction + +### Cards & Lists +* **No Dividers:** Forbid the use of horizontal lines. Use the **Spacing Scale** (specifically `spacing.8` or `spacing.12`) to create "Active Negative Space." +* **Card Styling:** Apply `roundedness.xl` (0.75rem). Cards should use `surface_container` on a `surface` background. + +### Data Visualization (Optimized for Dark Mode) +* **Primary Data:** `primary` (#62df7d) +* **Secondary Data (Blue):** `secondary` (#7bd0ff) +* **Tertiary Data (Orange/Gold):** `tertiary` (#ffb95f) +* **Alerts/Errors:** `error` (#ffb4ab) +* *Note: Use 70% opacity for grid lines in charts to keep the focus on the data glow.* + +### Buttons +* **Primary:** Gradient fill (Primary to Primary Container), `roundedness.full`, white text (`on_primary`). +* **Secondary:** `surface_container_highest` fill with `primary` text. No border. +* **Tertiary/Ghost:** No background. `on_surface` text with an icon. + +### Inputs +* Use `surface_container_lowest` for the field background to create an "etched into the interface" look. +* Active state: A `1px` "Ghost Border" using `primary` at 40% opacity. + +--- + +## 6. Do’s and Don’ts + +### Do: +* **Do** use asymmetrical layouts (e.g., a left-aligned headline with a right-shifted data visualization). +* **Do** leverage the `surface_container` tiers to create hierarchy. +* **Do** use `spaceGrotesk` for all numerical data to emphasize the "Observatory" theme. +* **Do** ensure a minimum contrast ratio of 4.5:1 for all functional text. + +### Don’t: +* **Don’t** use `#000000` for backgrounds; it kills the sense of depth. Use `surface` (`#0c1324`). +* **Don’t** use 1px dividers or "boxes within boxes." +* **Don’t** use standard drop shadows. If it doesn't look like light or glass, rethink it. +* **Don’t** crowd the interface. If in doubt, add `spacing.12` of padding. \ No newline at end of file