diff --git a/.env b/.env
deleted file mode 100644
index ac0c8a2..0000000
--- a/.env
+++ /dev/null
@@ -1,4 +0,0 @@
-# TabataFit Environment Variables
-# Supabase Configuration
-EXPO_PUBLIC_SUPABASE_URL=https://supabase.1000co.fr
-EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzcyMjMzMjAwLCJleHAiOjE5Mjk5OTk2MDB9.SlYN046eGvUSObW0tFQHcMRUqFvtMqBLfFRlZliSx_w
diff --git a/.env.example b/.env.example
deleted file mode 100644
index 459ffac..0000000
--- a/.env.example
+++ /dev/null
@@ -1,13 +0,0 @@
-# TabataFit Environment Variables
-# Copy this file to .env and fill in your credentials
-
-# Supabase Configuration
-EXPO_PUBLIC_SUPABASE_URL=your_supabase_project_url
-EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
-
-# RevenueCat (Apple subscriptions)
-# Defaults to test_ sandbox key if not set
-EXPO_PUBLIC_REVENUECAT_API_KEY=your_revenuecat_api_key
-
-# Admin Dashboard (optional - for admin authentication)
-EXPO_PUBLIC_ADMIN_EMAIL=admin@tabatafit.app
diff --git a/README.md b/README.md
deleted file mode 100644
index 3c13cc2..0000000
--- a/README.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# TabataFit
-
-> **Apple Fitness+ for Tabata** — The Premium HIIT Experience
-
-
-
-
-
-
-
-## Vision
-
-TabataFit est l'Apple Fitness+ du Tabata. Une expérience premium, video-first, guidée par des coachs, qui transforme 4 minutes d'exercice en une expérience de fitness immersive.
-
-## Features
-
-- 🎬 **Video-led workouts** — HD video demonstrations by professional trainers
-- ⏱️ **Smart timer** — Tabata timer with work/rest phases
-- 🔥 **Burn Bar** — Compare your calories with the community
-- 📊 **Activity tracking** — Streaks, stats, and trends
-- 🎵 **Music sync** — Curated playlists for each workout
-- ⌚ **Apple Watch** — Heart rate and activity rings
-
-## Tech Stack
-
-- **Framework**: Expo SDK 52
-- **Navigation**: Expo Router v3
-- **State**: Zustand
-- **Video**: expo-av (HLS streaming)
-- **Payments**: RevenueCat
-- **Analytics**: PostHog
-
-## Getting Started
-
-```bash
-# Install dependencies
-npm install
-
-# Start development server
-npx expo start
-
-# Run on device (scan QR with Expo Go)
-```
-
-## Documentation
-
-| Document | Description |
-|----------|-------------|
-| [PRD v2.0](./TabataFit_PRD_v2.0.md) | Product Requirements |
-| [PDD v2.0](./TabataFit_PDD_v2.0.md) | Product Design |
-| [BDSD v2.0](./TabataFit_BDSD_v2.0.md) | Brand Design |
-
-## Project Structure
-
-```
-src/
- features/
- home/ # Home tab
- workouts/ # Workouts browser
- player/ # Video player + timer
- activity/ # Stats & history
- browse/ # Filters & trainers
- profile/ # User settings
- shared/
- components/ # Reusable UI
- hooks/ # Custom hooks
- constants/ # Design tokens
-app/ # Expo Router routes
-```
-
-## Testing
-
-```bash
-# Unit tests with coverage
-npm run test:coverage
-
-# Component render tests
-npm run test:render
-
-# All unit + render tests
-npm test && npm run test:render
-
-# Maestro E2E (requires Expo dev server + simulator)
-npm run test:maestro
-
-# Admin-web tests
-cd admin-web && npm test # Unit tests
-cd admin-web && npm run test:e2e # Playwright E2E
-```
-
-### Test Coverage
-
-| Layer | Target | Tests |
-|-------|--------|-------|
-| Stores | 80%+ | playerStore, activityStore, userStore, programStore |
-| Services | 80%+ | analytics, music, purchases, sync |
-| Hooks | 70%+ | useTimer, useHaptics, useAudio, usePurchases, useMusicPlayer, useNotifications, useSupabaseData |
-| Components | 50%+ | StyledText, VideoPlayer, WorkoutCard, GlassCard, CollectionCard, modals, Skeleton |
-| Data | 80%+ | achievements, collections, programs, trainers, workouts |
-
-### E2E Tests
-
-- **Mobile (Maestro)**: Onboarding, tab navigation, program browse, workout player, activity, profile/settings
-- **Admin Web (Playwright)**: Auth, navigation, workouts CRUD, trainers, collections
-
-## License
-
-Proprietary — All rights reserved.
-
----
-
-Built with ❤️ for HIIT lovers
diff --git a/SUPABASE_MUSIC_SETUP.md b/SUPABASE_MUSIC_SETUP.md
deleted file mode 100644
index 0046754..0000000
--- a/SUPABASE_MUSIC_SETUP.md
+++ /dev/null
@@ -1,182 +0,0 @@
-# Supabase Music Storage Setup
-
-This guide walks you through setting up the Supabase Storage bucket for music tracks in TabataFit.
-
-## Overview
-
-TabataFit loads background music from Supabase Storage based on workout `musicVibe` values. The music service organizes tracks by vibe folders.
-
-## Step 1: Create the Storage Bucket
-
-1. Go to your Supabase Dashboard → Storage
-2. Click **New bucket**
-3. Name: `music`
-4. Enable **Public access** (tracks are streamed to authenticated users)
-5. Click **Create bucket**
-
-## Step 2: Set Up Folder Structure
-
-Create folders for each music vibe:
-
-```
-music/
-├── electronic/
-├── hip-hop/
-├── pop/
-├── rock/
-└── chill/
-```
-
-### Via Dashboard:
-1. Open the `music` bucket
-2. Click **New folder** for each vibe
-3. Name folders exactly as the `MusicVibe` type values
-
-### Via SQL (optional):
-```sql
--- Storage folders are virtual in Supabase
--- Just upload files with path prefixes like "electronic/track.mp3"
-```
-
-## Step 3: Upload Music Tracks
-
-### Supported Formats
-- MP3 (recommended)
-- M4A (AAC)
-- OGG (if needed)
-
-### File Naming Convention
-Name files with artist and title for best results:
-```
-Artist Name - Track Title.mp3
-```
-
-Examples:
-```
-Neon Dreams - Energy Pulse.mp3
-Urban Flow - Street Heat.mp3
-The Popstars - Summer Energy.mp3
-```
-
-### Upload via Dashboard:
-1. Open a vibe folder (e.g., `electronic/`)
-2. Click **Upload files**
-3. Select your audio files
-4. Ensure the path shows `music/electronic/`
-
-### Upload via CLI:
-```bash
-# Install Supabase CLI if not already installed
-npm install -g supabase
-
-# Login
-supabase login
-
-# Link your project
-supabase link --project-ref your-project-ref
-
-# Upload tracks
-supabase storage upload music/electronic/ "path/to/your/tracks/*.mp3"
-```
-
-## Step 4: Configure Storage Policies
-
-### RLS Policy for Authenticated Users
-
-Go to Supabase Dashboard → Storage → Policies → `music` bucket
-
-Add these policies:
-
-#### 1. Select Policy (Read Access)
-```sql
-CREATE POLICY "Allow authenticated users to read music"
-ON storage.objects FOR SELECT
-TO authenticated
-USING (bucket_id = 'music');
-```
-
-#### 2. Insert Policy (Admin Upload Only)
-```sql
-CREATE POLICY "Allow admin uploads"
-ON storage.objects FOR INSERT
-TO authenticated
-WITH CHECK (
- bucket_id = 'music'
- AND (auth.jwt() ->> 'role') = 'admin'
-);
-```
-
-### Via Dashboard UI:
-1. Go to **Storage** → **Policies**
-2. Under `music` bucket, click **New policy**
-3. Select **For SELECT** (get)
-4. Allowed operation: **Authenticated**
-5. Policy definition: `true` (or custom check)
-
-## Step 5: Test the Setup
-
-1. Start your Expo app: `npx expo start`
-2. Start a workout with music enabled
-3. Check console logs for:
- ```
- [Music] Loaded X tracks for vibe: electronic
- ```
-
-### Troubleshooting
-
-**No tracks loading:**
-- Check Supabase credentials in `.env`
-- Verify folder names match `MusicVibe` type exactly
-- Check Storage RLS policies allow read access
-
-**Tracks not playing:**
-- Ensure files are accessible (try signed URL in browser)
-- Check audio format is supported by expo-av
-- Verify CORS settings in Supabase
-
-**CORS errors:**
-Add to Supabase Dashboard → Settings → API → CORS:
-```
-app://*
-http://localhost:8081
-```
-
-## Step 6: Environment Variables
-
-Ensure your `.env` file has:
-
-```env
-EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
-EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
-```
-
-## Music Vibes Reference
-
-The app supports these vibe categories:
-
-| Vibe | Description | Typical BPM |
-|------|-------------|-------------|
-| `electronic` | EDM, House, Techno | 128-140 |
-| `hip-hop` | Rap, Trap, Beats | 85-110 |
-| `pop` | Pop hits, Dance-pop | 100-130 |
-| `rock` | Rock, Alternative | 120-160 |
-| `chill` | Lo-fi, Ambient, Downtempo | 60-90 |
-
-## Best Practices
-
-1. **Track Duration**: 3-5 minutes ideal for Tabata workouts
-2. **File Size**: Keep under 10MB for faster loading
-3. **Bitrate**: 128-192kbps MP3 for good quality/size balance
-4. **Loudness**: Normalize tracks to similar levels (-14 LUFS)
-5. **Metadata**: Include ID3 tags for artist/title info
-
-## Alternative: Local Development
-
-If Supabase is not configured, the app uses mock tracks automatically. To force mock data, temporarily set invalid Supabase credentials.
-
-## Next Steps
-
-- [ ] Upload initial track library (5-10 tracks per vibe)
-- [ ] Test on physical device
-- [ ] Consider CDN for production scale
-- [ ] Implement track favoriting/personal playlists
diff --git a/SUPABASE_SETUP.md b/SUPABASE_SETUP.md
deleted file mode 100644
index 3687fc8..0000000
--- a/SUPABASE_SETUP.md
+++ /dev/null
@@ -1,228 +0,0 @@
-# TabataFit Supabase Integration
-
-This document explains how to set up and use the Supabase backend for TabataFit.
-
-## Overview
-
-TabataFit now uses Supabase as its backend for:
-- **Database**: Storing workouts, trainers, collections, programs, and achievements
-- **Storage**: Managing video files, thumbnails, and trainer avatars
-- **Authentication**: Admin dashboard access control
-- **Real-time**: Future support for live features
-
-## Setup Instructions
-
-### 1. Create Supabase Project
-
-1. Go to [Supabase Dashboard](https://app.supabase.com)
-2. Create a new project
-3. Note your project URL and anon key
-
-### 2. Configure Environment Variables
-
-Create a `.env` file in the project root:
-
-```bash
-EXPO_PUBLIC_SUPABASE_URL=your_supabase_project_url
-EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
-```
-
-### 3. Run Database Migrations
-
-1. Go to your Supabase project's SQL Editor
-2. Open `supabase/migrations/001_initial_schema.sql`
-3. Run the entire script
-
-This creates:
-- All necessary tables (workouts, trainers, collections, programs, achievements)
-- Storage buckets (videos, thumbnails, avatars)
-- Row Level Security policies
-- Triggers for auto-updating timestamps
-
-### 4. Seed the Database
-
-Run the seed script to populate your database with initial data:
-
-```bash
-npx ts-node supabase/seed.ts
-```
-
-This will import all 50 workouts, 5 trainers, 6 collections, 3 programs, and 8 achievements.
-
-### 5. Set Up Admin User
-
-1. Go to Supabase Dashboard → Authentication → Users
-2. Create a new user with email/password
-3. Go to SQL Editor and run:
-
-```sql
-INSERT INTO admin_users (id, email, role)
-VALUES ('USER_UUID_HERE', 'admin@example.com', 'admin');
-```
-
-Replace `USER_UUID_HERE` with the actual user UUID from step 2.
-
-### 6. Configure Storage
-
-In Supabase Dashboard → Storage:
-
-1. Verify buckets exist: `videos`, `thumbnails`, `avatars`
-2. Set bucket privacy to public for all three
-3. Configure CORS if needed for your domain
-
-## Architecture
-
-### Data Flow
-
-```
-┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
-│ React Native │────▶│ Supabase │────▶│ PostgreSQL │
-│ Client │ │ Client │ │ Database │
-└─────────────────┘ └──────────────┘ └─────────────────┘
- │
- │ ┌──────────────┐
- └──────────────▶│ Admin │
- │ Dashboard │
- └──────────────┘
-```
-
-### Key Components
-
-1. **Supabase Client** (`src/shared/supabase/`)
- - `client.ts`: Configured Supabase client
- - `database.types.ts`: TypeScript definitions for tables
-
-2. **Data Service** (`src/shared/data/dataService.ts`)
- - `SupabaseDataService`: Handles all database operations
- - Falls back to mock data if Supabase is not configured
-
-3. **React Hooks** (`src/shared/hooks/useSupabaseData.ts`)
- - `useWorkouts`, `useTrainers`, `useCollections`, etc.
- - Automatic loading states and error handling
-
-4. **Admin Service** (`src/admin/services/adminService.ts`)
- - CRUD operations for content management
- - File upload/delete for storage
- - Admin authentication
-
-5. **Admin Dashboard** (`app/admin/`)
- - `/admin/login`: Authentication screen
- - `/admin`: Main dashboard with stats
- - `/admin/workouts`: Manage workouts
- - `/admin/trainers`: Manage trainers
- - `/admin/collections`: Manage collections
- - `/admin/media`: Storage management
-
-## Database Schema
-
-### Tables
-
-| Table | Description |
-|-------|-------------|
-| `trainers` | Trainer profiles with colors and avatars |
-| `workouts` | Workout definitions with exercises and metadata |
-| `collections` | Curated workout collections |
-| `collection_workouts` | Many-to-many link between collections and workouts |
-| `programs` | Multi-week workout programs |
-| `program_workouts` | Link between programs and workouts with week/day |
-| `achievements` | User achievement definitions |
-| `admin_users` | Admin dashboard access control |
-
-### Storage Buckets
-
-| Bucket | Purpose | Access |
-|--------|---------|--------|
-| `videos` | Workout videos | Public read, Admin write |
-| `thumbnails` | Workout thumbnails | Public read, Admin write |
-| `avatars` | Trainer avatars | Public read, Admin write |
-
-## Using the App
-
-### As a User
-
-The app works seamlessly:
-- If Supabase is configured: Loads data from the cloud
-- If not configured: Falls back to local mock data
-
-### As an Admin
-
-1. Navigate to `/admin` in the app
-2. Sign in with your admin credentials
-3. Manage content through the dashboard:
- - View all workouts, trainers, collections
- - Delete items (create/edit coming soon)
- - Upload media files
-
-## Development
-
-### Adding New Workouts
-
-```typescript
-import { adminService } from '@/src/admin/services/adminService'
-
-await adminService.createWorkout({
- title: 'New Workout',
- trainer_id: 'emma',
- category: 'full-body',
- level: 'Beginner',
- duration: 4,
- calories: 45,
- rounds: 8,
- prep_time: 10,
- work_time: 20,
- rest_time: 10,
- equipment: ['No equipment'],
- music_vibe: 'electronic',
- exercises: [
- { name: 'Jumping Jacks', duration: 20 },
- // ...
- ],
-})
-```
-
-### Uploading Media
-
-```typescript
-const videoUrl = await adminService.uploadVideo(file, 'workout-1.mp4')
-const thumbnailUrl = await adminService.uploadThumbnail(file, 'workout-1.jpg')
-```
-
-## Troubleshooting
-
-### "Supabase is not configured" Warning
-
-This is expected if environment variables are not set. The app will use mock data.
-
-### Authentication Errors
-
-1. Verify admin user exists in `admin_users` table
-2. Check that email/password match
-3. Ensure user is confirmed in Supabase Auth
-
-### Storage Upload Failures
-
-1. Verify storage buckets exist
-2. Check RLS policies allow admin uploads
-3. Ensure file size is within limits
-
-### Data Not Syncing
-
-1. Check network connection
-2. Verify Supabase URL and key are correct
-3. Check browser console for errors
-
-## Security Considerations
-
-- Row Level Security (RLS) is enabled on all tables
-- Public can only read content
-- Only admin users can write content
-- Storage buckets have public read access
-- Storage uploads restricted to admin users
-
-## Future Enhancements
-
-- [ ] Real-time workout tracking
-- [ ] User progress sync across devices
-- [ ] Offline support with local caching
-- [ ] Push notifications
-- [ ] Analytics and user insights
diff --git a/TabataFit_BDSD_v2.0.md b/TabataFit_BDSD_v2.0.md
deleted file mode 100644
index 704f1a9..0000000
--- a/TabataFit_BDSD_v2.0.md
+++ /dev/null
@@ -1,594 +0,0 @@
-# TabataFit — Brand Design Specification v2.0
-> Apple Liquid Glass Design for Tabata
-
----
-
-## Brand Positioning
-
-**TabataFit = Apple Fitness+ avec Liquid Glass (iOS 18.4)**
-
-| Attribute | Description |
-|-----------|-------------|
-| **Analogy** | "If Apple made a Tabata app in 2026" |
-| **Vibe** | Premium, glassy, fluid, immersive |
-| **Emotion** | Confident, empowering, sleek |
-| **Differentiator** | Video-led + Liquid Glass UI |
-
----
-
-## 🪟 Liquid Glass System (iOS 18.4 Style)
-
-### Concept
-Le **Liquid Glass** est le nouveau design language d'Apple qui combine :
-- **Glassmorphism avancé** — Blur dynamique, transparence multicouche
-- **Fluidité organique** — Formes arrondies, animations liquides
-- **Lumière réactive** — Reflets, glows qui répondent au contenu
-- **Profondeur atmosphérique** — Layers de verre empilés
-
-### Glass Layers
-
-```typescript
-const GLASS = {
- // Base glass (surfaces)
- BASE: {
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
- backdropFilter: 'blur(40px)',
- borderWidth: 1,
- borderColor: 'rgba(255, 255, 255, 0.1)',
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 8 },
- shadowOpacity: 0.3,
- shadowRadius: 32,
- },
-
- // Elevated glass (cards, modals)
- ELEVATED: {
- backgroundColor: 'rgba(255, 255, 255, 0.08)',
- backdropFilter: 'blur(60px)',
- borderWidth: 1,
- borderColor: 'rgba(255, 255, 255, 0.15)',
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 12 },
- shadowOpacity: 0.4,
- shadowRadius: 48,
- },
-
- // Inset glass (input fields, controls)
- INSET: {
- backgroundColor: 'rgba(0, 0, 0, 0.2)',
- backdropFilter: 'blur(20px)',
- borderWidth: 1,
- borderColor: 'rgba(255, 255, 255, 0.05)',
- },
-
- // Tinted glass (accent overlays)
- TINTED: {
- backgroundColor: 'rgba(255, 107, 53, 0.15)', // Brand tint
- backdropFilter: 'blur(40px)',
- borderWidth: 1,
- borderColor: 'rgba(255, 107, 53, 0.3)',
- },
-}
-```
-
-### Liquid Animations
-
-```typescript
-const LIQUID = {
- // Morphing shapes
- MORPH: {
- duration: 600,
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
- },
-
- // Ripple effect on tap
- RIPPLE: {
- scale: { from: 0.8, to: 1 },
- opacity: { from: 0.5, to: 0 },
- duration: 400,
- },
-
- // Breathing glow
- BREATHE: {
- scale: { from: 1, to: 1.02 },
- shadowRadius: { from: 20, to: 30 },
- duration: 2000,
- loop: true,
- },
-
- // Slide with liquid easing
- SLIDE: {
- damping: 20,
- stiffness: 300,
- mass: 1,
- },
-}
-```
-
----
-
-## Visual Identity
-
-### Logo Concept
-
-```
-┌─────────────────────────────┐
-│ │
-│ TABATA │ ← Bold, black
-│ FIT │ ← Accent orange
-│ │
-│ [Flame icon] │ ← Optional mark
-│ │
-└─────────────────────────────┘
-```
-
-### Brand Voice
-
-| DO | DON'T |
-|----|-------|
-| "Let's burn." | "Get ripped fast!" |
-| "4 minutes to stronger." | "Lose weight now!" |
-| "Your daily dose of HIIT." | "The #1 fitness app!" |
-| Minimal, confident copy | Excessive exclamation marks!! |
-
----
-
-## Color Palette
-
-### Primary Colors
-
-```
-┌─────────────────────────────────────────────────────────┐
-│ │
-│ BLACK ████████████ #000000 Background │
-│ CHARCOAL ████████████ #1C1C1E Surfaces │
-│ SLATE ████████████ #2C2C2E Elevated │
-│ │
-│ FLAME ████████████ #FF6B35 Brand accent │
-│ FLAME LIGHT ████████████ #FF8C5A Highlights │
-│ │
-│ ICE ████████████ #5AC8FA Rest phases │
-│ GLOW ████████████ #FFD60A Achievement │
-│ ENERGY ████████████ #30D158 Success/Complete│
-│ │
-└─────────────────────────────────────────────────────────┘
-```
-
-### Semantic Usage
-
-| Color | Hex | Usage |
-|-------|-----|-------|
-| **Black** | #000000 | Main background, video frames |
-| **Charcoal** | #1C1C1E | Cards, raised surfaces |
-| **Slate** | #2C2C2E | Modals, elevated elements |
-| **Flame** | #FF6B35 | Work phase, CTAs, brand |
-| **Ice** | #5AC8FA | Rest phase, calm states |
-| **Glow** | #FFD60A | Achievement badges, streaks |
-| **Energy** | #30D158 | Complete states, success |
-
-### Phase Colors (Critical)
-
-```typescript
-const PHASE_COLORS = {
- PREP: '#FF9500', // Orange-yellow - get ready
- WORK: '#FF6B35', // Flame orange - WORK!
- REST: '#5AC8FA', // Ice blue - recover
- COMPLETE: '#30D158', // Energy green - done!
-}
-```
-
----
-
-## Typography
-
-### Font Stack
-
-**Primary**: Inter (Google Fonts)
-- Clean, modern, excellent readability
-- Variable weight support
-- Apple SF Pro alternative
-
-### Type Scale
-
-| Style | Size | Weight | Use Case |
-|-------|------|--------|----------|
-| **HERO** | 48px | 900 | Marketing, celebration |
-| **TITLE_1** | 34px | 700 | Screen titles |
-| **TITLE_2** | 28px | 700 | Section headers |
-| **TITLE_3** | 22px | 600 | Card titles |
-| **BODY** | 17px | 400 | Default text |
-| **BODY_BOLD** | 17px | 600 | Emphasis |
-| **CAPTION** | 15px | 400 | Metadata |
-| **MICRO** | 13px | 400 | Small labels |
-| **TIMER** | 96px | 900 | Countdown display |
-
-### Timer Typography (Special)
-
-```typescript
-const TIMER_STYLES = {
- // Main countdown number
- NUMBER: {
- fontFamily: 'Inter_900Black',
- fontSize: 96,
- fontVariant: ['tabular-nums'], // Monospace digits
- letterSpacing: -2,
- },
-
- // Phase label (WORK, REST)
- PHASE: {
- fontFamily: 'Inter_700Bold',
- fontSize: 24,
- letterSpacing: 2,
- textTransform: 'uppercase',
- },
-
- // Round indicator
- ROUND: {
- fontFamily: 'Inter_500Medium',
- fontSize: 17,
- fontVariant: ['tabular-nums'],
- },
-}
-```
-
----
-
-## Spacing System
-
-```typescript
-const SPACING = {
- // Base unit: 4px
- 0: 0,
- 1: 4,
- 2: 8,
- 3: 12,
- 4: 16,
- 5: 20,
- 6: 24,
- 8: 32,
- 10: 40,
- 12: 48,
- 16: 64,
-
- // Semantic
- XS: 4,
- SM: 8,
- MD: 16,
- LG: 24,
- XL: 32,
- XXL: 48,
-}
-
-const LAYOUT = {
- SCREEN_PADDING: 24, // Horizontal screen padding
- CARD_RADIUS: 16, // Standard card radius
- BUTTON_RADIUS: 12, // Button radius
- TAB_BAR_HEIGHT: 80, // Bottom tab bar
- STATUS_BAR: 44, // iOS status bar
-}
-```
-
----
-
-## Component Styles
-
-### Cards
-
-```typescript
-const CARD = {
- CONTAINER: {
- backgroundColor: '#1C1C1E',
- borderRadius: 16,
- overflow: 'hidden',
- },
-
- THUMBNAIL: {
- aspectRatio: 16/9,
- backgroundColor: '#2C2C2E',
- },
-
- CONTENT: {
- padding: 16,
- },
-
- // Variants
- FEATURED: {
- borderRadius: 20,
- },
-
- COMPACT: {
- flexDirection: 'row',
- borderRadius: 12,
- },
-}
-```
-
-### Buttons
-
-```typescript
-const BUTTON = {
- PRIMARY: {
- backgroundColor: '#FF6B35',
- paddingVertical: 16,
- paddingHorizontal: 24,
- borderRadius: 12,
- alignItems: 'center',
- justifyContent: 'center',
- },
-
- PRIMARY_TEXT: {
- color: '#FFFFFF',
- fontFamily: 'Inter_600SemiBold',
- fontSize: 17,
- letterSpacing: 0.5,
- },
-
- SECONDARY: {
- backgroundColor: 'transparent',
- borderWidth: 1,
- borderColor: '#3A3A3C',
- },
-
- GHOST: {
- backgroundColor: 'transparent',
- },
-}
-```
-
-### Timer Display
-
-```typescript
-const TIMER = {
- CONTAINER: {
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- padding: 24,
- background: 'linear-gradient(transparent, rgba(0,0,0,0.8))',
- },
-
- NUMBER: {
- fontFamily: 'Inter_900Black',
- fontSize: 96,
- color: '#FFFFFF',
- textAlign: 'center',
- },
-
- PROGRESS_BAR: {
- height: 4,
- backgroundColor: 'rgba(255,255,255,0.2)',
- borderRadius: 2,
- marginTop: 16,
- },
-
- PROGRESS_FILL: {
- height: '100%',
- borderRadius: 2,
- // Color based on phase
- },
-}
-```
-
----
-
-## Animation Specifications
-
-### Timing Constants
-
-```typescript
-const DURATION = {
- INSTANT: 100,
- FAST: 200,
- NORMAL: 300,
- SLOW: 500,
- XSLow: 800,
-}
-
-const EASING = {
- // Standard iOS ease
- DEFAULT: 'ease-out',
-
- // Spring animations
- BOUNCY: {
- damping: 15,
- stiffness: 180,
- },
-
- GENTLE: {
- damping: 20,
- stiffness: 100,
- },
-}
-```
-
-### Key Animations
-
-**Timer Countdown:**
-```typescript
-// Number pulses slightly each second
-Animated.sequence([
- Animated.timing(scale, { toValue: 1.05, duration: 100 }),
- Animated.timing(scale, { toValue: 1, duration: 100 }),
-])
-```
-
-**Progress Bar:**
-```typescript
-// Smooth linear fill during phase
-Animated.timing(width, {
- toValue: 100,
- duration: phaseDuration,
- easing: Easing.linear,
-})
-```
-
-**Phase Transition:**
-```typescript
-// Color crossfade
-Animated.timing(colorProgress, {
- toValue: 1,
- duration: 300,
-})
-```
-
-**Workout Complete:**
-```typescript
-// Celebration with scale + fade
-Animated.parallel([
- Animated.spring(scale, { toValue: 1, ...BOUNCY }),
- Animated.timing(opacity, { toValue: 1, duration: 500 }),
-])
-```
-
----
-
-## Icon System
-
-Using SF Symbols / Ionicons equivalent:
-
-| Context | Icon | Size |
-|---------|------|------|
-| Home Tab | house.fill | 24 |
-| Workouts Tab | flame.fill | 24 |
-| Activity Tab | chart.bar.fill | 24 |
-| Browse Tab | square.grid.2x2.fill | 24 |
-| Profile Tab | person.fill | 24 |
-| Play | play.fill | 20 |
-| Pause | pause.fill | 20 |
-| Close | xmark | 20 |
-| Back | chevron.left | 20 |
-| Forward | chevron.right | 20 |
-| Heart | heart.fill | 20 |
-| Search | magnifyingglass | 20 |
-
----
-
-## Video Player UI
-
-### Overlay Layout
-
-```
-┌─────────────────────────────────────────────┐
-│ [Close X] [♥︎] [···] │ ← Top bar
-│ │
-│ │
-│ [VIDEO CONTENT] │
-│ │
-│ │
-│ │
-│ ┌─────────────────────────────────────────┐ │
-│ │ 🔥 WORK │ │
-│ │ │ │
-│ │ 00:14 │ │ ← Timer overlay
-│ │ │ │
-│ │ Round 3 of 8 │ │
-│ │ ████████████░░░░ │ │
-│ └─────────────────────────────────────────┘ │
-└─────────────────────────────────────────────┘
-```
-
-### Video Requirements
-
-- **Resolution**: 1080p minimum
-- **Aspect**: 16:9 (landscape) or 9:16 (portrait mode)
-- **Format**: HLS (m3u8) for streaming
-- **Audio**: AAC, 128kbps minimum
-- **Thumbnail**: JPG, same aspect ratio
-
----
-
-## Sound Design
-
-### Sound Effects
-
-| Event | Sound | Description |
-|-------|-------|-------------|
-| Phase Start | "ding" | Quick, bright chime |
-| Count 3-2-1 | "tick" | Subtle tick sound |
-| Workout Start | "whoosh" | Energy whoosh |
-| Workout Complete | "success" | Celebratory chime |
-| Button Tap | "tap" | Soft tap feedback |
-| Streak Achieved | "fire" | Crackling fire |
-
-### Haptics
-
-| Event | Haptic |
-|-------|--------|
-| Phase change | Medium |
-| Button tap | Light |
-| Countdown tick | Selection |
-| Workout complete | Success |
-| Error | Error |
-
----
-
-## Dark Mode Only
-
-TabataFit is **dark mode only** — no light mode. This is a deliberate design choice matching Apple Fitness+ and creating an immersive, cinematic experience.
-
-Reasons:
-1. Better video contrast
-2. Less eye strain during workouts
-3. Premium feel
-4. Consistent with fitness studio lighting
-
----
-
-## Image Guidelines
-
-### Trainer Photos
-
-- Professional, high-quality headshots
-- Warm, approachable expressions
-- Fitness attire visible
-- Consistent lighting style
-- Background: Dark or studio setting
-
-### Workout Thumbnails
-
-- Action shot from the workout
-- Clear exercise demonstration
-- Trainer visible
-- Dark/gradient background
-- Text overlay: Title, duration, level
-
-### Collection Banners
-
-- 16:9 aspect ratio
-- Composite of trainer + exercises
-- Gradient overlay for text
-- Brand accent color elements
-
----
-
-## Copy Guidelines
-
-### Tone
-
-- **Confident** but not arrogant
-- **Encouraging** but not cheesy
-- **Clear** and direct
-- **Minimal** — let the content speak
-
-### Examples
-
-| Context | Good | Bad |
-|---------|------|-----|
-| CTA | "Start Workout" | "Start Your Workout Now!" |
-| Empty state | "No workouts yet" | "You haven't done any workouts :(" |
-| Error | "Connection lost" | "Oh no! Something went wrong!" |
-| Success | "Workout complete" | "AMAZING! You crushed it!!!" |
-
-### Coach Dialogue
-
-- Motivational but authentic
-- Form cues during exercises
-- Breathing reminders during rest
-- Encouragement without clichés
-
----
-
-*Document created: February 18, 2026*
-*Version: 2.0*
-*Brand: Apple Fitness+ inspired*
diff --git a/TabataFit_PDD_v2.0.md b/TabataFit_PDD_v2.0.md
deleted file mode 100644
index e368f86..0000000
--- a/TabataFit_PDD_v2.0.md
+++ /dev/null
@@ -1,765 +0,0 @@
-# TabataFit — Product Design Document v2.0
-> Apple Fitness+ Design Language for Tabata
-
----
-
-## Design Philosophy
-
-**"Make it feel like a premium fitness studio in your pocket."**
-
-TabataFit adopts le design language d'Apple Fitness+ :
-- **Dark mode premium** — Fond noir profond, couleurs vibrantes
-- **Video-first** — Le contenu est le héros
-- **Typography bold** — Gros titres, textes épurés
-- **Subtle animations** — Transitions fluides, feedback délicat
-- **Inclusive imagery** — Diversité des coachs et body types
-
----
-
-## Color System — Dark Premium
-
-### Background Colors
-
-```typescript
-const COLORS = {
- // Backgrounds
- BACKGROUND: '#000000', // Pure black — comme Apple TV
- SURFACE: '#1C1C1E', // Raised surfaces
- ELEVATED: '#2C2C2E', // Cards, modals
- OVERLAY: 'rgba(0,0,0,0.6)', // Video overlays
-
- // Brand Accent (Vibrant Orange-Red)
- BRAND: '#FF6B35', // Energy, action
- BRAND_LIGHT: '#FF8C5A', // Highlights
- BRAND_DARK: '#E55A25', // Pressed states
-
- // Secondary Accents
- SUCCESS: '#34C759', // Completed, streaks
- WARNING: '#FF9500', // Rest phases
- INFO: '#5AC8FA', // Tips, info
-
- // Text
- TEXT_PRIMARY: '#FFFFFF', // Main text
- TEXT_SECONDARY: '#EBEBF5', // Secondary (87% opacity)
- TEXT_TERTIARY: '#EBEBF599', // Tertiary (60% opacity)
- TEXT_DISABLED: '#3A3A3C', // Disabled text
-
- // Semantic
- WORK: '#FF6B35', // Active work phase
- REST: '#5AC8FA', // Rest phase (calm blue)
- PREP: '#FF9500', // Countdown prep
-}
-```
-
-### Gradient Presets
-
-```typescript
-const GRADIENTS = {
- // Hero banners
- HERO_WORK: ['#FF6B35', '#E55A25'],
- HERO_REST: ['#5AC8FA', '#007AFF'],
- HERO_FEAT: ['#1C1C1E', '#000000'],
-
- // Video overlays
- VIDEO_OVERLAY: ['transparent', 'rgba(0,0,0,0.8)'],
- VIDEO_TOP: ['rgba(0,0,0,0.4)', 'transparent'],
-
- // Buttons
- CTA: ['#FF6B35', '#FF8C5A'],
-}
-```
-
----
-
-## Typography System — Apple SF Pro Style
-
-```typescript
-const TYPOGRAPHY = {
- // Hero/Display
- HERO: {
- fontFamily: 'Inter_900Black',
- fontSize: 48,
- lineHeight: 56,
- letterSpacing: -1,
- },
-
- // Section Headers (like Apple Fitness+)
- TITLE_1: {
- fontFamily: 'Inter_700Bold',
- fontSize: 34,
- lineHeight: 41,
- letterSpacing: 0.37,
- },
-
- TITLE_2: {
- fontFamily: 'Inter_700Bold',
- fontSize: 28,
- lineHeight: 34,
- letterSpacing: 0.36,
- },
-
- TITLE_3: {
- fontFamily: 'Inter_600SemiBold',
- fontSize: 22,
- lineHeight: 28,
- letterSpacing: 0.35,
- },
-
- // Body
- BODY: {
- fontFamily: 'Inter_400Regular',
- fontSize: 17,
- lineHeight: 22,
- letterSpacing: -0.41,
- },
-
- BODY_BOLD: {
- fontFamily: 'Inter_600SemiBold',
- fontSize: 17,
- lineHeight: 22,
- letterSpacing: -0.41,
- },
-
- // Metadata
- CAPTION_1: {
- fontFamily: 'Inter_400Regular',
- fontSize: 15,
- lineHeight: 20,
- letterSpacing: -0.24,
- },
-
- CAPTION_2: {
- fontFamily: 'Inter_400Regular',
- fontSize: 14,
- lineHeight: 18,
- letterSpacing: -0.15,
- },
-
- // Timer (special)
- TIMER: {
- fontFamily: 'Inter_900Black',
- fontSize: 96,
- lineHeight: 96,
- letterSpacing: -2,
- },
-
- TIMER_PHASE: {
- fontFamily: 'Inter_700Bold',
- fontSize: 24,
- lineHeight: 28,
- letterSpacing: 2, // Uppercase tracking
- textTransform: 'uppercase',
- },
-}
-```
-
----
-
-## Screen Designs
-
-### 1. Home Tab — "For You"
-
-#### Layout Structure
-
-```
-┌─────────────────────────────────────────────────────┐
-│ status bar │
-├─────────────────────────────────────────────────────┤
-│ │
-│ Bonjour, Alex [Profile] │ ← 24px padding
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ [VIDEO PREVIEW - LOOPING] │ │ ← Hero Card
-│ │ │ │ 16:9 aspect
-│ │ ┌─────────────────────────────────┐ │ │
-│ │ │ 🔥 FEATURED │ │ │
-│ │ │ │ │ │
-│ │ │ FULL BODY BURN │ │ │ ← Text overlay
-│ │ │ 4 min • Beginner • Emma │ │ │ on video
-│ │ │ │ │ │
-│ │ │ [▶️ START] [♡ Save] │ │ │
-│ │ └─────────────────────────────────┘ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Continue See All → │ ← Section header
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
-│ │ [Thumb] │ │ [Thumb] │ │ [Thumb] │ │ ← Horizontal scroll
-│ │ ━━━━━ │ │ ━━━━━ │ │ ━━━━━ │ │ 140x200px cards
-│ │ 65% │ │ 30% │ │ 10% │ │
-│ │ Core │ │ HIIT │ │ Full │ │
-│ │ Burn │ │ Extreme │ │ Body │ │
-│ └─────────┘ └─────────┘ └─────────┘ │
-│ │
-│ Popular This Week │
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
-│ │ [Thumb] │ │ [Thumb] │ │ [Thumb] │ │ [Thumb] │ │ ← Smaller cards
-│ │ │ │ │ │ │ │ │ │ │ 120x120px
-│ │ Quick │ │ Strength│ │ Cardio │ │ Core │ │
-│ │ Burn │ │ Tabata │ │ Blast │ │ Crush │ │
-│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
-│ │
-│ Collections │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ 🌅 Morning Energizer │ │ ← Full-width cards
-│ │ 5 workouts • 20 min total │ │
-│ └─────────────────────────────────────────────┘ │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ 🔥 7-Day Challenge │ │
-│ │ 7 workouts • Progressive intensity │ │
-│ └─────────────────────────────────────────────┘ │
-│ │
-├─────────────────────────────────────────────────────┤
-│ [Home] [Workouts] [Activity] [Browse] [Profile] │
-└─────────────────────────────────────────────────────┘
-```
-
-#### Component Specs
-
-**Hero Card:**
-- Full width - 48px padding
-- 16:9 aspect ratio
-- Video preview looping (muted)
-- Gradient overlay: transparent → rgba(0,0,0,0.8)
-- Featured badge top-left
-- Title, metadata, CTA bottom
-
-**Continue Watching Card:**
-- 140px width × 200px height
-- Thumbnail with progress bar overlay
-- Progress percentage badge
-- Workout name + duration
-
-**Popular Card:**
-- 120px × 120px square
-- Thumbnail only
-- Category name below
-
-**Collection Card:**
-- Full width - 48px padding
-- 80px height
-- Icon + title + description
-- Chevron right
-
----
-
-### 2. Workouts Tab
-
-#### Layout Structure
-
-```
-┌─────────────────────────────────────────────────────┐
-│ status bar │
-├─────────────────────────────────────────────────────┤
-│ │
-│ Workouts [🔍 Search] │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ [LARGE CATEGORY THUMBNAIL] │ │
-│ │ │ │
-│ │ 🔥 QUICK BURN │ │
-│ │ 4 min • All levels │ │
-│ │ 12 workouts │ │
-│ │ [→] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ [LARGE CATEGORY THUMBNAIL] │ │
-│ │ │ │
-│ │ 💪 STRENGTH TABATA │ │
-│ │ 8 min • Intermediate │ │
-│ │ 8 workouts │ │
-│ │ [→] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ 🏃 CARDIO BLAST │ │
-│ │ 4-12 min • All levels • 15 workouts [→] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ 🧘 CORE & FLEXIBILITY │ │
-│ │ 4 min • Beginner • 6 workouts [→] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ ⚡ HIIT EXTREME │ │
-│ │ 12-20 min • Advanced • 10 workouts [→] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-├─────────────────────────────────────────────────────┤
-│ [Home] [Workouts] [Activity] [Browse] [Profile] │
-└─────────────────────────────────────────────────────┘
-```
-
-#### Category Detail View
-
-```
-┌─────────────────────────────────────────────────────┐
-│ ← Quick Burn │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ 4 min • All levels • 12 workouts │ │
-│ │ │ │
-│ │ Filter: [All] [Beginner] [Intermediate] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ ┌──────┐ │ │
-│ │ │[Vid] │ Full Body Ignite │ │
-│ │ │ │ 4 min • Beginner • Emma │ │
-│ │ └──────┘ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ ┌──────┐ │ │
-│ │ │[Vid] │ Cardio Crusher │ │
-│ │ │ │ 4 min • Intermediate • Alex │ │
-│ │ └──────┘ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ ┌──────┐ │ │
-│ │ │[Vid] │ Lower Body Blast │ │
-│ │ │ │ 4 min • Intermediate • Jake │ │
-│ │ └──────┘ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
----
-
-### 3. Pre-Workout Detail Screen
-
-```
-┌─────────────────────────────────────────────────────┐
-│ ← [♡] [···] │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ │ │
-│ │ [VIDEO PREVIEW - LOOPING] │ │
-│ │ │ │
-│ │ Coach Emma in action │ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ FULL BODY IGNITE │
-│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
-│ │
-│ 👩 Emma • 💪 Beginner • ⏱️ 4 min • 🔥 45cal │
-│ │
-│ ───────────────────────────────────────────────── │
-│ │
-│ What You'll Need │
-│ ○ No equipment required │
-│ ○ Yoga mat optional │
-│ │
-│ ───────────────────────────────────────────────── │
-│ │
-│ Exercises (8 rounds) │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ 1. Jump Squats 20s work │ │
-│ │ 2. Mountain Climbers 20s work │ │
-│ │ 3. Burpees 20s work │ │
-│ │ 4. High Knees 20s work │ │
-│ │ ─────────────────────── │ │
-│ │ Repeat × 2 rounds │ │
-│ └─────────────────────────────────────────────┘ │
-│ │
-│ ───────────────────────────────────────────────── │
-│ │
-│ Music │
-│ 🎵 Electronic Energy │
-│ Upbeat, high-energy electronic tracks │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ ▶️ START WORKOUT │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
----
-
-### 4. Active Workout Screen — The Core Experience
-
-#### Work Phase
-
-```
-┌─────────────────────────────────────────────────────┐
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ │ [FULL SCREEN VIDEO] │ │
-│ │ │ │
-│ │ Coach doing Jump Squats │ │
-│ │ in perfect form │ │
-│ │ │ │
-│ │ │ │
-│ │ ┌───────────────────────────────────────┐ │ │
-│ │ │ ┌─────────────────────────────────┐ │ │ │
-│ │ │ │ 🔥 WORK │ │ │ │ ← Timer overlay
-│ │ │ │ │ │ │ │ bottom gradient
-│ │ │ │ 00:14 │ │ │ │
-│ │ │ │ │ │ │ │
-│ │ │ │ Round 3 of 8 │ │ │ │
-│ │ │ │ ████████████░░░░ 65% │ │ │ │
-│ │ │ └─────────────────────────────────┘ │ │ │
-│ │ └───────────────────────────────────────┘ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ JUMP SQUATS │
-│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
-│ │
-│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
-│ │ 52 │ │ 142 │ │ 85% │ │
-│ │ CALORIES │ │ BPM │ │ EFFORT │ │
-│ └───────────┘ └───────────┘ └───────────┘ │
-│ │
-│ Burn Bar │
-│ ░░░░░░░░████████████████░░░░ 72nd percentile │
-│ │
-│ [⏸️ Pause] [⛶ Fullscreen]│
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
-#### Rest Phase
-
-```
-┌─────────────────────────────────────────────────────┐
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ [COACH IN REST POSITION] │ │
-│ │ │ │
-│ │ "Shake it out, take a breath" │ │
-│ │ │ │
-│ │ ┌───────────────────────────────────────┐ │ │
-│ │ │ ┌─────────────────────────────────┐ │ │ │
-│ │ │ │ 💙 REST │ │ │ │ ← Blue rest theme
-│ │ │ │ │ │ │ │
-│ │ │ │ 00:08 │ │ │ │
-│ │ │ │ │ │ │ │
-│ │ │ │ Next: Mountain Climbers │ │ │ │
-│ │ │ │ ░░░░░░░░░░░░░░░░░░░ 40% │ │ │ │
-│ │ │ └─────────────────────────────────┘ │ │ │
-│ │ └───────────────────────────────────────┘ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ UP NEXT │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ ┌──────┐ │ │
-│ │ │ GIF │ Mountain Climbers │ │
-│ │ │preview│ "Core engaged, drive knees forward" │ │
-│ │ └──────┘ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ [⏸️ Pause] [⛶ Fullscreen]│
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
-#### 3-2-1 Countdown (Pre-Work)
-
-```
-┌─────────────────────────────────────────────────────┐
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ │ GET READY! │ │
-│ │ │ │
-│ │ 3 │ │ ← Giant number
-│ │ │ │ centered
-│ │ │ │
-│ │ JUMP SQUATS │ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
----
-
-### 5. Workout Complete Screen
-
-```
-┌─────────────────────────────────────────────────────┐
-│ │
-│ │
-│ 🎉 │
-│ │
-│ WORKOUT COMPLETE │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ [ANIMATED CELEBRATION RINGS] │ │
-│ │ │ │
-│ │ 🔥 💪 ⚡ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
-│ │ 52 │ │ 4 │ │ 100% │ │
-│ │ CALORIES │ │ MINUTES │ │ COMPLETE │ │
-│ └───────────┘ └───────────┘ └───────────┘ │
-│ │
-│ Burn Bar │
-│ You beat 73% of users! │
-│ ░░░░░░░░████████████████░░░░ │
-│ │
-│ ───────────────────────────────────────────────── │
-│ │
-│ 🔥 7 Day Streak! │
-│ Keep the momentum going! │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ 📤 SHARE YOUR WORKOUT │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ ← BACK TO HOME │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Recommended Next │
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
-│ │ [Thumb] │ │ [Thumb] │ │ [Thumb] │ │
-│ │ Core │ │ Upper │ │ Cardio │ │
-│ │ Crush │ │ Body │ │ Blast │ │
-│ └─────────┘ └─────────┘ └─────────┘ │
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
----
-
-### 6. Activity Tab
-
-```
-┌─────────────────────────────────────────────────────┐
-│ │
-│ Activity │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ 🔥 STREAK │ │
-│ │ │ │
-│ │ 7 │ │
-│ │ DAYS │ │
-│ │ │ │
-│ │ ● ● ● ● ● ● ● ○ ○ ○ │ │
-│ │ M T W T F S S M T W │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ This Week │
-│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
-│ │ 5 │ │ 156 │ │ 20 │ │
-│ │ WORKOUTS │ │ CALORIES │ │ MINUTES │ │
-│ │ 5 goal │ │ 150 goal │ │ 20 goal │ │
-│ └───────────┘ └───────────┘ └───────────┘ │
-│ │
-│ Monthly Summary │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ [CALENDAR HEAT MAP] │ │
-│ │ │ │
-│ │ Jan 2026 │ │
-│ │ S M T W T F S │ │
-│ │ 1 2 3 4 5 6 │ │
-│ │ 7 8 9 10 11 12 13 │ │
-│ │ 14 15 16 17 18 19 20 │ │
-│ │ ░ ░ █ █ ░ █ █ │ │
-│ │ █ █ ░ █ █ ░ ░ │ │
-│ │ ░ █ █ █ █ █ █ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Trends │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ 📈 Workouts trending up! │ │
-│ │ +23% vs last month │ │
-│ │ │ │
-│ │ [WEEKLY CHART] │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Burn Bar Position │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ Your average: 45 cal/workout │ │
-│ │ ████████████░░░░ 68th percentile │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
----
-
-### 7. Browse Tab
-
-```
-┌─────────────────────────────────────────────────────┐
-│ │
-│ Browse │
-│ │
-│ Filters [Edit] │
-│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
-│ │ All ▼ │ │ 4 min │ │ 8 min │ │ Begin │ │
-│ └────────┘ └────────┘ └────────┘ └────────┘ │
-│ │
-│ Trainers │
-│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
-│ │ 👩 │ │ 👨 │ │ 👩 │ │ 👨 │ │
-│ │ Emma │ │ Jake │ │ Mia │ │ Alex │ │
-│ │ 12 wk │ │ 8 wk │ │ 10 wk │ │ 6 wk │ │
-│ └────────┘ └────────┘ └────────┘ └────────┘ │
-│ │
-│ Duration │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ ○ 4 min (Classic Tabata) 20 │ │
-│ │ ○ 8 min (Double Tabata) 15 │ │
-│ │ ○ 12 min (Triple Tabata) 10 │ │
-│ │ ○ 20 min (Tabata Marathon) 5 │ │
-│ └─────────────────────────────────────────────┘ │
-│ │
-│ Focus Area │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ ○ Full Body 20 │ │
-│ │ ○ Upper Body 8 │ │
-│ │ ○ Lower Body 8 │ │
-│ │ ○ Core 8 │ │
-│ │ ○ Cardio 6 │ │
-│ └─────────────────────────────────────────────┘ │
-│ │
-│ Music Vibe │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ ○ Electronic Energy 18 │ │
-│ │ ○ Hip-Hop Beats 12 │ │
-│ │ ○ Rock Power 10 │ │
-│ │ ○ Chill Focus 10 │ │
-│ └─────────────────────────────────────────────┘ │
-│ │
-│ Collections │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ 🌅 Morning Energizer • 5 workouts │ │
-│ └─────────────────────────────────────────────┘ │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ 💪 No Equipment • 15 workouts │ │
-│ └─────────────────────────────────────────────┘ │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ 🔥 7-Day Challenge • 7 workouts │ │
-│ └─────────────────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
----
-
-### 8. Profile Tab
-
-```
-┌─────────────────────────────────────────────────────┐
-│ │
-│ Profile │
-│ │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ 👤 Alex Martin │ │
-│ │ Member since Jan 2026 │ │
-│ │ ✨ Premium │ │
-│ │ │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Weekly Goal │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ 5 workouts per week │ │
-│ │ ████████████████░░░░ 4/5 this week │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Achievements │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ 🏆 7-Day Streak 🥵 First Sweat │ │
-│ │ 💯 100 Workouts 🌅 Early Bird │ │
-│ │ 🔥 500 Calories ⚡ Speed Demon │ │
-│ │ │ │
-│ │ [See All Achievements →] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Settings │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ Notifications [→] │ │
-│ │ Apple Watch [→] │ │
-│ │ Music Preferences [→] │ │
-│ │ Workout Preferences [→] │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ Account │
-│ ┌───────────────────────────────────────────────┐ │
-│ │ Subscription [→] │ │
-│ │ Privacy & Security [→] │ │
-│ │ Help & Support [→] │ │
-│ │ Sign Out │ │
-│ └───────────────────────────────────────────────┘ │
-│ │
-│ TabataFit v1.0.0 │
-│ Made with ❤️ for HIIT lovers │
-│ │
-└─────────────────────────────────────────────────────┘
-```
-
----
-
-## Animation Specifications
-
-### Screen Transitions
-- **Push/Pop**: 300ms ease-out
-- **Modal**: Slide up from bottom, 350ms
-
-### Micro-interactions
-- **Button press**: Scale to 0.96, 100ms
-- **Card tap**: Scale to 0.98, 150ms
-- **Toggle**: 200ms spring animation
-
-### Timer Animations
-- **Countdown**: Number scales up/down each second
-- **Progress bar**: Smooth width animation
-- **Phase change**: Color crossfade 300ms
-
-### Celebration
-- **Confetti**: Lottie animation on workout complete
-- **Rings**: Animated fill when stats update
-- **Streak badge**: Pulse animation
-
----
-
-## Accessibility
-
-- **Dynamic Type**: Support up to 200% text scaling
-- **VoiceOver**: Full screen reader support
-- **Reduce Motion**: Disable animations when requested
-- **High Contrast**: Alternative color scheme option
-
----
-
-*Document created: February 18, 2026*
-*Version: 2.0*
-*Design System: Apple Fitness+ Inspired*
diff --git a/TabataFit_PRD_v2.0.md b/TabataFit_PRD_v2.0.md
deleted file mode 100644
index dcd4c66..0000000
--- a/TabataFit_PRD_v2.0.md
+++ /dev/null
@@ -1,545 +0,0 @@
-# TabataFit — Product Requirements Document v2.0
-> Apple Fitness+ for Tabata — The Premium HIIT Experience
-
----
-
-## Vision Statement
-
-**TabataFit est l'Apple Fitness+ du Tabata.** Une expérience premium, visuellement stunante, guidée par des coachs, qui transforme 4 minutes d'exercice en une expérience de fitness immersive.
-
-*"Workouts that work. Beautifully."*
-
----
-
-## Positionnement
-
-| Aspect | Apple Fitness+ | TabataFit |
-|--------|---------------|-----------|
-| **Focus** | Multi-activité (Yoga, HIIT, Strength, etc.) | Spécialiste Tabata/HIIT |
-| **Durée** | 5-45 min | 4-20 min (format Tabata) |
-| **Différenciateur** | Intégration Apple Watch | Timer intelligent + Coaching audio |
-| **Cible** | Grand public fitness | Athlètes HIIT, busy professionals |
-| **Vibe** | Studio californien | Énergie explosive, motivational |
-
----
-
-## Core Philosophy — Apple Fitness+ Principles
-
-1. **Content is King** — Vidéos HD, coachs charismatiques, production Netflix-quality
-2. **Inclusive** — Tous niveaux, modifications montrées
-3. **Personalized** — Recommandations basées sur l'historique
-4. **Immersive** — Music sync, Burn Bar, stats temps réel
-5. **Beautiful** — Design épuré, animations fluides, dark theme élégant
-
----
-
-## Architecture Produit
-
-### Tab Bar (5 onglets — comme Apple Fitness+)
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ │
-│ 🏠 Home 🔥 Workouts 📊 Activity 🔍 Browse 👤 Profile │
-│ │
-└─────────────────────────────────────────────────────────────┘
-```
-
-### 1. Home Tab — "For You"
-
-**Inspiration**: Apple Fitness+ Home — grande bannière, collections, recommandations
-
-```
-┌─────────────────────────────────────────┐
-│ ☀️ Bonjour Alex │
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ 🎬 FEATURED WORKOUT │ │
-│ │ ─────────────────────── │ │
-│ │ Full Body Burn │ │
-│ │ 4 min • Beginner • Emma │ │
-│ │ │ │
-│ │ [▶️ START NOW] │ │
-│ └───────────────────────────────────┘ │
-│ │
-│ Continue Watching │
-│ ┌─────┐ ┌─────┐ ┌─────┐ │
-│ │ 65%│ │ 30%│ │ 10%│ │
-│ └─────┘ └─────┘ └─────┘ │
-│ │
-│ Popular This Week │
-│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
-│ └─────┘ └─────┘ └─────┘ └─────┘ │
-│ │
-│ Collections │
-│ 🌅 Morning Energizer │
-│ 💪 No Equipment Needed │
-│ 🔥 7-Day Challenge │
-│ │
-└─────────────────────────────────────────┘
-```
-
-**Elements clés:**
-- Hero banner avec vidéo preview en boucle
-- "Continue Watching" — workouts non terminés
-- "Popular This Week" — trending workouts
-- Collections thématiques
-- Coach du moment
-
-### 2. Workouts Tab — Parcourir par type
-
-**Inspiration**: Apple Fitness+ workout browser — categories visuelles
-
-```
-┌─────────────────────────────────────────┐
-│ WORKOUTS 🔍 │
-│ │
-│ ┌─────────────────────────────────┐ │
-│ │ 🔥 QUICK BURN │ │
-│ │ 4 min • All levels │ │
-│ │ 12 workouts │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ ┌─────────────────────────────────┐ │
-│ │ 💪 STRENGTH TABATA │ │
-│ │ 8 min • Intermediate │ │
-│ │ 8 workouts │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ ┌─────────────────────────────────┐ │
-│ │ 🏃 CARDIO BLAST │ │
-│ │ 4-12 min • All levels │ │
-│ │ 15 workouts │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ ┌─────────────────────────────────┐ │
-│ │ 🧘 CORE & FLEXIBILITY │ │
-│ │ 4 min • Beginner friendly │ │
-│ │ 6 workouts │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ ┌─────────────────────────────────┐ │
-│ │ ⚡ HIIT EXTREME │ │
-│ │ 12-20 min • Advanced │ │
-│ │ 10 workouts │ │
-│ └─────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────┘
-```
-
-**Categories:**
-1. **Quick Burn** — 4 min, perfect for beginners
-2. **Strength Tabata** — Resistance exercises
-3. **Cardio Blast** — Pure cardio, no equipment
-4. **Core & Flexibility** — Abs, stretching
-5. **HIIT Extreme** — Advanced, longer sessions
-
-### 3. Activity Tab — Stats & Progress
-
-**Inspiration**: Apple Fitness+ Activity rings + trends
-
-```
-┌─────────────────────────────────────────┐
-│ ACTIVITY │
-│ │
-│ ┌─────────────────────────────────┐ │
-│ │ 🔥 STREAK │ │
-│ │ ───────── │ │
-│ │ 7 │ │
-│ │ DAYS │ │
-│ │ │ │
-│ │ ○ ○ ○ ○ ○ ○ ○ ● ○ ○ │ │
-│ │ M T W T F S S M T │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ This Week │
-│ ┌───────┐ ┌───────┐ ┌───────┐ │
-│ │ 5 │ │ 156 │ │ 32 │ │
-│ │Workout│ │ Calories│ │ Minutes│ │
-│ └───────┘ └───────┘ └───────┘ │
-│ │
-│ Trends │
-│ ┌─────────────────────────────────┐ │
-│ │ 📈 Workouts are trending up! │ │
-│ │ +23% vs last month │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ Burn Bar Position │
-│ ┌─────────────────────────────────┐ │
-│ │ Your avg: 45 cal/workout │ │
-│ │ ████████░░░░ 68th percentile │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ Monthly Summary │
-│ [ Calendar view with heat map ] │
-│ │
-└─────────────────────────────────────────┘
-```
-
-**Features:**
-- Streak counter avec calendrier visuel
-- Stats hebdomadaires (workouts, calories, minutes)
-- Trends ("You're on fire! 🔥")
-- Burn Bar — comparaison avec autres utilisateurs
-- Calendar heat map
-
-### 4. Browse Tab — Tout le contenu
-
-**Inspiration**: Apple Fitness+ Browse — filtres, trainers, music
-
-```
-┌─────────────────────────────────────────┐
-│ BROWSE │
-│ │
-│ Filters [Edit]│
-│ [All ▼] [4 min] [8 min] [Beginner] │
-│ │
-│ By Trainer │
-│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
-│ │ 👩 │ │ 👨 │ │ 👩 │ │ 👨 │ │
-│ │Emma │ │Jake │ │Mia │ │Alex │ │
-│ └─────┘ └─────┘ └─────┘ └─────┘ │
-│ │
-│ By Duration │
-│ ○ 4 min (Classic Tabata) │
-│ ○ 8 min (Double Tabata) │
-│ ○ 12 min (Triple Tabata) │
-│ ○ 20 min (Tabata Marathon) │
-│ │
-│ By Focus Area │
-│ ○ Full Body │
-│ ○ Upper Body │
-│ ○ Lower Body │
-│ ○ Core │
-│ ○ Cardio │
-│ │
-│ Music Vibe │
-│ ○ Electronic Energy │
-│ ○ Hip-Hop Beats │
-│ ○ Rock Power │
-│ ○ Chill Focus │
-│ │
-└─────────────────────────────────────────┘
-```
-
-### 5. Profile Tab — Settings & Account
-
-**Inspiration**: Apple Fitness+ Profile — minimal, clean
-
-```
-┌─────────────────────────────────────────┐
-│ PROFILE │
-│ │
-│ ┌─────────────────────────────────┐ │
-│ │ 👤 Alex Martin │ │
-│ │ Member since Jan 2026 │ │
-│ │ Premium ✨ │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ Goals │
-│ ┌─────────────────────────────────┐ │
-│ │ Weekly Goal: 5 workouts │ │
-│ │ ████████░░ 4/5 this week │ │
-│ └─────────────────────────────────┘ │
-│ │
-│ Achievements │
-│ 🏆 7-Day Streak │
-│ 🥵 First Sweat │
-│ 💯 100 Workouts │
-│ [See all →] │
-│ │
-│ Settings │
-│ • Notifications │
-│ • Apple Watch │
-│ • Music Preferences │
-│ • Account │
-│ • Subscription │
-│ │
-└─────────────────────────────────────────┘
-```
-
----
-
-## The Workout Experience — Cœur du Produit
-
-### Pre-Workout Screen
-
-**Inspiration**: Apple Fitness+ workout preview — trailer, details, start
-
-```
-┌─────────────────────────────────────────┐
-│ ← │
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ │ │
-│ │ [VIDEO PREVIEW LOOP] │ │
-│ │ Coach Emma demonstrating │ │
-│ │ Jump Squats │ │
-│ │ │ │
-│ └───────────────────────────────────┘ │
-│ │
-│ FULL BODY BURN │
-│ ───────────────────────────────────── │
-│ │
-│ 👩 Emma • 💪 Intermediate • ⏱️ 4 min │
-│ │
-│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
-│ │
-│ What You'll Need │
-│ ○ No equipment │
-│ ○ Mat recommended │
-│ │
-│ Exercises Preview │
-│ 1. Jump Squats (20s work) │
-│ 2. Mountain Climbers (20s work) │
-│ 3. Burpees (20s work) │
-│ 4. High Knees (20s work) │
-│ × 2 rounds │
-│ │
-│ Music │
-│ 🎵 Electronic Energy playlist │
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ ▶️ START WORKOUT │ │
-│ └───────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────┘
-```
-
-### Active Workout Screen — Apple Fitness+ Style
-
-**Inspiration**: Apple Fitness+ player — video dominant, stats overlay
-
-```
-┌─────────────────────────────────────────┐
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ │ │
-│ │ │ │
-│ │ [FULL SCREEN VIDEO] │ │
-│ │ │ │
-│ │ Coach doing exercise │ │
-│ │ in perfect form │ │
-│ │ │ │
-│ │ │ │
-│ │ │ │
-│ │ ┌─────────────────────────────┐ │ │
-│ │ │ 🔥 WORK • 00:14 │ │ │
-│ │ │ Round 3 of 8 │ │ │
-│ │ │ ████████████░░░░ 65% │ │ │
-│ │ └─────────────────────────────┘ │ │
-│ │ │ │
-│ │ │ │
-│ └───────────────────────────────────┘ │
-│ │
-│ JUMP SQUATS │
-│ ───────────────────────────────────── │
-│ │
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
-│ │ 14 │ │ 52 │ │ 85% │ │
-│ │ cal │ │ bpm │ │ effort │ │
-│ └─────────┘ └─────────┘ └─────────┘ │
-│ │
-│ Burn Bar: ████████░░░░ 72% │
-│ │
-└─────────────────────────────────────────┘
-```
-
-**Key Features:**
-- Video full screen avec coach
-- Timer overlay (phase + countdown)
-- Round indicator
-- Progress bar
-- Stats temps réel (calories, bpm si Apple Watch)
-- Burn Bar
-
-### During Rest Phase
-
-```
-┌─────────────────────────────────────────┐
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ │ │
-│ │ [COACH IN REST POSE] │ │
-│ │ Stretching / breathing │ │
-│ │ │ │
-│ │ ┌─────────────────────────────┐ │ │
-│ │ │ 💙 REST • 00:08 │ │ │
-│ │ │ Next: Mountain Climbers │ │ │
-│ │ │ ░░░░░░░░░░░░░░░░░░ 40% │ │ │
-│ │ └─────────────────────────────┘ │ │
-│ │ │ │
-│ │ "Shake it out, you're │ │
-│ │ doing great!" │ │
-│ │ │ │
-│ └───────────────────────────────────┘ │
-│ │
-│ Up Next: Mountain Climbers │
-│ [GIF preview of next exercise] │
-│ │
-└─────────────────────────────────────────┘
-```
-
-### Workout Complete — Celebration
-
-**Inspiration**: Apple Fitness+ celebration screen
-
-```
-┌─────────────────────────────────────────┐
-│ │
-│ 🎉 WORKOUT COMPLETE! │
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ │ │
-│ │ [ANIMATED RINGS] │ │
-│ │ 🔥 🔥 🔥 │ │
-│ │ │ │
-│ └───────────────────────────────────┘ │
-│ │
-│ YOUR STATS │
-│ ───────────────────────────────────── │
-│ │
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
-│ │ 52 │ │ 4 │ │ 100% │ │
-│ │ CALORIES│ │ MINUTES │ │ COMPLETE│ │
-│ └─────────┘ └─────────┘ └─────────┘ │
-│ │
-│ Burn Bar │
-│ ████████████░░░ You beat 73%! │
-│ │
-│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
-│ │
-│ 🔥 7 Day Streak! Keep it going! │
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ SHARE YOUR WORKOUT │ │
-│ └───────────────────────────────────┘ │
-│ │
-│ ┌───────────────────────────────────┐ │
-│ │ ← BACK TO HOME │ │
-│ └───────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────┘
-```
-
----
-
-## Content Strategy — 50+ Workouts au Launch
-
-### Par Durée
-
-| Duration | Format | Rounds | Count |
-|----------|--------|--------|-------|
-| 4 min | Classic Tabata | 8 rounds | 20 workouts |
-| 8 min | Double Tabata | 16 rounds | 15 workouts |
-| 12 min | Triple Tabata | 24 rounds | 10 workouts |
-| 20 min | Tabata Marathon | 40 rounds | 5 workouts |
-
-### Par Focus
-
-- **Full Body** — 20 workouts
-- **Upper Body** — 8 workouts
-- **Lower Body** — 8 workouts
-- **Core** — 8 workouts
-- **Cardio Only** — 6 workouts
-
-### Par Niveau
-
-- **Beginner** — 15 workouts
-- **Intermediate** — 20 workouts
-- **Advanced** — 15 workouts
-
-### Trainers (5 au launch)
-
-1. **Emma** — Energy queen, beginner-friendly
-2. **Jake** — Strength focus, motivating
-3. **Mia** — Form perfectionist, technical
-4. **Alex** — Cardio beast, intense
-5. **Sofia** — Chill but effective, recovery
-
----
-
-## Technical Requirements
-
-### Video Pipeline
-- HLS streaming (adaptive bitrate)
-- 1080p minimum, 4K for featured
-- Offline download for Premium
-- Preload next exercise during rest
-
-### Audio
-- Multiple music tracks (by vibe)
-- Coach voice-over (can be muted)
-- Sound effects (beeps, transitions)
-- Haptic feedback sync
-
-### Apple Watch Integration
-- Heart rate display
-- Calories calculation
-- Activity rings update
-- Now Playing controls
-
-### Offline Support
-- Download workouts for offline
-- Sync when back online
-- Local stats caching
-
----
-
-## Monetization
-
-### Free Tier
-- 3 workouts free forever
-- Basic stats
-- Ads between workouts
-
-### Premium ($6.99/mo or $49.99/yr)
-- Unlimited workouts
-- All trainers
-- Offline downloads
-- Advanced stats & trends
-- Apple Watch integration
-- No ads
-- Family Sharing (up to 5)
-
----
-
-## Success Metrics
-
-| Metric | Target (Month 3) |
-|--------|------------------|
-| DAU | 10,000 |
-| Workout completion rate | 75% |
-| 7-day retention | 40% |
-| Premium conversion | 8% |
-| Average workouts/user/week | 3.5 |
-
----
-
-## Roadmap
-
-### Phase 1 — MVP (Weeks 1-4)
-- [ ] Home + Workouts tabs
-- [ ] 20 workouts (4 min only)
-- [ ] 2 trainers
-- [ ] Basic timer + video player
-
-### Phase 2 — Core (Weeks 5-8)
-- [ ] Activity tab with stats
-- [ ] 30 workouts total
-- [ ] 4 trainers
-- [ ] Apple Watch integration
-
-### Phase 3 — Premium (Weeks 9-12)
-- [ ] Browse + Profile tabs
-- [ ] 50+ workouts
-- [ ] 5 trainers
-- [ ] Offline downloads
-- [ ] Burn Bar
-- [ ] Subscription system
-
----
-
-*Document created: February 18, 2026*
-*Version: 2.0*
-*Status: Ready for Design Phase*
diff --git a/TabataKine_Guide_Complet.md b/TabataKine_Guide_Complet.md
deleted file mode 100644
index 750cb9f..0000000
--- a/TabataKine_Guide_Complet.md
+++ /dev/null
@@ -1,1179 +0,0 @@
-# TABATA KINÉ — Guide Complet des Programmes
-
-> **Programmes conçus par une Kinésithérapeute Diplômée d'État**
-> Sécurité · Progression médicale · Accessibilité
-> 6 programmes · 26 semaines · 100+ exercices détaillés
-
----
-
-## Sommaire
-
-1. [Vue d'ensemble](#1-vue-densemble)
-2. [Programme Débutant — 4 semaines](#2-programme-débutant--4-semaines)
-3. [Programme Intermédiaire — 4 semaines](#3-programme-intermédiaire--4-semaines)
-4. [Programme Avancé — 4 semaines](#4-programme-avancé--4-semaines)
-5. [Programme Post-Partum — 6 semaines](#5-programme-post-partum--6-semaines)
-6. [Programme Seniors — 4 semaines](#6-programme-seniors--4-semaines)
-7. [Programme Bureau — 4 semaines](#7-programme-bureau--4-semaines)
-8. [Récapitulatif global](#8-récapitulatif-global)
-
----
-
-## 1. Vue d'ensemble
-
-| Programme | Durée | Séances/sem | Blocs max | Impacts | Prérequis | Tier |
-|---|---|---|---|---|---|---|
-| 🟢 Débutant | 4 semaines | 3 | 3 blocs | ❌ Aucun | Aucun | Gratuit |
-| 🔵 Intermédiaire | 4 semaines | 4 | 4 blocs | ⚡ Léger | Débutant | Premium |
-| 🔴 Avancé | 4 semaines | 5 | 5 blocs | 🔥 Fort | Intermédiaire | Premium |
-| 🩷 Post-partum | 6 semaines | 4 | 3 blocs | ❌ Aucun | Accord médical | Kiné+ |
-| 🟤 Seniors | 4 semaines | 3 | 3 blocs | ❌ Aucun | Accord médecin | Kiné+ |
-| 🟡 Bureau | 4 semaines | 3–5 | 5 blocs | ❌ Aucun | Aucun | Premium |
-
-### La signature kiné — ce qui rend ces programmes uniques
-
-- Chaque exercice inclut un conseil kinésithérapeutique sur la technique, les erreurs à éviter et les contre-indications
-- Les progressions sont médicalement raisonnées : aucun impact avant maîtrise du mouvement, semaine de décharge systématique
-- Les programmes de niche (post-partum, seniors, bureau) adressent des besoins non couverts par les apps concurrentes
-- Les tests de fin de programme sont issus de protocoles cliniques validés
-
----
-
-## 2. Programme Débutant — 4 semaines
-
-**Objectif :** apprendre le protocole tabata, construire les bases techniques de chaque mouvement fondamental, et terminer 12 séances sans douleur articulaire. Le succès n'est pas mesuré en calories brûlées mais en confiance acquise.
-
-**Prérequis :** aucun — accessible à tous, pas de matériel requis.
-**Organisation :** 3 séances/semaine — Lundi · Mercredi · Vendredi recommandé.
-
-### Principes fondateurs
-
-**Règle 1 — Zéro impact les 2 premières semaines.** Pas de sauts, pas de chocs. Les articulations doivent s'adapter progressivement. Cause numéro 1 des abandons par douleur.
-
-**Règle 2 — La technique avant l'intensité.** 20 secondes c'est court mais suffisant pour faire une mauvaise répétition. Mieux vaut 8 squats propres que 15 squats avec le dos arrondi.
-
-**Règle 3 — Semaine 4 = décharge.** Volume réduit de 40%. C'est là que le corps consolide les adaptations. La progression se fait pendant le repos, pas pendant l'effort.
-
----
-
-### SEMAINE 1 — Découverte du rythme
-
-**Format :** 1 bloc tabata par séance (4 min) + échauffement + retour au calme
-**Durée totale :** ~20 minutes
-
----
-
-#### Séance 1A — Membres inférieurs
-
-**Échauffement — 4 min**
-- 1 min — Marche sur place avec genoux hauts
-- 1 min — Cercles de chevilles (30 sec chaque pied)
-- 1 min — Flexions de genoux lentes (3 sec descente / 1 sec montée)
-- 1 min — Fentes statiques alternées lentes
-
-**⏱ Bloc 1 — 4 min | 8 rounds | 20 sec effort / 10 sec repos**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Squat classique** — pieds largeur d'épaules, descente jusqu'aux cuisses parallèles au sol, regard droit, talons à plat | **Pont fessier** — allongé sur le dos, pieds à plat, monter et descendre le bassin lentement, serrer les fessiers en haut |
-| 📋 *Si les talons se soulèvent : écarter davantage les pieds ou placer un support sous les talons. Genoux dans l'axe des pieds, jamais vers l'intérieur.* | 📋 *Ne pas creuser le bas du dos en position haute. Le bassin monte grâce aux fessiers, pas grâce aux lombaires.* |
-
-**Retour au calme — 3 min**
-- 45 sec — Étirement quadriceps debout (chaque jambe)
-- 45 sec — Étirement ischio-jambiers assis (chaque jambe)
-- 30 sec — Respiration diaphragmatique
-
----
-
-#### Séance 1B — Membres supérieurs & gainage
-
-**Échauffement — 4 min**
-- 1 min — Rotations d'épaules avant et arrière
-- 1 min — Ouvertures de poitrine (mains croisées dans le dos)
-- 1 min — Cercles de poignets dans les deux sens
-- 1 min — Cat-cow : mobilité lombaire à quatre pattes
-
-**⏱ Bloc 1 — 4 min | 8 rounds | 20 sec effort / 10 sec repos**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompes genoux** — corps aligné des genoux aux épaules, descendre la poitrine à 2–3 cm du sol, coudes à 45° | **Planche basse sur avant-bras** — corps aligné, coudes sous les épaules, ne pas laisser tomber les hanches ni les remonter |
-| 📋 *Si douleur aux poignets : faire sur les poings fermés ou les avant-bras. Vérifier l'alignement poignet / coude / épaule.* | 📋 *Respirer normalement. La planche doit être une contraction active — réduire la durée si tremblements excessifs.* |
-
-**Retour au calme — 3 min**
-- 45 sec — Étirement pectoraux contre un mur (chaque côté)
-- 30 sec — Étirement triceps derrière la tête (chaque bras)
-- 30 sec — Étirement cervical latéral doux (chaque côté)
-
----
-
-#### Séance 1C — Corps entier
-
-**Échauffement — 4 min**
-- 1 min — Jumping jacks lents sans sauter (décaler les pieds)
-- 1 min — Rotations de hanches debout
-- 1 min — Marche avec bras croisés devant
-- 1 min — Squats ¼ de descente (activation légère)
-
-**⏱ Bloc 1 — 4 min | 8 rounds | 20 sec effort / 10 sec repos**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Step touch latéral** — pas latéraux rapides droite/gauche avec bras actifs, rythme soutenu | **Superman** — allongé ventre au sol, lever simultanément bras et jambes 2 secondes, relâcher complètement |
-| 📋 *Garder le regard droit. Les bras actifs contribuent à 20% de l'effort cardiovasculaire.* | 📋 *Ne pas forcer sur le cou — il reste dans l'axe. Exercice roi pour les lombaires et les paravertébraux.* |
-
-**Retour au calme — 3 min**
-- 1 min — Posture de l'enfant (balasana)
-- 45 sec — Étirement des hanches en pigeon (chaque côté)
-
----
-
-### SEMAINE 2 — Consolidation
-
-**Format :** 2 blocs tabata + 1 min récupération active entre les blocs
-**Durée totale :** ~25 minutes
-
-#### Nouveaux exercices introduits en semaine 2
-
-**Fente avant alternée** — un pas en avant, genou arrière descend vers le sol sans toucher.
-📋 *Le genou avant ne dépasse pas les orteils. Si douleur rotulienne : réduire l'amplitude.*
-
-**Dead bug** — allongé sur le dos, bras vers le plafond, jambes à 90°. Descendre un bras et la jambe opposée sans que le bas du dos se décolle. Expirer en descendant.
-📋 *Gainage profond transverse — excellent pour les lombaires. La version un seul membre reste disponible si besoin.*
-
-**Bird dog** — à quatre pattes, tendre simultanément le bras droit et la jambe gauche. Maintenir 2 secondes, bassin horizontal. Alterner.
-📋 *Placer une bouteille sur le dos pour vérifier que le bassin reste horizontal.*
-
----
-
-#### Séance 2A — Membres inférieurs renforcés
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Squat classique (consolidation)** — augmenter la profondeur, viser cuisses parallèles au sol, tempo 2-1-2 | **Pont fessier (consolidation)** — ajouter 1 sec de maintien en haut, soulever les orteils pour intensifier |
-
-⏸ *1 min récupération active — marche lente, respiration nasale*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Fente avant alternée** — vérifier que le genou avant ne dépasse pas les orteils, tronc droit, regard devant | **Dead bug** — dos bien à plat au sol, expirer en descendant le membre, réduire l'amplitude si le dos se décolle |
-
-**Retour au calme — 4 min**
-- 1 min — Étirement du psoas en fente basse (chaque côté)
-- 45 sec — Étirement des ischio-jambiers allongé (chaque jambe)
-- 30 sec — Respiration guidée
-
----
-
-#### Séance 2B — Haut du corps renforcé
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompes genoux (consolidation)** — essayer 2–3 pompes complètes sur orteils si possible | **Planche avant-bras (consolidation)** — tenter la planche sur orteils 10 sec puis repasser sur genoux si besoin |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Bird dog** — maintenir le bassin parfaitement horizontal | **Superman dynamique** — version dynamique : lever et descendre en rythme avec la respiration |
-
----
-
-#### Séance 2C — Corps entier mixte
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Step touch + bras (consolidation)** — augmenter la vitesse, bras au-dessus des épaules | **Superman (consolidation)** — maintenir la position haute 3 secondes au lieu de 2 |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Fente avant alternée + rotation de buste** — en fente basse, rotation du tronc vers le genou avant | **Dead bug lent** — 5 secondes par membre, qualité absolue |
-
----
-
-### SEMAINE 3 — Montée en intensité
-
-**Format :** 3 blocs tabata + 1 min récupération entre chaque
-**Durée totale :** ~30 minutes
-
-#### Nouveaux exercices introduits en semaine 3
-
-**Squat jump low** — même mouvement que le squat mais à la montée on monte sur la pointe des pieds sans quitter le sol. Préparation au squat sauté.
-📋 *Réception silencieuse sur avant-pied. Si bruit à l'atterrissage : réduire la hauteur.*
-
-**Step-up sur marche basse** — monter/descendre sur une marche basse, alterner les jambes. Équivalent cardio des fentes sautées sans l'impact.
-📋 *Genou de la jambe de montée au-dessus du pied de la marche.*
-
-**Mountain climber lent** — en position de pompes, ramener un genou vers la poitrine en 1 sec aller / 1 sec retour.
-📋 *Gainage parfait pendant tout le mouvement. Ne pas laisser les hanches monter.*
-
----
-
-#### Séances 3A/B/C — Structure type (3 blocs)
-
-**⏱ Bloc 1** — exercice maîtrisé des semaines précédentes
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Squat classique** — focus sur la respiration : inspirer en descendant, expirer en remontant | **Pont fessier unilatéral** — jambe non-travaillante tendue en l'air |
-| | 📋 *Le bassin ne doit pas s'incliner du côté de la jambe levée — signe de faiblesse du moyen fessier.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 2** — nouveau mouvement
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Squat jump low** — réception silencieuse sur avant-pied | **Fente avant alternée (maîtrisée)** — ¼ de descente supplémentaire |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 3** — mixte
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Step-up sur marche basse** — alterner les jambes à mi-bloc | **Mountain climber lent** — gainage parfait, hanches basses |
-
----
-
-### SEMAINE 4 — Décharge & consolidation
-
-**Format :** retour à 2 blocs tabata. Volume réduit de 40%.
-**Durée totale :** ~25 minutes
-
-> 💡 **Pourquoi la décharge ?** C'est pendant le repos que le corps consolide les adaptations musculaires et articulaires. Les sportifs qui sautent la décharge progressent moins vite à long terme.
-
-Pas de nouveaux exercices. Focus sur :
-- Technique parfaite à chaque répétition
-- Respiration consciente et coordonnée avec le mouvement
-- Ressenti musculaire : identifier les muscles sollicités
-- Bilan personnel : quels exercices sont devenus faciles ? Lesquels restent difficiles ?
-
----
-
-### Récapitulatif Programme Débutant
-
-| Semaine | Blocs/séance | Min effectives | Séances/sem | Impacts | Durée totale |
-|---|---|---|---|---|---|
-| Semaine 1 | 1 bloc | 4 min | 3 | ❌ Aucun | ~20 min |
-| Semaine 2 | 2 blocs | 8 min | 3 | ❌ Aucun | ~25 min |
-| Semaine 3 | 3 blocs | 12 min | 3 | ⚡ Très léger | ~30 min |
-| Semaine 4 | 2 blocs | 8 min | 3 | ❌ Aucun | ~25 min |
-
-**Critères de passage au programme Intermédiaire :**
-- Planche sur avant-bras tenue 30 secondes sans compensation
-- 10 squats propres consécutifs sans douleur aux genoux
-- 5 pompes complètes (sur orteils) avec corps parfaitement aligné
-- Aucune douleur articulaire résiduelle après les séances
-
----
-
-## 3. Programme Intermédiaire — 4 semaines
-
-**Objectif :** introduire la plyométrie contrôlée, intensifier le travail unilatéral, passer à 4 blocs tabata. L'utilisateur construit puissance et conscience corporelle, apprend à distinguer brûlure musculaire (normale) et douleur articulaire (signal d'arrêt).
-
-**Organisation :** 4 séances/semaine — Lundi · Mardi · Jeudi · Samedi. 1 séance mobilité/récupération active. Ne jamais faire 2 séances intenses consécutives.
-
-**Règle de réception des impacts :** silencieuse, sur avant-pied, genou fléchi. Un bruit à l'atterrissage = muscles ne font pas leur travail d'amortisseur.
-
----
-
-### SEMAINE 1 — Transition avec impacts
-
-**Format :** 3 blocs tabata + 1 min récupération active entre chaque
-**Durée totale :** ~35 minutes
-
----
-
-#### Séance 2A — Membres inférieurs plyométriques
-
-**Échauffement — 5 min**
-- 1 min — Marche rapide avec bras actifs
-- 1 min — Squats lents x10 (activation)
-- 1 min — Fentes alternées lentes x8
-- 30 sec — Sauts très légers sur place (test des chevilles)
-- 30 sec — Montées de genoux en trottinant
-- 1 min — Hip circles : cercles de hanches debout
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Squat jump** — descente contrôlée, explosion vers le haut, réception silencieuse avant-pied, absorption immédiate en flexion | **Pont fessier unilatéral** — une jambe tendue levée, montée du bassin, maintien 2 sec en haut |
-| 📋 *Si la réception fait du bruit : les muscles ne font pas leur travail. Réduire la hauteur. Genoux dans l'axe des pieds.* | 📋 *Le bassin ne s'incline pas du côté de la jambe levée. Si c'est le cas : faiblesse du moyen fessier.* |
-
-⏸ *1 min récupération active — marche lente, respiration nasale*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Fente sautée alternée** — depuis la fente basse, saut pour changer de jambe, réception directement en fente | **Isométrie squat (chaise)** — dos au mur, cuisses parallèles au sol, tenir 20 secondes, respirer calmement |
-| 📋 *Si douleur antérieure du genou : revenir à la fente marchée. Réception absorbée sur 2–3 secondes.* | 📋 *L'isométrie renforce l'endurance musculaire sans impact. Excellent pour skieurs et cyclistes.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 3**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Step-up explosif** — monter rapidement en poussant sur le pied avant, descendre contrôlé, alterner à mi-bloc | **Glute kickback à quatre pattes** — genou fléchi à 90°, pousser le talon vers le plafond, lent et contrôlé |
-| 📋 *La descente est aussi importante que la montée. Ne pas sauter en bas.* | 📋 *Ne pas cambrer le bas du dos pour aller plus haut — c'est la hanche qui travaille, pas les lombaires.* |
-
-**Retour au calme — 5 min**
-- 1 min — Étirement psoas en fente basse (chaque côté)
-- 45 sec — Étirement mollets contre un mur (chaque jambe)
-- 1 min — Automassage quadriceps
-- 30 sec — Respiration guidée
-
----
-
-#### Séance 2B — Haut du corps & gainage dynamique
-
-**Échauffement — 5 min**
-- 1 min — Rotations complètes d'épaules avec serviette
-- 1 min — Face pulls avec résistance manuelle (rétraction scapulaire)
-- 1 min — Pompes lentes x8 (échauffement scapulaires)
-- 1 min — Planche dynamique haute ↔ basse lentement
-- 1 min — Dead bug lent (consolidation)
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompes classiques complètes** — corps aligné, coudes à 45°, descendre jusqu'au contact de la poitrine | **Renegade row sans poids** — en position de pompes, lever alternativement un bras vers la hanche en contractant l'omoplate |
-| 📋 *Douleur aux poignets = vérifier l'alignement poignet/coude/épaule. Si persistant : pompes sur les poings.* | 📋 *Gainage total pendant le mouvement. Le bassin ne se balance pas.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompes avec rotation en T** — en haut de la pompe, rotation latérale avec bras vers le plafond, alterner | **Planche avec tap épaule** — en planche haute, toucher alternativement l'épaule opposée, minimiser la rotation du bassin |
-| 📋 *Exercice combiné : obliques + dentelés antérieurs + rotateurs d'épaule. Ouvrir complètement la hanche.* | 📋 *La résistance à la rotation du bassin est l'objectif. Pieds plus écartés pour stabiliser.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 3**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Dips sur chaise** — mains sur le bord, corps décollé, descendre les coudes à 90° | **Superman en Y·W·T** — allongé ventre, lever bras et jambes puis former successivement Y, W, T avec les bras |
-| 📋 *Si inconfort à l'avant de l'épaule : réduire l'amplitude. Contre-indiqué si tendinopathie du biceps.* | 📋 *Travail intense des rhomboïdes et trapèzes inférieurs. Muscles posturaux clés contre la position assise.* |
-
-**Retour au calme — 5 min**
-- 1 min — Étirement pectoraux en porte (chaque côté)
-- 45 sec — Étirement grand dorsal avec inclinaison latérale (chaque côté)
-- 1 min — Mobilisation cervicale douce
-
----
-
-#### Séance 2C — Corps entier cardio dominant
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Burpee modifié** — marcher les pieds en arrière jusqu'à planche, marcher retour, se relever (pas de saut) | **Mountain climber rapide** — alterner les jambes le plus vite possible en maintenant le gainage |
-| 📋 *Le burpee modifié permet de maîtriser le schéma moteur sans impact lombaire brutal. Le burpee complet vient en semaine 3.* | 📋 *Les hanches ne remontent pas au-dessus des épaules. Regard vers le sol.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Skaters** — saut latéral d'un pied sur l'autre en simulant le patineur, bras opposé en avant | **Planche to downward dog** — depuis planche avant-bras, pousser en planche haute puis lever les hanches en V inversé |
-| 📋 *Atterrissage sur un pied — demande une bonne proprioception de cheville. En cas d'entorse récente : éviter.* | 📋 *Tirer les talons vers le sol en V inversé pour étirer les mollets.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 3**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **High knees** — course sur place avec genoux hauts, bras actifs, regard droit, maintenir le rythme | **Bear crawl sur place** — à quatre pattes, genoux à 3 cm du sol, reculer/avancer sur 2 pas rapidement |
-| 📋 *Le but est le rythme, pas la hauteur maximale. Les bras pompants contribuent à 15% de la dépense calorique.* | 📋 *Très intense pour le gainage et les épaules malgré l'apparente simplicité. Genoux à 3 cm du sol.* |
-
-**Retour au calme — 5 min**
-- 2 min — Foam roller ou automassage mollets et IT band
-- 1 min — Pigeon yoga (chaque côté)
-- 1 min — Respiration 4-7-8 (inspirer 4 sec, bloquer 7, expirer 8) × 4 cycles
-
----
-
-#### Séance 2D — Mobilité & récupération active
-
-> ℹ️ **Format spécial — Pas de blocs 20/10.** Cette séance ne suit pas le protocole tabata. 25–30 min de travail doux mais structuré. Elle est aussi importante que les trois autres — c'est pendant la récupération que le corps reconstruit les fibres musculaires.
-
-- 10 min — Mobilité articulaire complète : chevilles, hanches, colonne, épaules
-- 10 min — Étirements ciblés sur les zones sollicitées en semaine
-- 5 min — Respiration et relaxation guidée
-
----
-
-### SEMAINE 2 — Montée en densité
-
-**Format :** 4 blocs tabata + 1 min récup entre chaque
-**Durée totale :** ~40 minutes
-
-#### Nouveaux exercices introduits en semaine 2
-
-**Thruster** — squat puis poussée des bras vers le haut. Expirer lors de la poussée. Exercice total-body par excellence.
-📋 *La montée en pression intra-abdominale est importante — expirer lors de la poussée vers le haut.*
-
-**Burpee complet** — depuis debout : squat, mains au sol, saut des pieds en arrière, pompe optionnelle, saut retour, saut vertical avec clap. Point critique : fléchir les genoux en posant les mains.
-📋 *Ne jamais arrondir violemment le dos sous fatigue.*
-
-**Hollow body** — allongé, bras tendus derrière la tête, jambes tendues à 30° du sol, bas du dos collé au sol. Maintenir 20 secondes.
-📋 *Gainage profond issu de la gymnastique artistique — le plus efficace pour les abdominaux profonds.*
-
-**V-sit hold** — assis, jambes levées à 45° et bras parallèles au sol. Tenir.
-📋 *Si douleur lombaire : fléchir légèrement les genoux. Ne jamais forcer si le bas du dos compense.*
-
----
-
-### SEMAINE 3 — Intensité maximale
-
-**Format :** 4 blocs tabata denses
-**Durée totale :** ~40 minutes
-
-#### Exercices signature de la semaine 3
-
-**Burpee avec saut latéral** — ajouter un saut latéral à la fin du burpee classique. Augmente l'impact cardiovasculaire et la coordination.
-
-**Pistol squat assisté** — squat sur une jambe en tenant un support léger.
-📋 *Révélateur des asymétries de force et de mobilité. Si un côté est significativement plus difficile : information précieuse sur les déséquilibres à corriger.*
-
-**Archer push-up** — pompes larges en ramenant tout le poids sur un seul bras, l'autre restant tendu à plat. Quasi-pompe à un bras.
-
-**Turkish get-up simplifié** — depuis allongé, se lever en séquence contrôlée : coude → main → genou → debout, puis redescendre.
-📋 *Exercice de rééducation fonctionnelle utilisé en cabinet kiné. Exceptionnel pour la coordination et la proprioception.*
-
----
-
-### SEMAINE 4 — Décharge & bilan
-
-**Format :** retour à 3 blocs tabata, exercices maîtrisés, focus sur la technique parfaite.
-
-#### Tests de fin de programme Intermédiaire
-
-| Test | Protocole | Objectif |
-|---|---|---|
-| Planche avant-bras | Tenir en planche basse sans compensation | 60 secondes |
-| Burpee endurance | Compter les burpees complets en 1 minute | 12 à 15 répétitions |
-| Squat jump | 8 rounds de 20 sec de squat jump | Maintenir le rythme au round 7-8 |
-| Asymétrie | Pistol squat assisté G/D | Différence < 20% entre les côtés |
-
----
-
-### Récapitulatif Programme Intermédiaire
-
-| Semaine | Blocs/séance | Min effectives | Séances/sem | Impacts | Durée totale |
-|---|---|---|---|---|---|
-| Semaine 1 | 3 blocs | 12 min | 4 | ⚡ Moyens | ~35 min |
-| Semaine 2 | 4 blocs | 16 min | 4 | 🔥 Intenses | ~40 min |
-| Semaine 3 | 4 blocs | 16 min | 4 | 🔥🔥 Max | ~40 min |
-| Semaine 4 | 3 blocs | 12 min | 4 | ⚡ Moyens | ~35 min |
-
----
-
-## 4. Programme Avancé — 4 semaines
-
-**Objectif :** mouvements complexes sous fatigue, travail unilatéral systématique, préparation physique fonctionnelle de haut niveau. L'utilisateur développe une intelligence physique : savoir doser l'intensité sur 20 minutes de travail effectif.
-
-**Prérequis :**
-- 15 burpees/min sans s'effondrer
-- Planche avant-bras 60 secondes
-- Squat jump ×8 rounds propres
-- Pistol squat assisté G/D symétrique
-- Aucune douleur articulaire chronique
-
-**Organisation :** 5 séances/semaine — 4 tabata + 1 MetCon. Repos : mercredi + dimanche.
-
-> ⚠️ La fatigue neurologique se manifeste par une perte de coordination, des réactions ralenties, une irritabilité. Si ces signes apparaissent : réduire d'une séance par semaine avant de forcer.
-
----
-
-### SEMAINE 1 — Bascule vers la complexité
-
-**Format :** 4 blocs tabata + 1 min 30 récupération active entre chaque
-**Durée totale :** ~45 minutes
-
----
-
-#### Séance 3A — Membres inférieurs explosifs
-
-**Échauffement — 6 min**
-- 1 min — Jogging sur place progressif
-- 1 min — Leg swings dynamiques avant/arrière et latéraux
-- 1 min — Fentes dynamiques avec rotation du tronc
-- 1 min — Squat profond maintenu 3 sec en bas (ouverture des hanches)
-- 1 min — Sauts bas et rapides à deux pieds (activation système nerveux)
-- 30 sec — Clamshells rapides debout (activation fessiers)
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Squat jump avec rotation 180°** — squat, explosion, rotation 180° dans les airs, réception en squat absorbée | **Pistol squat complet** — squat sur une jambe sans support, jambe libre tendue devant, descente complète |
-| 📋 *La réception en rotation sollicite fortement le LCA. Antécédent de genou = rester au squat jump classique. Réception sur 2–3 secondes, jamais jambe tendue.* | 📋 *Talon qui se soulève = manque de mobilité de cheville. Tronc qui bascule = faiblesse du moyen fessier. Corriger avant de forcer.* |
-
-⏸ *1 min 30 récupération active — marche avec respiration nasale*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Nordic curl assisté** — à genoux, pieds bloqués, descendre le buste vers le sol lentement en résistant avec les ischio-jambiers, poser les mains avant d'atterrir | **Isométrie fente bulgare** — pied arrière posé sur une chaise, descendre en fente, maintenir 20 secondes |
-| 📋 *Cliniquement prouvé pour réduire les blessures ischio-jambiers de 50%. Même 3–4 reps contrôlées par round sont excellentes.* | 📋 *Sollicitation intense du psoas-iliaque et du quadriceps. Excellent pour les coureurs et personnes sédentaires.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 3**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Box jump** — saut à deux pieds sur surface surélevée stable, réception en squat, descente en marchant | **Lateral bound** — saut explosif latéral d'un pied sur l'autre le plus loin possible, réception sur un pied avec absorption |
-| 📋 *Descendre en marchant — ne pas sauter en arrière. Sauter en arrière pour descendre multiplie les contraintes articulaires.* | 📋 *Exercice de prévention des entorses par renforcement proprioceptif. Regard fixe droit devant.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 4**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Broad jump + recul** — saut vers l'avant le plus loin possible à deux pieds, reculer en marche contrôlée, relancer immédiatement | **Single leg deadlift sans poids** — debout sur une jambe, basculer le buste en levant la jambe libre derrière, dos plat, descendre vers le sol |
-| 📋 *Le recul contrôlé est aussi important que le saut. Il renforce les stabilisateurs de cheville sous fatigue.* | 📋 *Exercice fondamental de proprioception. Si le dos s'arrondit avant de toucher le sol : réduire l'amplitude. Qualité > profondeur.* |
-
-**Retour au calme — 6 min**
-- 2 min — Douche froide sur les jambes si possible
-- 1 min 30 — Étirement ischio-jambiers dynamique puis statique (chaque jambe)
-- 1 min — Massage des mollets et du pied
-- 30 sec — Respiration guidée récupération
-
----
-
-#### Séance 3B — Haut du corps athlétique
-
-**Échauffement — 6 min**
-- 1 min — Cercles d'épaules complets avec résistance
-- 1 min — Face pulls avec élastique (rétraction scapulaire)
-- 1 min — Pompes lentes x6 (descente 4 secondes)
-- 1 min — Dips lents x6 sur chaise
-- 1 min — Planche dynamique haute ↔ basse x6
-- 1 min — Rotation thoracique en position de fente basse
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompes à un bras assistées** — une main au sol, l'autre sur support surélevé, alterner les côtés à mi-bloc | **Pike push-up** — en V inversé (hanches hautes), fléchir les coudes pour descendre la tête vers le sol |
-| 📋 *L'asymétrie révèle les faiblesses de stabilisation scapulaire. L'épaule du bras porteur ne s'affaisse pas.* | 📋 *Simule le développé épaules. Contre-indiqué si syndrome d'accrochage sous-acromial. Amplitude réduite au départ.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompe plyométrique** — pompe explosive avec les mains qui décollent du sol, réception souple, enchaîner immédiatement | **Around the world planche** — en planche haute, déplacer les mains pour faire un tour complet imaginaire |
-| 📋 *Poignets en parfait alignement — échauffement spécifique des poignets non négociable. En cas de douleur : version sur genoux.* | 📋 *Ultra-exigeant pour les rotateurs de l'épaule et les dentelés antérieurs.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 3**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Archer push-up complet** — pompes larges en ramenant tout le poids sur un bras, l'autre tendu, alterner | **Planche latérale avec rotation** — en planche latérale, amener le bras libre sous le corps en rotation, revenir en ouverture vers le plafond |
-| 📋 *Maintenir l'alignement corps-bras porteur parfait.* | 📋 *Sollicite simultanément obliques, carré des lombes et rotateurs de l'épaule.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 4**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pseudo planche push-up** — mains plus basses vers les hanches, corps incliné avant, pompe avec bras le long du corps | **Superman dynamique battement** — allongé ventre, lever et descendre rapidement bras et jambes en battements contrôlés |
-| 📋 *Charge extrême sur les triceps et pectoraux inférieurs. Commencer par 3–4 répétitions propres.* | 📋 *Travail des extenseurs dorsaux sous fatigue. Maintenir le cou dans l'axe.* |
-
-**Retour au calme — 6 min**
-- 1 min — Étirement rotateurs externes d'épaule contre mur (chaque côté)
-- 2 min — Mobilisation thoracique sur rouleau
-- 1 min — Étirement grand pectoral profond (chaque côté)
-
----
-
-#### Séance 3C — Corps entier haute intensité
-
-**Échauffement — 6 min**
-Activation neurologique progressive : jumping jacks → high knees → skaters → burpee modifié → 2 burpees complets → mobilisation chevilles/poignets → 10 hollow body holds 5 secondes
-
-**⏱ Bloc 1**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Burpee avec saut groupé** — burpee complet, à la montée ramener les genoux vers la poitrine | **V-up** — allongé, lever simultanément jambes tendues et buste, toucher les orteils au sommet |
-| 📋 *Le saut groupé augmente la charge lombaire à la réception. Atterrir genoux fléchis, jamais en extension.* | 📋 *Si douleur au bas du dos : revenir au hollow body ou dead bug. Contre-indiqué si psoas irrité.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 2**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Tuck jump** — sauts avec genoux ramenés au maximum vers la poitrine, rythme rapide, atterrissage silencieux | **Mountain climber croisé** — en planche haute, ramener le genou droit vers le coude gauche et vice versa |
-| 📋 *Si syndrome de l'essuie-glace (IT band) : éviter et substituer par jumping squats.* | 📋 *Plus difficile que le MC classique car la rotation engage les obliques. Hanches basses.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 3**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Devil's press poids de corps** — burpee, en planche ramener les deux genoux simultanément (grenouille), se relever, sauter | **Bear crawl en déplacement** — à quatre pattes genoux décollés, avancer 4 pas et reculer 4 pas rapidement |
-| 📋 *Enchaînement fluide entre les phases. Pas de pause entre le grenouille et le relever.* | 📋 *Gainage parfait malgré la vitesse. Genoux à 3 cm du sol.* |
-
-⏸ *1 min 30 récupération active*
-
-**⏱ Bloc 4**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Sprawl** — depuis debout, jeter les mains au sol, projeter les hanches vers le bas (ventre au sol), remonter en explosif | **Hollow body rocks** — depuis hollow body, se balancer d'avant en arrière comme un berceau sans perdre la position |
-| 📋 *Exercice issu du MMA. La projection vers le bas doit être contrôlée — ne jamais s'écraser.* | 📋 *La continuité du gainage pendant le balancement est l'objectif. Si la position se casse : reprendre le hollow body statique.* |
-
-**Retour au calme — 6 min**
-- 2 min — Foam roller colonne vertébrale
-- 1 min 30 — Supine twist (torsion au sol) chaque côté
-- 2 min — Savasana avec respiration guidée
-
----
-
-#### Séance 3D — Gainage profond & mobilité avancée
-
-> ℹ️ **Format spécial — Pas de blocs 20/10.** 35 minutes de travail structuré de qualité. La séance qui sépare les sportifs qui durent de ceux qui se blessent.
-
-**Gainage profond — 15 min**
-- Hollow body progressif : 5×10 sec → 5×15 sec → 3×20 sec
-- Dead bug avec résistance imaginaire : 3×10 répétitions très lentes
-- Pallof press anti-rotation debout : 3×10 chaque côté
-- Copenhagen plank (pied sur chaise) : 3×20 sec chaque côté
-📋 *Le Copenhagen plank est l'exercice de prévention des adducteurs le plus efficace existant. Très peu connu du grand public.*
-
-**Mobilité active — 15 min**
-- 90/90 stretching (mobilité profonde de hanche) : 2 min chaque côté
-- Squat profond asiatique maintenu : 2 min progressifs
-- Brettzel (rotation thoracique + fléchisseur de hanche) : 2 min chaque côté
-- Mobilisation du thorax en extension sur rouleau : 2 min
-
-**Récupération — 5 min**
-- Nidra yoga : scan corporel allongé, relâchement segment par segment
-
----
-
-#### Séance 3E — MetCon 20 minutes (AMRAP)
-
-> 📚 **MetCon — Metabolic Conditioning** : réaliser autant de tours que possible en 20 minutes. Teste l'endurance de force sur la durée. Le nombre de tours complets est le KPI de progression.
-
-Circuit à répéter en continu pendant 20 minutes :
-- 5 — Burpees complets
-- 10 — Squats jump
-- 10 — Mountain climbers croisés (5 chaque côté)
-- 5 — Pompes explosives
-- 10 — V-ups
-
-> ⚠️ Ne jamais sacrifier la technique sur les burpees et les pompes sous fatigue — c'est là que les blessures d'épaule surviennent. Si la forme s'effondre : marcher 15 secondes et reprendre.
-
----
-
-### SEMAINE 2 — Densification
-
-**Format :** 5 blocs tabata + 1 min récup
-**Durée totale :** ~50 minutes
-
-#### Exercices introduits en semaine 2
-
-**Handstand push-up contre le mur** — en équilibre dos au mur, fléchir les coudes pour descendre la tête. Commencer par amplitude réduite de 5 cm.
-📋 *Vérifier l'absence de douleur cervicale. Ne jamais toucher le sol avec force.*
-
-**Single leg burpee** — burpee complet sur une seule jambe, l'autre reste décollée du sol pendant toute la séquence.
-📋 *Test ultime de force, coordination et proprioception. L'asymétrie G/D est visible immédiatement.*
-
-**Dragon flag partiel** — allongé, mains sous les épaules, lever le corps en planche depuis les épaules. Version partielle = fléchir les genoux.
-📋 *Ne jamais forcer si douleur lombaire. L'exercice de Bruce Lee.*
-
----
-
-### SEMAINE 3 — Pic d'intensité & Complexes
-
-**Format :** 5 blocs tabata + MetCon étendu à 25 minutes
-
-> 📚 **Qu'est-ce qu'un complexe ?** Deux exercices différents enchaînés sans pause, comptés comme une seule répétition. Outil favori des préparateurs physiques de haut niveau : combine force, puissance et endurance. La fatigue s'accumule très vite.
-
-#### Complexes de la semaine 3
-
-**🔥 Complexe 1 — Lower body power**
-Squat jump + fente sautée : squat jump, atterrissage en fente droite, saut retour en squat, recommencer.
-
-**🔥 Complexe 2 — Push + core**
-Pompe explosive + mountain climber ×2 : pompe avec mains décollées, atterrissage, 2 MC croisés rapides, recommencer.
-
-**🔥 Complexe 3 — Total body**
-Burpee + broad jump : burpee complet, dans l'élan du saut se propulser vers l'avant le plus loin possible, reculer en marchant, recommencer.
-
-> ⚠️ Si la technique s'effondre au round 5 : réduire le complexe à un seul exercice pour finir le bloc proprement. La qualité prime toujours.
-
----
-
-### SEMAINE 4 — Décharge & Tests finaux
-
-**Format :** 3 blocs tabata. Exercices connus, focus sur la technique parfaite. Tests de performance complets.
-
-#### Tests finaux du Programme Avancé
-
-| Test | Protocole | Objectif avancé | Signal d'alerte |
-|---|---|---|---|
-| Endurance de force | Burpees en 3 minutes | 35+ répétitions | < 25 = revoir sem 3 |
-| Force relative | Pistol squat complet G et D | 5 reps propres chaque côté | Écart > 20% = déséquilibre |
-| Gainage dynamique | Planche + MC croisé 2 minutes | Maintien sans perte de position | Bascule hanche = fatigue |
-| Puissance | Broad jump mesuré | Progression vs test intermédiaire | +10 cm minimum |
-| Coordination | Single leg DL × 8 chaque côté | Sans poser le pied ni tomber | Chronométrer les yeux fermés |
-| Asymétrie | Tous tests G vs D | Écart < 20% partout | Si > 20% : programme correctif |
-
----
-
-### Récapitulatif Programme Avancé
-
-| Semaine | Blocs/séance | Min effectives | Séances/sem | Type exercices | Durée totale |
-|---|---|---|---|---|---|
-| Semaine 1 | 4 blocs | 16 min | 5 | Unilatéral + plyos | ~45 min |
-| Semaine 2 | 5 blocs | 20 min | 5 | Handstand + complexes légers | ~50 min |
-| Semaine 3 | 5 blocs + complexes | 20 min+ | 5 | Complexes complets | ~50 min |
-| Semaine 4 | 3 blocs | 12 min | 5 | Bilan + tests | ~35 min |
-
----
-
-## 5. Programme Post-Partum — 6 semaines
-
-> ⚠️ **AVERTISSEMENT MÉDICAL OBLIGATOIRE**
-> Ce programme est conçu pour les femmes ayant accouché il y a minimum **8 semaines (voie basse)** ou **12 semaines (césarienne)**, avec accord de leur médecin ou sage-femme lors de la visite post-natale. En cas de douleur pelvienne, fuites urinaires, sensation de pesanteur ou cicatrice douloureuse : consulter un kinésithérapeute périnéal avant de commencer.
-
-**Objectif :** reconnecter le cerveau au périnée et aux abdominaux profonds, rééduquer la sangle abdominale, corriger les déséquilibres posturaux de la grossesse, retrouver progressivement le mouvement en toute sécurité.
-
-### Contexte kiné — Ce que le corps a vécu
-
-**Le périnée** a subi une distension importante, parfois une déchirure ou épisiotomie. Doit être réhabilité avant tout travail de gainage ou d'impact. La reprise trop précoce des abdominaux classiques est l'erreur n°1.
-
-**La sangle abdominale** présente souvent un diastasis des droits — écartement de la ligne blanche. Faire des crunchs avec un diastasis non résolu aggrave la situation. Test requis avant phase 2.
-
-**Les ligaments** restent laxes plusieurs mois après l'accouchement (relaxine encore présente). Les articulations sont plus vulnérables. Aucun impact avant récupération ligamentaire.
-
-**La posture** a été modifiée par 9 mois de grossesse : hyperlordose lombaire, épaules enroulées, bassin antéversé. Le programme corrige ces déséquilibres, ne les aggrave pas.
-
----
-
-### PHASE 1 — Semaines 1-2 : Réveil du corps
-
-**Objectif :** reconnecter le cerveau au périnée et aux abdominaux profonds.
-
-> **Format spécial — Protocole INVERSÉ : 10 sec effort / 20 sec repos.**
-> Pourquoi ? Les muscles profonds (transverse, plancher pelvien) ont besoin de contractions courtes et qualitatives, pas de résistance à la fatigue. L'objectif est neuromusculaire, pas cardiovasculaire.
-
-**4 séances/semaine — Durée totale : 20 minutes**
-
-#### Échauffement — 5 min
-- Respiration diaphragmatique en décubitus : inspirer en laissant le ventre se gonfler, expirer en rentrant doucement le nombril. 10 respirations conscientes.
-- Bascules de bassin allongée : aplatir puis creuser doucement le bas du dos. Réapprendre la proprioception lombaire.
-- Rotations de chevilles et poignets.
-- Mobilisation de la nuque et des épaules (tensions fréquentes du portage et de l'allaitement).
-
-#### ⏱ Bloc principal — 10 min | Protocole INVERSÉ 10 sec effort / 20 sec repos
-
-**Exercice 1 — Hypopressif de base**
-Debout, légère flexion des genoux, dos neutre. Inspirer profondément, expirer complètement puis rentrer le ventre au maximum sans bloquer la respiration. Maintenir 8 secondes.
-📋 *Les exercices hypopressifs créent une dépression abdominale qui remonte le plancher pelvien sans pression vers le bas. Base de la rééducation périnéale. Absent de toutes les apps tabata concurrentes.*
-
-**Exercice 2 — Pont fessier doux coordonné**
-Allongée, pieds à plat. Montée lente du bassin avec contraction simultanée du périnée. 3 sec montée / 3 sec maintien / 3 sec descente.
-📋 *Associer la contraction périnéale à la montée du bassin est le premier réflexe postural à reconstruire. Fondamental pour prévenir les fuites urinaires à long terme.*
-
-**Exercice 3 — Clamshell**
-Allongée sur le côté, genoux fléchis, ouvrir et fermer le genou supérieur. Renforcement du moyen fessier sans contrainte pelvienne.
-
-**Exercice 4 — Dead bug modifié (un seul membre)**
-Dos au sol, un seul membre à la fois (bras OU jambe, pas les deux simultanément). Colonne lombaire en contact avec le sol.
-📋 *La version complète (bras et jambe opposés) viendra en phase 2 uniquement si le diastasis est inférieur à 2 cm.*
-
-#### Retour au calme — 5 min
-- Étirement du psoas allongée (genoux vers la poitrine alternativement)
-- Respiration guidée 2 minutes
-- Automassage nuque et épaules
-
----
-
-### PHASE 2 — Semaines 3-4 : Reconstruction
-
-**Objectif :** réintroduire le gainage global, augmenter l'intensité cardiovasculaire. Retour au protocole tabata classique 20/10 sur des exercices sans impact.
-
-> 📋 **Test diastasis obligatoire avant phase 2.** Allongée en crunch partiel, placer les doigts sur la ligne blanche. Si l'écartement dépasse 2 doigts : rester en phase 1 deux semaines supplémentaires et consulter un kiné périnéal. Ce n'est pas un échec — c'est de la prudence médicale.
-
-#### Nouveaux exercices introduits en Phase 2
-
-**Bird dog progressif (deux membres)** — bras droit + jambe gauche simultanément si le diastasis le permet. Bassin parfaitement horizontal.
-📋 *La version un membre de la phase 1 reste disponible en cas de doute.*
-
-**Squat avec respiration coordonnée** — inspirer en descendant, expirer EN REMONTANT avec contraction périnéale.
-📋 *La majorité des fuites urinaires à l'effort viennent d'une dissociation entre effort et contraction périnéale. Ce squat "respiré" est thérapeutique.*
-
-**Fente statique basse tenue** — sans saut, sans impact. 20 secondes de maintien. Renforcement sans pression pelvienne verticale.
-
-**Planche sur genoux et avant-bras** — 20 secondes de maintien en respirant normalement. Pas de planche complète sur orteils avant la semaine 5.
-
----
-
-### PHASE 3 — Semaines 5-6 : Retour au Tabata
-
-**Objectif :** réintégrer un vrai protocole tabata avec exercices à intensité modérée. Format identique au programme Débutant semaines 1-2.
-
-**Ce qui reste exclu jusqu'à la fin du programme :**
-- Tous les sauts et impacts — même légers
-- Les crunchs, sit-ups, V-ups, relevés de buste classiques
-- La planche complète sur orteils avant semaine 5
-- Les exercices avec pression intra-abdominale forte
-
-**Exercices de la phase 3 :**
-- Squat avec respiration coordonnée (périnée)
-- Pont fessier bilatéral avec contraction périnéale
-- Bird dog complet
-- Planche sur orteils (introduction semaine 5 uniquement)
-- Fente avant statique
-- Dead bug complet (si diastasis < 2 doigts confirmé)
-
-> ⚠️ *La sensation subjective de récupération est toujours en avance sur la réalité tissulaire. Même si l'utilisatrice se sent prête pour les sauts : attendre 3 mois de rééducation périnéale.*
-
----
-
-### Récapitulatif Programme Post-Partum
-
-| Phase | Semaines | Protocole | Séances/sem | Impacts | Focus principal |
-|---|---|---|---|---|---|
-| Phase 1 | Sem. 1-2 | 10 sec / 20 sec (inversé) | 4 | ❌ Aucun | Périnée + transverse |
-| Phase 2 | Sem. 3-4 | 20 sec / 10 sec | 4 | ❌ Aucun | Gainage global |
-| Phase 3 | Sem. 5-6 | 20 sec / 10 sec | 4 | ❌ Aucun | Retour tabata doux |
-
-**Après le programme :** accès au programme Débutant standard. Parcours logique : Post-partum → Débutant → Intermédiaire → Avancé.
-
----
-
-## 6. Programme Seniors — 4 semaines
-
-> ⚠️ **Avertissement médical.** Programme destiné aux personnes de 60 ans et plus, actives ou en reprise d'activité, sans contre-indication médicale majeure. Si vous n'avez pas pratiqué de sport depuis plus de 2 ans ou si vous avez des antécédents cardiovasculaires : consulter votre médecin avant de commencer.
-
-**Objectif :** lutter contre la sarcopénie, améliorer l'équilibre et la proprioception pour prévenir les chutes, renforcer les muscles fonctionnels du quotidien, maintenir ou améliorer la mobilité articulaire.
-
-### Les 4 réalités physiologiques qui guident ce programme
-
-**Sarcopénie** — perte de ~1% de masse musculaire par an après 50 ans. Le tabata adapté est l'un des outils les plus efficaces pour la contrer.
-
-**Proprioception** — l'équilibre décline avec l'âge. Les chutes sont la 1ère cause de perte d'autonomie. Chaque séance contient un exercice de prévention des chutes.
-
-**Récupération** — plus lente : 48–72h contre 24–48h chez le jeune adulte. Maximum 3 séances/semaine, jamais 2 jours consécutifs.
-
-**Mobilité articulaire** — amplitudes réduites, cartilages plus fragiles. Chaque exercice est réalisable dans les amplitudes disponibles — pas les amplitudes théoriques d'un adulte jeune.
-
-### Les 3 règles non-négociables
-
-- **ZÉRO impact** tout le programme — pas de sauts, pas de burpees, pas de réceptions
-- **Protocole modifié** : 20 sec / 15 sec (semaines 1-2) puis 20/10 (semaines 3-4)
-- **Travail d'équilibre** dans chaque séance — non négociable
-
----
-
-### SEMAINE 1 — Éveil et repères
-
-**Format :** 2 blocs tabata (20/15) par séance. 3 séances/semaine.
-**Durée totale :** ~25 minutes
-
----
-
-#### Séance Senior A — Membres inférieurs & équilibre
-
-**Échauffement — 6 min**
-- 2 min — Marche sur place avec bras actifs
-- 1 min — Montées de genoux lentes en tenant le dossier d'une chaise
-- 1 min — Flexions-extensions de chevilles assis
-- 1 min — Cercles de hanches debout
-- 1 min — Transferts de poids latéraux (balancement doux d'un pied sur l'autre)
-
-**⏱ Bloc 1 — 20 sec effort / 15 sec repos**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Squat assisté avec chaise** — se lever et s'asseoir lentement d'une chaise sans se laisser tomber, contrôler la descente jusqu'au bout | **Équilibre unipodal** — se tenir sur un pied en tenant légèrement un support, 10 secondes chaque pied |
-| 📋 *C'est le mouvement fonctionnel le plus important de la vie quotidienne. RÈGLE : ne jamais s'écraser — contrôler la descente jusqu'au bout.* | 📋 *Fermer les yeux progressivement augmente considérablement la difficulté proprioceptive — progression naturelle pour les semaines suivantes.* |
-
-⏸ *1 min récupération active — marche lente*
-
-**⏱ Bloc 2 — 20 sec effort / 15 sec repos**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Fente statique tenue avec appui** — un pied devant, l'autre derrière, légère flexion, tenir 20 secondes, alterner | **Marche talon-orteil** — marcher en ligne en posant le talon immédiatement devant l'orteil de l'autre pied |
-| 📋 *Tenir un support de la main non-travaillante. L'appui sécurise et permet de se concentrer sur l'amplitude.* | 📋 *Exercice de coordination vestibulaire utilisé en rééducation neurologique. Prévoir un mur à portée de main.* |
-
-**Retour au calme — 5 min**
-- Étirements en position assise (jambes, dos)
-- Respiration guidée
-- Automassage des pieds (essentiels pour la proprioception)
-
----
-
-#### Séance Senior B — Haut du corps & gainage doux
-
-**Échauffement — 6 min**
-- Mobilisation complète des épaules, poignets, nuque
-- Activation des rhomboïdes : pincer les omoplates 10 fois
-- Chat-chameau en position assise (mobilité lombaire)
-
-**⏱ Bloc 1 — 20 sec / 15 sec**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompes contre le mur** — debout à 50 cm d'un mur, mains à hauteur d'épaules | **Rowing avec serviette** — simuler le geste de rameur en tirant les coudes vers l'arrière, omoplates rapprochées |
-| 📋 *À l'angle correct (corps incliné à 30°), la charge sur pectoraux et triceps est significative sans contrainte sur les poignets.* | 📋 *Renforcement des trapèzes moyens et rhomboïdes — muscles clés de la posture. Contre les épaules enroulées.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 2 — 20 sec / 15 sec**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Planche contre le mur** — corps incliné, avant-bras contre le mur, maintenir la position | **Rotation thoracique assise** — assis, bras croisés sur la poitrine, rotation lente droite-gauche, bassin fixe |
-| 📋 *Alternative idéale à la planche au sol pour les personnes avec difficultés à descendre ou douleurs aux poignets.* | 📋 *Mobilité thoracique essentielle pour la conduite, les gestes du quotidien et la prévention des douleurs cervicales.* |
-
----
-
-#### Séance Senior C — Corps entier fonctionnel
-
-> 💡 Cette séance travaille les **mouvements de la vie quotidienne**. C'est ce qu'aucune autre app tabata ne propose — et c'est précisément ce dont les seniors ont besoin.
-
-**⏱ Bloc 1 — 20 sec / 15 sec**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Ramasser au sol** — descendre pour simuler le ramassage d'un objet EN FLÉCHISSANT LES GENOUX, pas en arrondissant le dos | **Monter-descendre une marche** — monter et descendre une marche basse alternativement |
-| 📋 *C'est le mouvement le plus souvent réalisé incorrectement — l'une des principales causes de lumbago. Ce programme enseigne le bon geste.* | 📋 *Tenir un mur ou une rambarde si nécessaire. La sécurité prime sur la difficulté.* |
-
-⏸ *1 min récupération active*
-
-**⏱ Bloc 2 — 20 sec / 15 sec**
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Porter et poser simulé** — tenir un livre ou bouteille, le poser à hauteur d'épaule (étagère simulée), le reprendre | **Rotation avec objet léger** — tenir un objet, rotation droite-gauche en portant l'objet d'un côté à l'autre |
-| 📋 *Travaille la coordination œil-main, stabilité d'épaule et contraction abdominale réflexe. Directement transférable à la vie quotidienne.* | 📋 *Maintenir le bassin immobile — seul le tronc tourne.* |
-
----
-
-### SEMAINE 2 — Renforcement
-
-Passage au protocole 20/10 classique. Nouveaux exercices :
-- **Squat sans chaise** — même mouvement mais sans support. Chaise derrière soi comme filet de sécurité.
-- **Équilibre unipodal yeux fermés** — 5 secondes chaque pied.
-- **Pompes contre plan incliné (table)** — angle plus difficile, charge plus importante.
-- **Semi-squat unilatéral avec appui** — une jambe légèrement fléchie, l'autre levée légèrement.
-
-### SEMAINE 3 — Progression & confiance
-
-3 blocs tabata par séance. Exercices debout plus dynamiques — toujours sans impact. Nouveaux exercices :
-- **Marche rapide avec bras pompants** — simuler une marche athlétique intense sur place. Excellent pour la FC sans impact.
-- **Squat avec rotation** — au sommet du squat, rotation du buste à 45°. Travail combiné jambes + tronc.
-- **Deadlift fonctionnel avec objet léger** — charnière de hanche en avant. Prévient les douleurs lombaires chroniques.
-📋 *Le deadlift fonctionnel est le geste de se pencher en avant. Le renforcer prévient directement les douleurs lombaires chroniques, très fréquentes après 60 ans.*
-- **Élévation latérale des bras** — bras tendus, lever latéralement à hauteur d'épaule. Maintien de la capacité à porter.
-
-### SEMAINE 4 — Décharge & autonomie
-
-Retour à 2 blocs. Exercices connus. Tests de fin de programme.
-
-#### Tests cliniques de fin de programme
-
-| Test clinique | Protocole | Objectif |
-|---|---|---|
-| Five Times Sit to Stand | Se lever/s'asseoir d'une chaise, compter en 30 secondes | 12+ répétitions |
-| Équilibre unipodal | Tenir sur chaque pied yeux ouverts | 20 secondes sans appui |
-| Force haut du corps | Pompes contre le mur en 20 secondes | 12+ répétitions |
-
-> 📋 Ces trois tests sont des indicateurs cliniques réels utilisés en bilan kinésithérapeutique. Les intégrer dans l'app donne une valeur médicale concrète aux résultats.
-
----
-
-### Récapitulatif Programme Seniors
-
-| Semaine | Blocs/séance | Protocole | Séances/sem | Nouveautés | Durée totale |
-|---|---|---|---|---|---|
-| Semaine 1 | 2 blocs | 20/15 | 3 | Bases fonctionnelles | ~25 min |
-| Semaine 2 | 2 blocs | 20/10 | 3 | Sans chaise, incliné | ~25 min |
-| Semaine 3 | 3 blocs | 20/10 | 3 | Mouvements dynamiques | ~30 min |
-| Semaine 4 | 2 blocs | 20/10 | 3 | Tests cliniques | ~25 min |
-
----
-
-## 7. Programme Bureau — 4 semaines
-
-**Objectif :** intégrer le mouvement comme réflexe dans la journée de travail. 10 minutes de mouvement actif réduisent les douleurs lombaires de 30%, améliorent la concentration de 20% et contrebalancent partiellement les effets d'une journée sédentaire.
-
-### Les 5 contraintes de conception
-
-- **🔇 Silence** — pas de sauts, pas d'impacts, pas de chutes au sol, niveau sonore d'un bureau normal
-- **🚫 Pas de sol** — tous les exercices debout ou assis sur une chaise, zéro exercice au sol
-- **💧 Pas de sueur visible** — intensité calibrée pour élever la FC sans transpiration abondante
-- **⏱ 10 ou 20 min max** — deux formats selon la disponibilité
-- **👔 Tenue de bureau** — aucun exercice qui déforme une veste ou froisse un pantalon
-
----
-
-### FORMAT A — 10 minutes au bureau
-
-**Protocole :** 20 sec effort / 10 sec repos. 4 blocs de 2 minutes. Pendant une pause, sans bouger de sa zone de travail.
-
-#### Échauffement — 1 minute (assis, discret)
-- Cercles de poignets dans les deux sens
-- Mobilisation cervicale : inclinaisons et rotations douces
-- Ouverture de poitrine : bras en arrière, omoplates rapprochées
-
-#### ⏱ Bloc 1 — Activation profonde (assis)
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Contraction abdominale isométrique** — rentrer le nombril, maintenir en respirant normalement, invisible de l'extérieur | **Serrage fessier** — serrer les fessiers et maintenir 20 secondes, discret, invisible |
-| 📋 *C'est le transverse. Le contracter régulièrement en position assise combat directement les douleurs lombaires liées à la sédentarité.* | 📋 *Contre la rétroversion du bassin liée à la position assise prolongée.* |
-
-#### ⏱ Bloc 2 — Membres inférieurs (assis)
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Extension de jambe** — assis, lever une jambe tendue à hauteur de hanche, maintenir, alterner | **Élévation de talons assis** — pieds à plat, lever les talons en contractant les mollets |
-| 📋 *Renforcement du quadriceps. Ajouter une contraction du pied (orteils vers soi) pour activer les tibias antérieurs.* | 📋 *Active la pompe veineuse des jambes. Prévention des jambes lourdes et des varices.* |
-
-#### ⏱ Bloc 3 — Membres inférieurs (debout, silencieux)
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Wall sit (chaise invisible)** — dos au mur, cuisses parallèles au sol, maintenir | **Élévation de talons debout** — montées sur la pointe des pieds, descente lente en 3 secondes |
-| 📋 *Aucun bruit, aucun mouvement visible de l'extérieur, intensité maximale pour les quadriceps. Respirer calmement.* | 📋 *Renforcement des soléaires et gastrocnémiens. Bras tendus devant soi pour l'équilibre si nécessaire.* |
-
-#### ⏱ Bloc 4 — Haut du corps (debout)
-
-| Rounds impairs | Rounds pairs |
-|---|---|
-| **Pompes contre le bureau** — mains sur le bureau, corps incliné à 30°, silencieux, propre | **Pull apart imaginaire** — bras tendus devant, simuler l'écartement d'une résistance élastique en rétractant les omoplates |
-| 📋 *S'assurer que le bureau est stable avant de commencer. Plus incliné = plus facile.* | 📋 *Contre le syndrome de l'épaule roulée du bureau. Ouvrir la poitrine au maximum.* |
-
-#### Retour au calme — 1 minute
-- Étirement cervical latéral doux (chaque côté)
-- Étirement des poignets (extenseurs et fléchisseurs)
-- 3 grandes respirations diaphragmatiques
-
----
-
-### FORMAT B — 20 minutes en espace calme
-
-**Protocole :** tabata classique 20/10. 5 blocs de 4 minutes. Plus de liberté de mouvement, toujours zéro impact.
-
-#### Exercices spécifiques du Format B
-
-**Squat silencieux avec tempo ralenti** — tempo 3-3 pour rester sous le seuil de transpiration tout en maintenant l'efficacité musculaire.
-
-**Fente statique avec rotation de buste** — fente basse tenue + rotation du tronc vers le genou avant. Mobilité thoracique et renforcement simultanés.
-
-**Bureau dips** — mains sur le bureau derrière soi, corps décollé, fléchir les coudes. Triceps et stabilisateurs d'épaule.
-📋 *Vérifier la stabilité du bureau avant de commencer.*
-
-**Calf raises avec déséquilibre** — montées sur la pointe des pieds avec les yeux fermés. Double effet : renforcement + proprioception.
-
-**Isométrie fessiers debout** — contracter les fessiers alternativement sans bouger les jambes. Activation du moyen fessier inhibé par la position assise.
-
-**Rotation de buste debout rapide** — bras croisés sur la poitrine, rotation droite-gauche rapide. Active les obliques et échauffe les disques intervertébraux.
-
----
-
-### FORMAT C — Walking Meeting (20 minutes)
-
-> Format conçu pour être utilisé pendant une marche. L'app guide uniquement en **audio** — aucun besoin de regarder l'écran.
-
-**Structure :** alterner des phases de marche rapide tabata (20 sec intensité maximale) et des phases de marche normale (10 sec récup), sur 5 blocs de 4 minutes.
-
-| Type de marche | Utilisation | Intensité |
-|---|---|---|
-| Marche normale | Phase de repos (10 sec) | ⬜ Faible |
-| Marche rapide | Phase d'effort standard | 🟡 Modérée |
-| Marche genoux hauts | Phase d'effort intensifié | 🟠 Élevée |
-| Marche en fente | Pas le plus long possible | 🔴 Haute |
-| Montée d'escaliers | Si disponibles | 🔴 Haute |
-
-📋 *La marche active à 6–7 km/h suffit à atteindre 70–80% de la fréquence cardiaque maximale chez un adulte sédentaire. Amplement suffisant pour les bénéfices cardiovasculaires du HIIT sans impact traumatisant.*
-
----
-
-### Progression sur 4 semaines
-
-| Semaine | Format A | Format B | Format C | Objectif |
-|---|---|---|---|---|
-| Semaine 1 | 3×/semaine | — | — | Créer l'habitude de la pause active |
-| Semaine 2 | 2×/semaine | 1×/semaine | — | Introduction du format long |
-| Semaine 3 | 1×/semaine | 2×/semaine | 1×/semaine | Walking meeting intégré |
-| Semaine 4 | Libre | Libre | Libre | Autonomie — choisir selon la semaine |
-
-> 💡 **L'objectif final** n'est pas de suivre un programme — c'est d'intégrer le mouvement comme réflexe dans la journée de travail. La semaine 4 sans structure fixe est intentionnelle : c'est la semaine de la prise d'autonomie.
-
----
-
-## 8. Récapitulatif global
-
-| Programme | Durée | Séances/sem | Blocs max | Impacts | Protocole | Tier | Exercices signature |
-|---|---|---|---|---|---|---|---|
-| 🟢 Débutant | 4 sem | 3 | 3 | ❌ | 20/10 | Gratuit | Squat, pompes genoux, pont fessier, planche AV, bird dog, dead bug |
-| 🔵 Intermédiaire | 4 sem | 4 | 4 | ⚡ | 20/10 | Premium | Squat jump, fente sautée, burpee, hollow body, pistol squat assisté |
-| 🔴 Avancé | 4 sem | 5 | 5 | 🔥 | 20/10 | Premium | Nordic curl, pistol squat complet, pompe plyométrique, complexes, MetCon |
-| 🩷 Post-partum | 6 sem | 4 | 3 | ❌ | 10/20 → 20/10 | Kiné+ | Hypopressifs, pont périnéal, dead bug 1 membre, bird dog progressif |
-| 🟤 Seniors | 4 sem | 3 | 3 | ❌ | 20/15 → 20/10 | Kiné+ | Squat chaise, équilibre unipodal, pompes mur, marche talon-orteil |
-| 🟡 Bureau | 4 sem | 3–5 | 5 | ❌ | 20/10 | Premium | Wall sit, pompes bureau, pull apart, extension de jambe assis |
-
-### Parcours utilisateurs recommandés
-
-**Parcours Standard**
-Débutant → Intermédiaire → Avancé
-
-**Parcours Maman**
-Post-partum → Débutant → Intermédiaire → Avancé
-
-**Parcours Senior**
-Seniors → Débutant (si capacité) → Maintenance Seniors cyclique
-
-**Parcours Actif Sédentaire**
-Bureau → Débutant → Intermédiaire → Bureau en maintenance
-
----
-
-> *Ce guide représente 26 semaines de contenu kiné structuré. Chaque exercice est médicalement raisonné. Chaque progression est physiologiquement justifiée. C'est ce qu'aucune app tabata concurrente ne propose aujourd'hui.*
-
----
-*Document élaboré en avril 2026 — App Tabata Kiné — Tous droits réservés*
diff --git a/app.json b/app.json
deleted file mode 100644
index 37c7d9e..0000000
--- a/app.json
+++ /dev/null
@@ -1,91 +0,0 @@
-{
- "expo": {
- "name": "TabataFit",
- "slug": "tabatafit",
- "version": "1.0.0",
- "orientation": "portrait",
- "icon": "./assets/images/icon.png",
- "scheme": "tabatafit",
- "userInterfaceStyle": "automatic",
- "newArchEnabled": true,
- "ios": {
- "supportsTablet": true,
- "bundleIdentifier": "com.millianlmx.tabatafit",
- "buildNumber": "1",
- "infoPlist": {
- "NSHealthShareUsageDescription": "TabataFit uses HealthKit to read and write workout data including heart rate, calories burned, and exercise minutes.",
- "NSHealthUpdateUsageDescription": "TabataFit saves your workout sessions to Apple Health so you can track your fitness progress.",
- "NSCameraUsageDescription": "TabataFit uses the camera for profile photos and workout form checks.",
- "NSUserTrackingUsageDescription": "TabataFit uses this to provide personalized workout recommendations.",
- "ITSAppUsesNonExemptEncryption": false
- },
- "config": {
- "usesNonExemptEncryption": false
- },
- "associatedDomains": [
- "applinks:tabatafit.app"
- ]
- },
- "android": {
- "adaptiveIcon": {
- "backgroundColor": "#E6F4FE",
- "foregroundImage": "./assets/images/android-icon-foreground.png",
- "backgroundImage": "./assets/images/android-icon-background.png",
- "monochromeImage": "./assets/images/android-icon-monochrome.png"
- },
- "edgeToEdgeEnabled": true,
- "predictiveBackGestureEnabled": false,
- "package": "com.millianlmx.tabatafit",
- "intentFilters": [
- {
- "action": "VIEW",
- "autoVerify": true,
- "data": [
- {
- "scheme": "https",
- "host": "tabatafit.app",
- "pathPrefix": "/workout"
- },
- {
- "scheme": "https",
- "host": "tabatafit.app",
- "pathPrefix": "/player"
- },
- {
- "scheme": "https",
- "host": "tabatafit.app",
- "pathPrefix": "/program"
- }
- ],
- "category": ["BROWSABLE", "DEFAULT"]
- }
- ]
- },
- "web": {
- "output": "static",
- "favicon": "./assets/images/favicon.png"
- },
- "plugins": [
- "expo-router",
- [
- "expo-splash-screen",
- {
- "image": "./assets/images/splash-icon.png",
- "imageWidth": 200,
- "resizeMode": "contain",
- "backgroundColor": "#ffffff",
- "dark": {
- "backgroundColor": "#000000"
- }
- }
- ],
- "expo-video",
- "expo-localization",
- "./plugins/withStoreKitConfig"
- ],
- "experiments": {
- "typedRoutes": true,
- "reactCompiler": true
- }
- }
-}
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
deleted file mode 100644
index a5c65a7..0000000
--- a/app/(tabs)/_layout.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * TabataGo Tab Layout
- * Native liquid glass tab bar (iOS 26+) via expo-router/unstable-native-tabs
- * 3 tabs: Home, Activity, Profile
- */
-
-import { Redirect } from 'expo-router'
-import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'
-import { useTranslation } from 'react-i18next'
-
-import { BRAND, TEXT, NAVY } from '@/src/shared/constants/colors'
-import { useUserStore } from '@/src/shared/stores'
-
-export default function TabLayout() {
- const { t } = useTranslation()
- const onboardingCompleted = useUserStore((s) => s.profile.onboardingCompleted)
-
- if (!onboardingCompleted) {
- return
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/app/(tabs)/activity.tsx b/app/(tabs)/activity.tsx
deleted file mode 100644
index 2cee486..0000000
--- a/app/(tabs)/activity.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * TabataGo Activity Tab
- * Streak, weekly sessions, program history — driven by progressStore.
- */
-
-import { useMemo } from 'react'
-import { View, Text, StyleSheet, ScrollView } from 'react-native'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useTranslation } from 'react-i18next'
-import { Icon } from '@/src/shared/components/Icon'
-import { useProgressStore } from '@/src/shared/stores/progressStore'
-import { useThemeColors } from '@/src/shared/theme'
-import type { ThemeColors } from '@/src/shared/theme/types'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { GREEN, NAVY, TEXT, BORDER_COLORS } from '@/src/shared/constants/colors'
-
-export default function ActivityScreen() {
- const { t } = useTranslation()
- const insets = useSafeAreaInsets()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
-
- const history = useProgressStore(s => s.history)
- const streak = useProgressStore(s => s.streak)
- const weeklyCount = useProgressStore(s => s.getWeeklyCount())
- const completedCount = useProgressStore(s => s.getCompletedCount())
-
- const totalMinutes = useMemo(
- () => history.reduce((sum, s) => sum + Math.round(s.durationSeconds / 60), 0),
- [history],
- )
-
- return (
-
- {t('screens:tabs.progression')}
-
- {/* Streak hero */}
-
-
- {streak.current}
- {t('screens:activity.dayStreak')}
-
- {t('screens:activity.longest')}: {streak.longest}
-
-
-
- {/* Stats grid */}
-
-
-
-
-
-
- {/* Recent history */}
- {history.length > 0 && (
-
- {t('screens:activity.recent')}
- {history.slice(0, 10).map((session, i) => (
-
-
-
- {session.programId}
-
- {Math.round(session.durationSeconds / 60)} min
- {' · '}
- {new Date(session.completedAt).toLocaleDateString()}
-
-
-
- ))}
-
- )}
-
- {history.length === 0 && (
-
- {t('screens:activity.emptyTitle')}
- {t('screens:activity.emptySubtitle')}
-
- )}
-
- )
-}
-
-function StatCard({ icon, value, label, color }: { icon: any; value: number; label: string; color: string }) {
- return (
-
-
- {value}
- {label}
-
- )
-}
-
-const cardStyles = StyleSheet.create({
- card: {
- flex: 1,
- alignItems: 'center',
- padding: SPACING[3],
- borderRadius: RADIUS.LG,
- backgroundColor: NAVY[800],
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- gap: SPACING[1],
- borderCurve: 'continuous',
- },
- value: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
- label: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, textAlign: 'center' },
-})
-
-function createStyles(colors: ThemeColors) {
- return StyleSheet.create({
- container: { flex: 1, backgroundColor: colors.bg.base },
- content: { paddingHorizontal: LAYOUT.SCREEN_PADDING },
-
- title: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, marginBottom: SPACING[5] },
-
- streakHero: {
- alignItems: 'center',
- paddingVertical: SPACING[6],
- marginBottom: SPACING[4],
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.XL,
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- gap: SPACING[1],
- },
- streakCount: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, fontSize: 56, fontVariant: ['tabular-nums'] },
- streakLabel: { ...TYPOGRAPHY.HEADLINE, color: TEXT.SECONDARY },
- streakRecord: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[1] },
-
- grid: { flexDirection: 'row', gap: SPACING[3], marginBottom: SPACING[6] },
-
- historySection: { gap: SPACING[2] },
- sectionTitle: {
- ...TYPOGRAPHY.CAPTION_1,
- color: TEXT.TERTIARY,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- marginBottom: SPACING[1],
- },
- historyRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- padding: SPACING[3],
- backgroundColor: colors.surface.default.backgroundColor,
- borderRadius: RADIUS.MD,
- borderWidth: 1,
- borderColor: colors.surface.default.borderColor,
- },
- historyTitle: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.PRIMARY },
- historyMeta: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: 2 },
-
- emptyState: { alignItems: 'center', marginTop: SPACING[12], gap: SPACING[2] },
- emptyTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY },
- emptySubtitle: { ...TYPOGRAPHY.BODY, color: TEXT.TERTIARY, textAlign: 'center' },
- })
-}
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
deleted file mode 100644
index fff039b..0000000
--- a/app/(tabs)/index.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-/**
- * TabataGo Home Screen
- * Mascot + 3 stat pills + 3 body zone cards + settings button.
- */
-
-import { View, Text, StyleSheet, ScrollView, Pressable } from 'react-native'
-import { useRouter } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useTranslation } from 'react-i18next'
-
-import { Icon } from '@/src/shared/components/Icon'
-import { Mascot } from '@/src/shared/components/Mascot'
-import { useUserStore } from '@/src/shared/stores/userStore'
-import { useProgressStore } from '@/src/shared/stores/progressStore'
-import { BODY_ZONE_META, type BodyZone } from '@/src/shared/types/workoutProgram'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { TEXT, NAVY, GREEN, BORDER_COLORS } from '@/src/shared/constants/colors'
-import { withOpacity } from '@/src/shared/utils/color'
-
-const BODY_ZONES: BodyZone[] = ['upper-body', 'lower-body', 'full-body']
-
-export default function HomeScreen() {
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const { t } = useTranslation()
-
- const firstName = useUserStore(s => s.profile.name)
- const streak = useProgressStore(s => s.streak.current)
- const weeklyCount = useProgressStore(s => s.getWeeklyCount())
- const completedCount = useProgressStore(s => s.getCompletedCount())
-
- const nameSuffix = firstName ? `, ${firstName}` : ''
- const mascotMessage = streak > 0
- ? t('screens:home.mascotStreak', { count: streak, name: nameSuffix })
- : t('screens:home.mascotReady', { name: nameSuffix })
-
- return (
-
- {/* Header with settings */}
-
- TabataGo
- router.push('/settings')} style={styles.iconBtn} hitSlop={8}>
-
-
-
-
- {/* Mascot */}
-
-
-
-
- {/* Stats pills */}
-
-
-
-
-
-
- {/* Body zone cards */}
- {t('screens:zone.chooseYourFocus')}
-
- {BODY_ZONES.map(zone => (
- router.push(`/zone/${zone}`)} />
- ))}
-
-
- )
-}
-
-function StatPill({
- value,
- label,
- icon,
- color,
-}: {
- value: number
- label: string
- icon: any
- color: string
-}) {
- return (
-
-
- {value}
- {label}
-
- )
-}
-
-function ZoneCard({ zone, onPress }: { zone: BodyZone; onPress: () => void }) {
- const meta = BODY_ZONE_META[zone]
- const { t } = useTranslation()
- return (
- [
- styles.zoneCard,
- { borderColor: withOpacity(meta.color, 0.3) },
- pressed && { opacity: 0.85, transform: [{ scale: 0.98 }] },
- ]}
- >
- {/* Colored top strip with large icon */}
-
-
-
-
-
-
- {/* Content area */}
-
- {meta.label}
-
- {t(meta.descKey)}
-
-
- {/* Bottom row: level badge + chevron */}
-
-
-
- {t('screens:home.zoneLevels')}
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: NAVY[900] },
- content: { paddingHorizontal: SPACING[5] },
-
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: SPACING[4],
- },
- brand: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, letterSpacing: -0.5 },
- iconBtn: {
- width: 40,
- height: 40,
- borderRadius: 20,
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: NAVY[800],
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- },
-
- mascotWrap: { alignItems: 'center', marginVertical: SPACING[4] },
-
- statsRow: { flexDirection: 'row', gap: SPACING[2], marginBottom: SPACING[6] },
- pill: {
- flex: 1,
- alignItems: 'center',
- paddingVertical: SPACING[3],
- paddingHorizontal: SPACING[2],
- borderRadius: RADIUS.MD,
- borderWidth: 1,
- backgroundColor: NAVY[800],
- gap: 4,
- },
- pillValue: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
- pillLabel: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, textAlign: 'center' },
-
- sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, marginBottom: SPACING[3] },
- zoneList: { gap: SPACING[4] },
- zoneCard: {
- borderRadius: RADIUS.XL,
- borderWidth: 1,
- backgroundColor: NAVY[800],
- overflow: 'hidden' as const,
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
- },
- zoneTopStrip: {
- alignItems: 'center' as const,
- justifyContent: 'center' as const,
- paddingVertical: SPACING[5],
- },
- zoneIconCircle: {
- width: 72,
- height: 72,
- borderRadius: 36,
- alignItems: 'center' as const,
- justifyContent: 'center' as const,
- },
- zoneContent: {
- padding: SPACING[4],
- gap: SPACING[2],
- },
- zoneTitle: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
- zoneDesc: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY, lineHeight: 20 },
- zoneFooter: {
- flexDirection: 'row' as const,
- alignItems: 'center' as const,
- justifyContent: 'space-between' as const,
- marginTop: SPACING[1],
- },
- zoneBadge: {
- paddingHorizontal: SPACING[3],
- paddingVertical: SPACING[1],
- borderRadius: RADIUS.SM,
- },
- zoneBadgeText: { ...TYPOGRAPHY.CAPTION_1, fontWeight: '600' as const },
-})
diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx
deleted file mode 100644
index aecbf56..0000000
--- a/app/(tabs)/profile.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-/**
- * TabataGo Profile Tab
- * User info, subscription status, quick stats. Settings via form sheet.
- */
-
-import { useMemo } from 'react'
-import { View, Text, StyleSheet, ScrollView, Pressable } from 'react-native'
-import { useRouter } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useTranslation } from 'react-i18next'
-import { Icon } from '@/src/shared/components/Icon'
-import { useUserStore } from '@/src/shared/stores/userStore'
-import { useProgressStore } from '@/src/shared/stores/progressStore'
-import { usePurchases } from '@/src/shared/hooks'
-import { useThemeColors } from '@/src/shared/theme'
-import type { ThemeColors } from '@/src/shared/theme/types'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { GREEN, NAVY, TEXT, BORDER_COLORS } from '@/src/shared/constants/colors'
-
-export default function ProfileScreen() {
- const { t } = useTranslation()
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
-
- const profile = useUserStore(s => s.profile)
- const { isPremium } = usePurchases()
-
- const completedCount = useProgressStore(s => s.getCompletedCount())
- const streak = useProgressStore(s => s.streak)
- const weeklyCount = useProgressStore(s => s.getWeeklyCount())
-
- const avatarLetter = profile.name?.[0]?.toUpperCase() || '?'
-
- return (
-
- {/* Avatar + name */}
-
-
- {avatarLetter}
-
-
- {profile.name || t('screens:profile.guest')}
-
-
- {isPremium ? t('screens:settings.premium') : t('screens:settings.free')}
-
-
-
- router.push('/settings')} hitSlop={8}>
-
-
-
-
- {/* Stats row */}
-
-
-
-
-
-
- {/* Upgrade banner (free users) */}
- {!isPremium && (
- router.push('/paywall')}
- >
-
-
- {t('screens:profile.upgradeTitle')}
- {t('screens:profile.upgradeDescription')}
-
-
-
- )}
-
- {/* Settings link */}
- router.push('/settings')}>
-
- {t('screens:settings.title')}
-
-
-
- )
-}
-
-function StatPill({ value, label, icon, color }: { value: number; label: string; icon: any; color: string }) {
- return (
-
-
- {value}
- {label}
-
- )
-}
-
-const pillStyles = StyleSheet.create({
- pill: {
- flex: 1,
- alignItems: 'center',
- paddingVertical: SPACING[3],
- borderRadius: RADIUS.MD,
- borderWidth: 1,
- backgroundColor: NAVY[800],
- borderColor: BORDER_COLORS.DIM,
- gap: 4,
- },
- value: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
- label: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, textAlign: 'center' },
-})
-
-function createStyles(colors: ThemeColors) {
- return StyleSheet.create({
- container: { flex: 1, backgroundColor: colors.bg.base },
- content: { paddingHorizontal: LAYOUT.SCREEN_PADDING },
-
- profileHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- marginBottom: SPACING[5],
- },
- avatar: {
- width: 60,
- height: 60,
- borderRadius: 30,
- backgroundColor: NAVY[700] ?? NAVY[800],
- alignItems: 'center',
- justifyContent: 'center',
- borderWidth: 2,
- borderColor: BORDER_COLORS.DIM,
- },
- avatarLetter: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY },
- name: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
- planBadge: {
- alignSelf: 'flex-start',
- marginTop: 4,
- paddingHorizontal: SPACING[2],
- paddingVertical: 2,
- borderRadius: RADIUS.SM,
- borderWidth: 1,
- },
- planText: { ...TYPOGRAPHY.CAPTION_2, fontWeight: '600' },
-
- statsRow: { flexDirection: 'row', gap: SPACING[2], marginBottom: SPACING[5] },
-
- upgradeBanner: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- padding: SPACING[4],
- borderRadius: RADIUS.LG,
- borderWidth: 1,
- backgroundColor: colors.surface.default.backgroundColor,
- marginBottom: SPACING[3],
- borderCurve: 'continuous',
- },
- upgradeTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY },
- upgradeDesc: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.SECONDARY, marginTop: 2 },
-
- settingsRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- padding: SPACING[4],
- borderRadius: RADIUS.LG,
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- backgroundColor: colors.surface.default.backgroundColor,
- borderCurve: 'continuous',
- },
- settingsLabel: { ...TYPOGRAPHY.BODY, color: TEXT.PRIMARY, flex: 1 },
- })
-}
diff --git a/app/CLAUDE.md b/app/CLAUDE.md
deleted file mode 100644
index 0c8e6a6..0000000
--- a/app/CLAUDE.md
+++ /dev/null
@@ -1,70 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 19, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5001 | 9:35 AM | 🔵 | Host Wrapper Located at Root Layout Level | ~153 |
-
-### Feb 20, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5115 | 8:57 AM | 🔵 | Root Layout Stack Configuration with Screen Animations | ~256 |
-| #5061 | 8:47 AM | 🔵 | Expo Router Tab Navigation Structure Found | ~196 |
-| #5053 | 8:23 AM | ✅ | Completed removal of all Host wrappers from application | ~255 |
-| #5052 | " | ✅ | Removed Host wrapper from root layout entirely | ~224 |
-| #5019 | 8:13 AM | 🔵 | Root layout properly wraps Stack with Host component | ~198 |
-
-### Feb 28, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5598 | 9:22 PM | 🟣 | Enabled PostHog analytics in development mode | ~253 |
-| #5597 | " | 🔄 | PostHogProvider initialization updated with client check and autocapture config | ~303 |
-| #5589 | 7:51 PM | 🟣 | PostHog screen tracking added to onboarding flow | ~246 |
-| #5588 | 7:50 PM | ✅ | Added trackScreen function to onboarding analytics imports | ~203 |
-| #5585 | " | ✅ | Enhanced PostHogProvider initialization with null safety | ~239 |
-| #5584 | 7:49 PM | ✅ | Imported trackScreen function in root layout | ~202 |
-| #5583 | " | 🟣 | PostHog user identification added to onboarding completion | ~291 |
-| #5582 | " | ✅ | Enhanced onboarding analytics with user identification | ~187 |
-| #5579 | 7:47 PM | 🔵 | Comprehensive analytics tracking in onboarding flow | ~345 |
-| #5575 | 7:44 PM | 🔵 | PostHog integration architecture in root layout | ~279 |
-| #5572 | 7:43 PM | 🔵 | PostHog integration points identified | ~228 |
-
-### Apr 10, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6017 | 10:06 AM | 🔵 | Explore Filter Sheet for Level and Equipment | ~307 |
-| #6000 | 10:01 AM | 🔵 | Root App Architecture Examined | ~316 |
-
-### Apr 11, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6129 | 7:42 PM | 🔄 | Onboarding Wow icon circle opacity refactored | ~295 |
-| #6126 | 7:41 PM | 🔵 | Assessment screen imports reviewed | ~293 |
-| #6122 | " | 🔵 | Onboarding screen uses dynamic color with hex transparency | ~277 |
-
-### Apr 13, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6175 | 10:04 PM | 🟣 | Completed Explore Tab Removal | ~196 |
-| #6170 | 10:03 PM | 🟣 | Removed Explore Filters Modal Route | ~143 |
-| #6165 | 10:02 PM | 🔵 | Located Explore Filters Screen Configuration | ~141 |
-| #6160 | " | 🔵 | Identified Explore Filters Screen Configuration | ~141 |
-| #6156 | " | 🔵 | Found Explore Tab References | ~155 |
-
-### Apr 17, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6373 | 10:26 AM | 🟣 | Registered body zone detail screen route in app navigation | ~296 |
-| #6372 | 10:25 AM | 🔵 | Reviewed app layout navigation configuration for workout and program screens | ~318 |
-| #6371 | " | 🔵 | Examined app routing layout structure for workout routes | ~247 |
-
\ No newline at end of file
diff --git a/app/_layout.tsx b/app/_layout.tsx
deleted file mode 100644
index 7b8473e..0000000
--- a/app/_layout.tsx
+++ /dev/null
@@ -1,260 +0,0 @@
-/**
- * TabataFit Root Layout
- * Expo Router v3 — SF Pro system font (no custom font loading)
- * Waits for store hydration before rendering
- */
-
-import '@/src/shared/i18n'
-import '@/src/shared/i18n/types'
-
-import { useState, useEffect, useCallback, Component } from 'react'
-import { Stack } from 'expo-router'
-import { StatusBar } from 'expo-status-bar'
-import { View, Text, Pressable, StyleSheet } from 'react-native'
-import * as SplashScreen from 'expo-splash-screen'
-import * as Notifications from 'expo-notifications'
-
-import { PostHogProvider } from 'posthog-react-native'
-
-import { ThemeProvider, useThemeColors } from '@/src/shared/theme'
-import { TEXT, NAVY, GREEN } from '@/src/shared/constants/colors'
-import { useUserStore } from '@/src/shared/stores'
-import { useNotifications } from '@/src/shared/hooks'
-import { OfflineBanner } from '@/src/shared/components/OfflineBanner'
-import { initializePurchases } from '@/src/shared/services/purchases'
-import { initializeAnalytics, getPostHogClient, trackScreen } from '@/src/shared/services/analytics'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-
-Notifications.setNotificationHandler({
- handleNotification: async () => ({
- shouldShowAlert: false,
- shouldPlaySound: false,
- shouldSetBadge: false,
- shouldShowBanner: false,
- shouldShowList: false,
- }),
-})
-
-SplashScreen.preventAutoHideAsync()
-
-// ─── Error Boundary (F-108) ────────────────────────────────────────────────
-interface ErrorBoundaryState {
- hasError: boolean
- error: Error | null
-}
-
-class ErrorBoundary extends Component<{ children: React.ReactNode }, ErrorBoundaryState> {
- state: ErrorBoundaryState = { hasError: false, error: null }
-
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
- return { hasError: true, error }
- }
-
- componentDidCatch(error: Error, info: React.ErrorInfo) {
- console.error('ErrorBoundary caught:', error, info.componentStack)
- }
-
- private handleRetry = () => {
- this.setState({ hasError: false, error: null })
- }
-
- render() {
- if (this.state.hasError) {
- return (
-
- ⚠️
- Something went wrong
-
- {this.state.error?.message ?? 'An unexpected error occurred.'}
-
-
- Try again
-
-
- )
- }
- return this.props.children
- }
-}
-
-const errorStyles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: NAVY[900],
- alignItems: 'center',
- justifyContent: 'center',
- padding: 32,
- },
- emoji: { fontSize: 48, marginBottom: 16 },
- title: {
- fontSize: 22,
- fontWeight: '700',
- color: TEXT.PRIMARY,
- marginBottom: 8,
- },
- message: {
- fontSize: 15,
- fontWeight: '400',
- color: TEXT.SECONDARY,
- textAlign: 'center',
- marginBottom: 24,
- lineHeight: 20,
- },
- button: {
- backgroundColor: '#00C896',
- paddingHorizontal: 32,
- paddingVertical: 14,
- borderRadius: 12,
- borderCurve: 'continuous',
- minHeight: 44,
- },
- buttonText: {
- fontSize: 17,
- fontWeight: '600',
- color: NAVY[900],
- },
-})
-
-// Create React Query Client
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- staleTime: 1000 * 60 * 5, // 5 minutes
- gcTime: 1000 * 60 * 60 * 24, // 24 hours
- retry: 2,
- refetchOnWindowFocus: false,
- },
- },
-})
-
-function RootLayoutInner() {
- const colors = useThemeColors()
-
- useNotifications()
-
- // Wait for persisted store to hydrate from AsyncStorage
- const [hydrated, setHydrated] = useState(useUserStore.persist.hasHydrated())
-
- useEffect(() => {
- const unsub = useUserStore.persist.onFinishHydration(() => setHydrated(true))
- return unsub
- }, [])
-
- // Initialize RevenueCat + PostHog after hydration
- useEffect(() => {
- if (hydrated) {
- initializePurchases().catch((err) => {
- console.error('Failed to initialize RevenueCat:', err)
- })
- initializeAnalytics().catch((err) => {
- console.error('Failed to initialize PostHog:', err)
- })
- }
- }, [hydrated])
-
- const onLayoutRootView = useCallback(async () => {
- if (hydrated) {
- await SplashScreen.hideAsync()
- }
- }, [hydrated])
-
- if (!hydrated) {
- return null
- }
-
- const content = (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-
- const posthogClient = getPostHogClient()
-
- // Only wrap with PostHogProvider if client is initialized
- if (!posthogClient) {
- return content
- }
-
- return (
-
- {content}
-
- )
-}
-
-export default function RootLayout() {
- return (
-
-
-
-
-
- )
-}
diff --git a/app/complete/CLAUDE.md b/app/complete/CLAUDE.md
deleted file mode 100644
index bee9fb7..0000000
--- a/app/complete/CLAUDE.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 20, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5047 | 8:22 AM | ✅ | Completed Host wrapper removal from all screens | ~241 |
-| #5046 | " | ✅ | Removed opening Host tag from workout complete screen | ~165 |
-| #5033 | 8:20 AM | ✅ | Removed Host import from workout complete screen | ~212 |
-| #5026 | 8:18 AM | 🔵 | Workout complete screen properly wraps content with Host component | ~252 |
-
\ No newline at end of file
diff --git a/app/complete/[id].tsx b/app/complete/[id].tsx
deleted file mode 100644
index d490af5..0000000
--- a/app/complete/[id].tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * TabataFit Workout Complete Screen
- * Celebration + stats driven by progressStore.
- */
-
-import { useEffect, useMemo, useRef } from 'react'
-import {
- View,
- Text as RNText,
- StyleSheet,
- ScrollView,
- Animated,
-} from 'react-native'
-import { useRouter, useLocalSearchParams } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { Icon, type IconName } from '@/src/shared/components/Icon'
-import { useTranslation } from 'react-i18next'
-import * as Sharing from 'expo-sharing'
-
-import { useHaptics } from '@/src/shared/hooks'
-import { useProgressStore } from '@/src/shared/stores'
-import { NativeButton } from '@/src/shared/components/native'
-
-import { useThemeColors } from '@/src/shared/theme'
-import type { ThemeColors } from '@/src/shared/theme/types'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPRING } from '@/src/shared/constants/animations'
-import { GREEN, NAVY, TEXT, BORDER_COLORS } from '@/src/shared/constants/colors'
-
-function StatCard({
- value,
- label,
- icon,
- delay = 0,
-}: {
- value: string | number
- label: string
- icon: IconName
- delay?: number
-}) {
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
- const scaleAnim = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- Animated.sequence([
- Animated.delay(delay),
- Animated.spring(scaleAnim, { toValue: 1, ...SPRING.BOUNCY, useNativeDriver: true }),
- ]).start()
- }, [delay])
-
- return (
-
-
- {value}
- {label}
-
- )
-}
-
-export default function WorkoutCompleteScreen() {
- const insets = useSafeAreaInsets()
- const router = useRouter()
- const haptics = useHaptics()
- const { t } = useTranslation()
- const { id } = useLocalSearchParams<{ id: string }>()
-
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
-
- const history = useProgressStore((s) => s.history)
- const streak = useProgressStore((s) => s.streak)
- const weeklyCount = useProgressStore((s) => s.getWeeklyCount())
-
- // Latest session (the one we just completed)
- const latest = history[0]
- const resultMinutes = latest ? Math.round(latest.durationSeconds / 60) : 0
-
- const handleGoHome = () => {
- haptics.buttonTap()
- router.replace('/')
- }
-
- const handleShare = async () => {
- haptics.selection()
- const isAvailable = await Sharing.isAvailableAsync()
- if (isAvailable) {
- await Sharing.shareAsync('https://tabatafit.app', {
- dialogTitle: t('screens:complete.shareTitle', { minutes: resultMinutes }),
- })
- }
- }
-
- useEffect(() => {
- haptics.workoutComplete()
- }, [])
-
- return (
-
-
- {/* Celebration */}
-
- 🎉
- {t('screens:complete.title')}
-
-
- {/* Stats Grid */}
-
-
-
-
-
-
-
-
- {/* Streak */}
-
-
-
-
-
-
- {t('screens:complete.streakDays', { count: streak.current })}
-
-
- {t('screens:complete.streakRecord', { count: streak.longest })}
-
-
-
-
-
-
- {/* Share */}
-
-
-
-
-
- {/* Fixed Bottom Button */}
-
-
-
-
-
-
- )
-}
-
-function createStyles(colors: ThemeColors) {
- return StyleSheet.create({
- container: { flex: 1, backgroundColor: colors.bg.base },
- scrollContent: { paddingHorizontal: LAYOUT.SCREEN_PADDING },
-
- celebrationSection: { alignItems: 'center', paddingVertical: SPACING[8] },
- celebrationEmoji: { fontSize: 64, marginBottom: SPACING[4] },
- celebrationTitle: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, letterSpacing: 1 },
-
- statsGrid: { flexDirection: 'row', gap: SPACING[3], marginBottom: SPACING[6] },
- statCard: {
- flex: 1,
- padding: SPACING[3],
- borderRadius: RADIUS.LG,
- backgroundColor: colors.surface.default.backgroundColor,
- alignItems: 'center',
- borderWidth: 1,
- borderColor: colors.surface.default.borderColor,
- borderCurve: 'continuous',
- },
- statValue: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, marginTop: SPACING[2], fontVariant: ['tabular-nums'] },
- statLabel: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, marginTop: SPACING[1] },
-
- divider: { height: 1, backgroundColor: BORDER_COLORS.DIM, marginVertical: SPACING[2] },
-
- streakSection: { flexDirection: 'row', alignItems: 'center', paddingVertical: SPACING[4], gap: SPACING[4] },
- streakBadge: { width: 64, height: 64, borderRadius: RADIUS.FULL, alignItems: 'center', justifyContent: 'center' },
- streakInfo: { flex: 1 },
- streakTitle: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
- streakSubtitle: { ...TYPOGRAPHY.BODY, color: TEXT.TERTIARY, marginTop: SPACING[1] },
-
- shareSection: { paddingVertical: SPACING[4], alignItems: 'center' },
-
- bottomBar: {
- position: 'absolute',
- bottom: 0, left: 0, right: 0,
- paddingHorizontal: LAYOUT.SCREEN_PADDING,
- paddingTop: SPACING[4],
- backgroundColor: colors.bg.base,
- borderTopWidth: 1,
- borderTopColor: BORDER_COLORS.DIM,
- },
- homeButtonContainer: { height: 56, justifyContent: 'center' },
- })
-}
diff --git a/app/onboarding.tsx b/app/onboarding.tsx
deleted file mode 100644
index 2ed84ec..0000000
--- a/app/onboarding.tsx
+++ /dev/null
@@ -1,1341 +0,0 @@
-/**
- * TabataFit Onboarding — 6-Screen Conversion Funnel
- * Problem → Empathy → Solution → Wow Moment → Personalization → Paywall
- */
-
-import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
-import {
- View,
- StyleSheet,
- Pressable,
- Animated,
- Dimensions,
- ScrollView,
- TextInput,
-} from 'react-native'
-import { useRouter } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { Icon } from '@/src/shared/components/Icon'
-
-import { Alert } from 'react-native'
-import { useTranslation } from 'react-i18next'
-import { useHaptics, usePurchases } from '@/src/shared/hooks'
-import { useUserStore } from '@/src/shared/stores'
-import { OnboardingStep } from '@/src/shared/components/OnboardingStep'
-import { StyledText } from '@/src/shared/components/StyledText'
-
-import { useThemeColors } from '@/src/shared/theme'
-import type { ThemeColors } from '@/src/shared/theme/types'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { DURATION, EASE, SPRING } from '@/src/shared/constants/animations'
-import { track, identifyUser, setUserProperties, trackScreen } from '@/src/shared/services/analytics'
-import { GREEN, NAVY, BORDER_COLORS } from '@/src/shared/constants/colors'
-import { withOpacity } from '@/src/shared/utils/color'
-import { PHASE } from '@/src/shared/constants/colors'
-import { NativeButton } from '@/src/shared/components/native'
-
-import type { FitnessLevel, FitnessGoal, WeeklyFrequency } from '@/src/shared/types'
-
-const { width: SCREEN_WIDTH } = Dimensions.get('window')
-const TOTAL_STEPS = 6
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SCREEN 1 — THE PROBLEM
-// ═══════════════════════════════════════════════════════════════════════════
-
-function ProblemScreen({ onNext }: { onNext: () => void }) {
- const { t } = useTranslation('screens')
- const haptics = useHaptics()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
- const clockScale = useRef(new Animated.Value(0.8)).current
- const clockOpacity = useRef(new Animated.Value(0)).current
- const textOpacity = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- // Clock animation
- Animated.parallel([
- Animated.spring(clockScale, {
- toValue: 1,
- ...SPRING.BOUNCY,
- useNativeDriver: true,
- }),
- Animated.timing(clockOpacity, {
- toValue: 1,
- duration: DURATION.SLOW,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }),
- ]).start()
-
- // Text fade in after clock
- setTimeout(() => {
- Animated.timing(textOpacity, {
- toValue: 1,
- duration: DURATION.SLOW,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }).start()
- }, 400)
- }, [])
-
- return (
-
-
-
-
-
-
-
- {t('onboarding.problem.title')}
-
-
- {t('onboarding.problem.subtitle1')}
-
-
- {t('onboarding.problem.subtitle2')}
-
-
-
-
- {
- haptics.buttonTap()
- onNext()
- }}
- >
-
- {t('onboarding.problem.cta')}
-
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SCREEN 2 — EMPATHY
-// ═══════════════════════════════════════════════════════════════════════════
-
-const BARRIERS = [
- { id: 'no-time', labelKey: 'onboarding.empathy.noTime' as const, icon: 'clock' as const },
- { id: 'low-motivation', labelKey: 'onboarding.empathy.lowMotivation' as const, icon: 'battery.0percent' as const },
- { id: 'no-knowledge', labelKey: 'onboarding.empathy.noKnowledge' as const, icon: 'questionmark.circle' as const },
- { id: 'no-gym', labelKey: 'onboarding.empathy.noGym' as const, icon: 'house' as const },
-]
-
-function EmpathyScreen({
- onNext,
- barriers,
- setBarriers,
-}: {
- onNext: () => void
- barriers: string[]
- setBarriers: (b: string[]) => void
-}) {
- const { t } = useTranslation('screens')
- const haptics = useHaptics()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
-
- const toggleBarrier = (id: string) => {
- haptics.selection()
- if (barriers.includes(id)) {
- setBarriers(barriers.filter((b) => b !== id))
- } else if (barriers.length < 2) {
- setBarriers([...barriers, id])
- }
- }
-
- return (
-
-
- {t('onboarding.empathy.title')}
-
-
- {t('onboarding.empathy.chooseUpTo')}
-
-
-
- {BARRIERS.map((item) => {
- const selected = barriers.includes(item.id)
- return (
- toggleBarrier(item.id)}
- >
-
-
- {t(item.labelKey)}
-
-
- )
- })}
-
-
-
- {
- if (barriers.length > 0) {
- haptics.buttonTap()
- onNext()
- }
- }}
- >
- 0 ? NAVY[900] : colors.text.disabled}
- >
- {t('common:continue')}
-
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SCREEN 3 — THE SOLUTION (Scientific Proof)
-// ═══════════════════════════════════════════════════════════════════════════
-
-function SolutionScreen({ onNext }: { onNext: () => void }) {
- const { t } = useTranslation('screens')
- const haptics = useHaptics()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
- const tabataHeight = useRef(new Animated.Value(0)).current
- const cardioHeight = useRef(new Animated.Value(0)).current
- const citationOpacity = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- // Animate bars
- Animated.sequence([
- Animated.delay(300),
- Animated.parallel([
- Animated.spring(tabataHeight, {
- toValue: 1,
- ...SPRING.GENTLE,
- useNativeDriver: false,
- }),
- Animated.spring(cardioHeight, {
- toValue: 1,
- ...SPRING.GENTLE,
- useNativeDriver: false,
- }),
- ]),
- Animated.delay(200),
- Animated.timing(citationOpacity, {
- toValue: 1,
- duration: DURATION.SLOW,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }),
- ]).start()
- }, [])
-
- const MAX_BAR_HEIGHT = 160
-
- return (
-
-
- {t('onboarding.solution.title')}
-
-
- {/* Comparison bars */}
-
- {/* Tabata bar */}
-
-
- {t('onboarding.solution.tabataCalories')}
-
-
-
-
-
- {t('onboarding.solution.tabata')}
-
-
- {t('onboarding.solution.tabataDuration')}
-
-
-
- {/* VS */}
-
-
- {t('onboarding.solution.vs')}
-
-
-
- {/* Cardio bar */}
-
-
- {t('onboarding.solution.cardioCalories')}
-
-
-
-
-
- {t('onboarding.solution.cardio')}
-
-
- {t('onboarding.solution.cardioDuration')}
-
-
-
-
- {/* Citation */}
-
-
- {t('onboarding.solution.citation')}
-
-
- {t('onboarding.solution.citationAuthor')}
-
-
-
-
- {
- haptics.buttonTap()
- onNext()
- }}
- >
-
- {t('onboarding.solution.cta')}
-
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SCREEN 4 — WOW MOMENT (Staggered Feature Reveal)
-// ═══════════════════════════════════════════════════════════════════════════
-
-const WOW_FEATURES = [
- { icon: 'timer' as const, iconColor: GREEN[500], titleKey: 'onboarding.wow.card1Title', subtitleKey: 'onboarding.wow.card1Subtitle' },
- { icon: 'dumbbell' as const, iconColor: PHASE.REST, titleKey: 'onboarding.wow.card2Title', subtitleKey: 'onboarding.wow.card2Subtitle' },
- { icon: 'mic' as const, iconColor: PHASE.PREP, titleKey: 'onboarding.wow.card3Title', subtitleKey: 'onboarding.wow.card3Subtitle' },
- { icon: 'arrow.up.right' as const, iconColor: PHASE.COMPLETE, titleKey: 'onboarding.wow.card4Title', subtitleKey: 'onboarding.wow.card4Subtitle' },
-] as const
-
-function WowScreen({ onNext }: { onNext: () => void }) {
- const { t } = useTranslation('screens')
- const haptics = useHaptics()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
- const wowStyles = useMemo(() => createWowStyles(colors), [colors])
- const rowAnims = useRef(WOW_FEATURES.map(() => ({
- opacity: new Animated.Value(0),
- translateY: new Animated.Value(20),
- }))).current
- const ctaOpacity = useRef(new Animated.Value(0)).current
- const [ctaReady, setCtaReady] = useState(false)
-
- useEffect(() => {
- // Staggered reveal: each row fades in + slides up, 150ms apart, starting at 300ms
- const STAGGER_DELAY = 150
- const ROW_DURATION = DURATION.NORMAL // 300ms
- const START_DELAY = 300
-
- WOW_FEATURES.forEach((_, i) => {
- setTimeout(() => {
- Animated.parallel([
- Animated.timing(rowAnims[i].opacity, {
- toValue: 1,
- duration: ROW_DURATION,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }),
- Animated.timing(rowAnims[i].translateY, {
- toValue: 0,
- duration: ROW_DURATION,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }),
- ]).start()
- }, START_DELAY + i * STAGGER_DELAY)
- })
-
- // CTA fades in 200ms after last row finishes
- const ctaDelay = START_DELAY + (WOW_FEATURES.length - 1) * STAGGER_DELAY + ROW_DURATION + 200
- setTimeout(() => {
- setCtaReady(true)
- Animated.timing(ctaOpacity, {
- toValue: 1,
- duration: ROW_DURATION,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }).start()
- }, ctaDelay)
- }, [])
-
- return (
-
-
- {t('onboarding.wow.title')}
-
-
- {t('onboarding.wow.subtitle')}
-
-
- {/* Feature list */}
-
- {WOW_FEATURES.map((feature, i) => (
-
-
-
-
-
-
- {t(feature.titleKey)}
-
-
- {t(feature.subtitleKey)}
-
-
-
- ))}
-
-
- {/* CTA fades in after all rows */}
-
- {
- if (ctaReady) {
- haptics.buttonTap()
- onNext()
- }
- }}
- >
-
- {t('common:next')}
-
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SCREEN 5 — PERSONALIZATION
-// ═══════════════════════════════════════════════════════════════════════════
-
-const LEVELS: { value: FitnessLevel; labelKey: string }[] = [
- { value: 'beginner', labelKey: 'common:levels.beginner' },
- { value: 'intermediate', labelKey: 'common:levels.intermediate' },
- { value: 'advanced', labelKey: 'common:levels.advanced' },
-]
-
-const GOALS: { value: FitnessGoal; labelKey: string }[] = [
- { value: 'weight-loss', labelKey: 'onboarding.personalization.goals.weightLoss' },
- { value: 'cardio', labelKey: 'onboarding.personalization.goals.cardio' },
- { value: 'strength', labelKey: 'onboarding.personalization.goals.strength' },
- { value: 'wellness', labelKey: 'onboarding.personalization.goals.wellness' },
-]
-
-const FREQUENCIES: { value: WeeklyFrequency; labelKey: string }[] = [
- { value: 2, labelKey: 'onboarding.personalization.frequencies.2x' },
- { value: 3, labelKey: 'onboarding.personalization.frequencies.3x' },
- { value: 5, labelKey: 'onboarding.personalization.frequencies.5x' },
-]
-
-function PersonalizationScreen({
- onNext,
- name,
- setName,
- level,
- setLevel,
- goal,
- setGoal,
- frequency,
- setFrequency,
-}: {
- onNext: () => void
- name: string
- setName: (n: string) => void
- level: FitnessLevel
- setLevel: (l: FitnessLevel) => void
- goal: FitnessGoal
- setGoal: (g: FitnessGoal) => void
- frequency: WeeklyFrequency
- setFrequency: (f: WeeklyFrequency) => void
-}) {
- const { t } = useTranslation('screens')
- const haptics = useHaptics()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
-
- return (
-
-
- {t('onboarding.personalization.title')}
-
-
- {/* Name input */}
-
-
- {t('onboarding.personalization.yourName')}
-
-
-
-
- {/* Fitness Level */}
-
-
- {t('onboarding.personalization.fitnessLevel')}
-
-
- {LEVELS.map((item) => (
- {
- haptics.selection()
- setLevel(item.value)
- }}
- >
-
- {t(item.labelKey)}
-
-
- ))}
-
-
-
- {/* Goal */}
-
-
- {t('onboarding.personalization.yourGoal')}
-
-
- {GOALS.map((item) => (
- {
- haptics.selection()
- setGoal(item.value)
- }}
- >
-
- {t(item.labelKey)}
-
-
- ))}
-
-
-
- {/* Frequency */}
-
-
- {t('onboarding.personalization.weeklyFrequency')}
-
-
- {FREQUENCIES.map((item) => (
- {
- haptics.selection()
- setFrequency(item.value)
- }}
- >
-
- {t(item.labelKey)}
-
-
- ))}
-
-
-
- {name.trim().length > 0 && (
-
- {t('onboarding.personalization.readyMessage')}
-
- )}
-
-
- {
- if (name.trim()) {
- haptics.buttonTap()
- onNext()
- }
- }}
- >
-
- {t('common:continue')}
-
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SCREEN 6 — PAYWALL
-// ═══════════════════════════════════════════════════════════════════════════
-
-const PREMIUM_FEATURE_KEYS = [
- 'onboarding.paywall.features.unlimited',
- 'onboarding.paywall.features.offline',
- 'onboarding.paywall.features.stats',
- 'onboarding.paywall.features.noAds',
-] as const
-
-function PaywallScreen({
- onSubscribe,
- onSkip,
-}: {
- onSubscribe: (plan: 'premium-monthly' | 'premium-yearly') => void
- onSkip: () => void
-}) {
- const { t } = useTranslation('screens')
- const haptics = useHaptics()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
- const {
- isLoading,
- monthlyPackage,
- annualPackage,
- purchasePackage,
- restorePurchases,
- } = usePurchases()
-
- const [selectedPlan, setSelectedPlan] = useState<'premium-monthly' | 'premium-yearly'>('premium-yearly')
- const [isPurchasing, setIsPurchasing] = useState(false)
- const featureAnims = useRef(PREMIUM_FEATURE_KEYS.map(() => new Animated.Value(0))).current
-
- const handlePlanSelect = (plan: 'premium-monthly' | 'premium-yearly') => {
- haptics.selection()
- setSelectedPlan(plan)
- track('onboarding_paywall_plan_selected', { plan })
- }
-
- useEffect(() => {
- // Staggered feature fade-in
- PREMIUM_FEATURE_KEYS.forEach((_, i) => {
- setTimeout(() => {
- Animated.timing(featureAnims[i], {
- toValue: 1,
- duration: DURATION.NORMAL,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }).start()
- }, i * 100)
- })
- }, [])
-
- // Get localized prices from RevenueCat packages
- const yearlyPrice = annualPackage?.product.priceString ?? t('onboarding.paywall.yearlyPrice')
- const monthlyPrice = monthlyPackage?.product.priceString ?? t('onboarding.paywall.monthlyPrice')
-
- const handlePurchase = async () => {
- if (isPurchasing) return
-
- const pkg = selectedPlan === 'premium-yearly' ? annualPackage : monthlyPackage
- const price = selectedPlan === 'premium-yearly'
- ? (annualPackage?.product.priceString ?? t('onboarding.paywall.yearlyPrice'))
- : (monthlyPackage?.product.priceString ?? t('onboarding.paywall.monthlyPrice'))
-
- track('onboarding_paywall_purchase_tapped', { plan: selectedPlan, price })
-
- // DEV mode: if RevenueCat hasn't loaded or has no packages, show simulated purchase dialog
- if (__DEV__ && (isLoading || !pkg)) {
- haptics.buttonTap()
- const planLabel = selectedPlan === 'premium-yearly'
- ? `Annual (${t('onboarding.paywall.yearlyPrice')})`
- : `Monthly (${t('onboarding.paywall.monthlyPrice')})`
- Alert.alert(
- 'Confirm Subscription',
- `Subscribe to TabataFit+ ${planLabel}?\n\nThis is a sandbox purchase — no real charge.`,
- [
- { text: 'Cancel', style: 'cancel' },
- {
- text: 'Subscribe',
- onPress: () => {
- track('onboarding_paywall_purchase_success', { plan: selectedPlan })
- onSubscribe(selectedPlan)
- },
- },
- ]
- )
- return
- }
-
- if (isLoading || !pkg) return
-
- setIsPurchasing(true)
- haptics.buttonTap()
-
- try {
- const result = await purchasePackage(pkg)
- if (result.success) {
- track('onboarding_paywall_purchase_success', { plan: selectedPlan })
- onSubscribe(selectedPlan)
- }
- } finally {
- setIsPurchasing(false)
- }
- }
-
- const handleRestore = async () => {
- haptics.buttonTap()
- const restored = await restorePurchases()
- track('onboarding_paywall_restored', { success: !!restored })
- if (restored) {
- // User has premium now, complete onboarding
- onSubscribe('premium-yearly')
- }
- }
-
- return (
-
-
- {t('onboarding.paywall.title')}
-
-
- {/* Features */}
-
- {PREMIUM_FEATURE_KEYS.map((featureKey, i) => (
-
-
-
- {t(featureKey)}
-
-
- ))}
-
-
- {/* Pricing cards */}
-
- {/* Annual */}
- handlePlanSelect('premium-yearly')}
- >
-
-
- {t('onboarding.paywall.bestValue')}
-
-
-
- {yearlyPrice}
-
-
- {t('common:units.perYear')}
-
-
- {t('onboarding.paywall.savePercent')}
-
-
-
- {/* Monthly */}
- handlePlanSelect('premium-monthly')}
- >
-
- {monthlyPrice}
-
-
- {t('common:units.perMonth')}
-
-
-
-
- {/* CTA */}
-
-
- {isPurchasing ? '...' : t('onboarding.paywall.trialCta')}
-
-
-
- {/* Guarantees */}
-
-
- {t('onboarding.paywall.guarantees')}
-
-
-
- {/* Restore Purchases */}
-
-
- {t('onboarding.paywall.restorePurchases')}
-
-
-
- {/* Skip */}
- {
- track('onboarding_paywall_skipped')
- onSkip()
- }}
- >
-
- {t('onboarding.paywall.skipButton')}
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// MAIN ONBOARDING CONTROLLER
-// ═══════════════════════════════════════════════════════════════════════════
-
-const STEP_NAMES: Record = {
- 1: 'problem',
- 2: 'empathy',
- 3: 'solution',
- 4: 'wow',
- 5: 'personalization',
- 6: 'paywall',
-}
-
-export default function OnboardingScreen() {
- const router = useRouter()
- const [step, setStep] = useState(1)
-
- // Personalization state
- const [barriers, setBarriers] = useState([])
- const [name, setName] = useState('')
- const [level, setLevel] = useState('beginner')
- const [goal, setGoal] = useState('cardio')
- const [frequency, setFrequency] = useState(3)
-
- const completeOnboarding = useUserStore((s) => s.completeOnboarding)
- const setSubscription = useUserStore((s) => s.setSubscription)
-
- // Analytics: track time per step and total onboarding time
- const onboardingStartTime = useRef(Date.now())
- const stepStartTime = useRef(Date.now())
-
- // Track onboarding_started + first step viewed on mount
- useEffect(() => {
- trackScreen('onboarding')
- track('onboarding_started')
- track('onboarding_step_viewed', { step: 1, step_name: STEP_NAMES[1] })
- }, [])
-
- const finishOnboarding = useCallback(
- (plan: 'free' | 'premium-monthly' | 'premium-yearly') => {
- const totalTime = Date.now() - onboardingStartTime.current
-
- track('onboarding_completed', {
- plan,
- total_time_ms: totalTime,
- steps_completed: step,
- })
-
- const userData = {
- name: name.trim() || 'Athlete',
- fitnessLevel: level,
- goal,
- weeklyFrequency: frequency,
- barriers,
- }
-
- completeOnboarding(userData)
-
- // Identify user in PostHog for session replay linking
- const userId = `user_${Date.now()}` // In production, use actual user ID from backend
- identifyUser(userId, {
- name: userData.name,
- fitness_level: level,
- fitness_goal: goal,
- weekly_frequency: frequency,
- subscription_plan: plan,
- onboarding_completed_at: new Date().toISOString(),
- barriers: barriers.join(','),
- })
-
- if (plan !== 'free') {
- setSubscription(plan)
- }
- router.replace('/')
- },
- [name, level, goal, frequency, barriers, step]
- )
-
- const nextStep = useCallback(() => {
- const now = Date.now()
- const timeOnStep = now - stepStartTime.current
-
- // Track step completed
- track('onboarding_step_completed', {
- step,
- step_name: STEP_NAMES[step],
- time_on_step_ms: timeOnStep,
- })
-
- // Track specific step data
- if (step === 2) {
- track('onboarding_barriers_selected', {
- barriers,
- barrier_count: barriers.length,
- })
- }
- if (step === 5) {
- track('onboarding_personalization_completed', {
- name_provided: name.trim().length > 0,
- level,
- goal,
- frequency,
- })
- }
-
- const next = Math.min(step + 1, TOTAL_STEPS)
- stepStartTime.current = now
-
- // Track next step viewed
- track('onboarding_step_viewed', { step: next, step_name: STEP_NAMES[next] })
-
- setStep(next)
- }, [step, barriers, name, level, goal, frequency])
-
- const prevStep = useCallback(() => {
- if (step > 1) {
- const prev = step - 1
- stepStartTime.current = Date.now()
- track('onboarding_step_back', { from_step: step, to_step: prev })
- setStep(prev)
- }
- }, [step])
-
- const renderStep = () => {
- switch (step) {
- case 1:
- return
- case 2:
- return (
-
- )
- case 3:
- return
- case 4:
- return
- case 5:
- return (
-
- )
- case 6:
- return (
- finishOnboarding(plan)}
- onSkip={() => finishOnboarding('free')}
- />
- )
- default:
- return null
- }
- }
-
- return (
-
- {renderStep()}
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// STYLES
-// ═══════════════════════════════════════════════════════════════════════════
-
-function createStyles(colors: ThemeColors) {
- return StyleSheet.create({
- // Layout helpers
- screenCenter: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- screenFull: {
- flex: 1,
- },
- titleCenter: {
- textAlign: 'center',
- },
- subtitle: {
- textAlign: 'center',
- },
- bottomAction: {
- position: 'absolute',
- bottom: SPACING[4],
- left: 0,
- right: 0,
- },
-
- // CTA Button
- ctaButton: {
- height: LAYOUT.BUTTON_HEIGHT,
- backgroundColor: GREEN[500],
- borderRadius: RADIUS.MD,
- alignItems: 'center',
- justifyContent: 'center',
- },
- ctaButtonDisabled: {
- backgroundColor: colors.bg.elevated,
- },
-
- // ── Screen 2: Barriers ──
- barrierGrid: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: SPACING[3],
- marginTop: SPACING[8],
- justifyContent: 'center',
- },
- barrierCard: {
- width: (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2,
- paddingVertical: SPACING[6],
- alignItems: 'center',
- borderRadius: RADIUS.LG,
- backgroundColor: NAVY[800],
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- },
- barrierCardSelected: {
- borderColor: GREEN.BORDER,
- backgroundColor: GREEN.DIM,
- },
-
- // ── Screen 3: Comparison ──
- comparisonContainer: {
- flexDirection: 'row',
- justifyContent: 'center',
- alignItems: 'flex-end',
- marginTop: SPACING[10],
- paddingHorizontal: SPACING[8],
- gap: SPACING[4],
- },
- barColumn: {
- alignItems: 'center',
- flex: 1,
- },
- barTrack: {
- width: 60,
- height: 160,
- backgroundColor: colors.bg.overlay1,
- borderRadius: RADIUS.SM,
- overflow: 'hidden',
- marginVertical: SPACING[3],
- justifyContent: 'flex-end',
- },
- barFill: {
- width: '100%',
- borderRadius: RADIUS.SM,
- },
- barTabata: {
- backgroundColor: GREEN[500],
- },
- barCardio: {
- backgroundColor: PHASE.REST,
- },
- vsContainer: {
- paddingBottom: 80,
- },
- citation: {
- marginTop: SPACING[8],
- paddingHorizontal: SPACING[4],
- },
- citationText: {
- textAlign: 'center',
- fontStyle: 'italic',
- lineHeight: 20,
- },
- citationAuthor: {
- textAlign: 'center',
- marginTop: SPACING[2],
- },
-
- // ── Screen 5: Personalization ──
- personalizationContent: {
- paddingBottom: SPACING[10],
- },
- fieldGroup: {
- marginTop: SPACING[6],
- },
- fieldLabel: {
- letterSpacing: 1.5,
- marginBottom: SPACING[2],
- },
- textInput: {
- height: LAYOUT.BUTTON_HEIGHT_SM,
- backgroundColor: colors.bg.surface,
- borderRadius: RADIUS.MD,
- paddingHorizontal: SPACING[4],
- color: colors.text.primary,
- fontSize: 17,
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- },
- segmentRow: {
- flexDirection: 'row',
- backgroundColor: colors.bg.surface,
- borderRadius: RADIUS.MD,
- padding: 3,
- gap: 2,
- },
- segmentButton: {
- flex: 1,
- height: 36,
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: RADIUS.SM,
- },
- segmentButtonActive: {
- backgroundColor: colors.bg.elevated,
- },
- readyMessage: {
- textAlign: 'center',
- marginTop: SPACING[6],
- },
-
- // ── Screen 6: Paywall ──
- paywallContent: {
- paddingBottom: SPACING[10],
- },
- featuresList: {
- marginTop: SPACING[8],
- gap: SPACING[4],
- },
- featureRow: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- pricingCards: {
- flexDirection: 'row',
- gap: SPACING[3],
- marginTop: SPACING[8],
- },
- pricingCard: {
- flex: 1,
- paddingVertical: SPACING[5],
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: RADIUS.LG,
- backgroundColor: NAVY[800],
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- },
- pricingCardSelected: {
- borderColor: GREEN.BORDER,
- borderWidth: 2,
- backgroundColor: GREEN.DIM,
- },
- bestValueBadge: {
- backgroundColor: GREEN[500],
- paddingHorizontal: SPACING[3],
- paddingVertical: SPACING[1],
- borderRadius: RADIUS.SM,
- marginBottom: SPACING[2],
- },
- trialButton: {
- height: LAYOUT.BUTTON_HEIGHT,
- backgroundColor: GREEN[500],
- borderRadius: RADIUS.MD,
- alignItems: 'center',
- justifyContent: 'center',
- marginTop: SPACING[6],
- },
- guarantees: {
- alignItems: 'center',
- marginTop: SPACING[4],
- },
- restoreButton: {
- alignItems: 'center',
- paddingVertical: SPACING[3],
- },
- skipButton: {
- alignItems: 'center',
- paddingVertical: SPACING[5],
- marginTop: SPACING[2],
- },
- })
-}
-
-// ── Screen 4: Feature List Styles ──
-function createWowStyles(colors: ThemeColors) {
- return StyleSheet.create({
- list: {
- gap: SPACING[5],
- marginTop: SPACING[4],
- },
- row: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[4],
- },
- iconCircle: {
- width: 44,
- height: 44,
- borderRadius: RADIUS.FULL,
- alignItems: 'center',
- justifyContent: 'center',
- },
- textCol: {
- flex: 1,
- },
- })
-}
diff --git a/app/paywall.tsx b/app/paywall.tsx
deleted file mode 100644
index 0f5fc5a..0000000
--- a/app/paywall.tsx
+++ /dev/null
@@ -1,460 +0,0 @@
-/**
- * TabataFit Paywall Screen
- * Premium subscription purchase flow
- */
-
-import React, { useMemo } from 'react'
-import {
- View,
- StyleSheet,
- ScrollView,
- Pressable,
-} from 'react-native'
-import { useRouter } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { Icon, type IconName } from '@/src/shared/components/Icon'
-
-import { useTranslation } from 'react-i18next'
-import { useHaptics, usePurchases } from '@/src/shared/hooks'
-import { StyledText } from '@/src/shared/components/StyledText'
-import { useThemeColors } from '@/src/shared/theme'
-import { NativeButton } from '@/src/shared/components/native'
-import type { ThemeColors } from '@/src/shared/theme/types'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { GREEN, NAVY, BORDER_COLORS } from '@/src/shared/constants/colors'
-
-// ═══════════════════════════════════════════════════════════════════════════
-// FEATURES LIST
-// ═══════════════════════════════════════════════════════════════════════════
-
-const PREMIUM_FEATURES: { icon: IconName; key: string }[] = [
- { icon: 'music.note.list', key: 'music' },
- { icon: 'infinity', key: 'workouts' },
- { icon: 'chart.bar.fill', key: 'stats' },
- { icon: 'flame.fill', key: 'calories' },
- { icon: 'bell.fill', key: 'reminders' },
- { icon: 'xmark.circle.fill', key: 'ads' },
-]
-
-// ═══════════════════════════════════════════════════════════════════════════
-// COMPONENTS
-// ═══════════════════════════════════════════════════════════════════════════
-
-interface PlanCardStyles {
- planCard: object
- planCardPressed: object
- savingsBadge: object
- savingsText: object
- planInfo: object
- planTitle: object
- planPeriod: object
- planPrice: object
- checkmark: object
-}
-
-function PlanCard({
- title,
- price,
- period,
- savings,
- isSelected,
- onPress,
- colors,
- styles,
-}: {
- title: string
- price: string
- period: string
- savings?: string
- isSelected: boolean
- onPress: () => void
- colors: ThemeColors
- styles: PlanCardStyles
-}) {
- const haptics = useHaptics()
-
- const handlePress = () => {
- haptics.selection()
- onPress()
- }
-
- return (
- [
- styles.planCard,
- isSelected && { borderColor: GREEN.BORDER },
- pressed && styles.planCardPressed,
- {
- backgroundColor: colors.bg.surface,
- borderColor: isSelected ? GREEN.BORDER : BORDER_COLORS.DIM,
- },
- ]}
- >
- {savings && (
-
- {savings}
-
- )}
-
-
- {title}
-
-
- {period}
-
-
-
- {price}
-
- {isSelected && (
-
-
-
- )}
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// MAIN SCREEN
-// ═══════════════════════════════════════════════════════════════════════════
-
-export default function PaywallScreen() {
- const { t } = useTranslation('screens')
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const haptics = useHaptics()
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
-
- // Extract plan card styles for the child component
- const planCardStyles = useMemo(
- () => ({
- planCard: styles.planCard,
- planCardPressed: styles.planCardPressed,
- savingsBadge: styles.savingsBadge,
- savingsText: styles.savingsText,
- planInfo: styles.planInfo,
- planTitle: styles.planTitle,
- planPeriod: styles.planPeriod,
- planPrice: styles.planPrice,
- checkmark: styles.checkmark,
- }),
- [styles],
- )
-
- const {
- monthlyPackage,
- annualPackage,
- purchasePackage,
- restorePurchases,
- isLoading,
- } = usePurchases()
-
- const [selectedPlan, setSelectedPlan] = React.useState<'monthly' | 'annual'>('annual')
-
- // Get prices from RevenueCat packages
- const monthlyPrice = monthlyPackage?.product.priceString ?? '$4.99'
- const annualPrice = annualPackage?.product.priceString ?? '$29.99'
- const annualMonthlyEquivalent = annualPackage
- ? (annualPackage.product.price / 12).toFixed(2)
- : '2.49'
-
- const handlePurchase = async () => {
- haptics.buttonTap()
- const pkg = selectedPlan === 'monthly' ? monthlyPackage : annualPackage
- if (!pkg) {
- console.log('[Paywall] No package available for purchase')
- return
- }
-
- const result = await purchasePackage(pkg)
- if (result.success) {
- haptics.workoutComplete()
- router.back()
- } else if (!result.cancelled) {
- console.log('[Paywall] Purchase error:', result.error)
- }
- }
-
- const handleRestore = async () => {
- haptics.selection()
- const restored = await restorePurchases()
- if (restored) {
- haptics.workoutComplete()
- router.back()
- }
- }
-
- const handleClose = () => {
- haptics.selection()
- router.back()
- }
-
- return (
-
- {/* Close Button */}
-
-
-
-
-
- {/* Header */}
-
-
- TabataFit+
-
-
- {t('paywall.subtitle')}
-
-
-
- {/* Features Grid */}
-
- {PREMIUM_FEATURES.map((feature) => (
-
-
-
-
-
- {t(`paywall.features.${feature.key}`)}
-
-
- ))}
-
-
- {/* Plan Selection */}
-
- setSelectedPlan('annual')}
- colors={colors}
- styles={planCardStyles}
- />
- setSelectedPlan('monthly')}
- colors={colors}
- styles={planCardStyles}
- />
-
-
- {/* Price Note */}
- {selectedPlan === 'annual' && (
-
- {t('paywall.equivalent', { price: annualMonthlyEquivalent })}
-
- )}
-
- {/* CTA Button */}
-
-
- {/* Restore & Terms */}
-
-
-
-
- router.push('/terms')}>
-
- {t('paywall.termsLink')}
-
-
- ·
- router.push('/terms')}>
-
- {t('paywall.privacyLink')}
-
-
-
-
-
- {t('paywall.terms')}
-
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// STYLES
-// ═══════════════════════════════════════════════════════════════════════════
-
-function createStyles(colors: ThemeColors) {
- return StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: colors.bg.base,
- },
- closeButton: {
- position: 'absolute',
- top: SPACING[4],
- right: SPACING[4],
- width: 44,
- height: 44,
- alignItems: 'center',
- justifyContent: 'center',
- zIndex: 10,
- },
- scrollView: {
- flex: 1,
- },
- scrollContent: {
- paddingHorizontal: SPACING[5],
- paddingTop: SPACING[8],
- },
- header: {
- alignItems: 'center',
- },
- title: {
- fontSize: 32,
- fontWeight: '700',
- color: colors.text.primary,
- textAlign: 'center',
- },
- subtitle: {
- fontSize: 16,
- color: colors.text.secondary,
- textAlign: 'center',
- marginTop: SPACING[2],
- },
- featuresGrid: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- marginTop: SPACING[6],
- marginHorizontal: -SPACING[2],
- },
- featureItem: {
- width: '33%',
- alignItems: 'center',
- paddingVertical: SPACING[3],
- },
- featureIcon: {
- width: 48,
- height: 48,
- borderRadius: 24,
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: SPACING[2],
- },
- featureText: {
- fontSize: 13,
- textAlign: 'center',
- },
- plansContainer: {
- marginTop: SPACING[6],
- gap: SPACING[3],
- },
- planCard: {
- flexDirection: 'row',
- alignItems: 'center',
- borderRadius: RADIUS.LG,
- padding: SPACING[4],
- borderWidth: 2,
- },
- planCardPressed: {
- opacity: 0.8,
- },
- savingsBadge: {
- position: 'absolute',
- top: -8,
- right: SPACING[3],
- backgroundColor: GREEN[500],
- paddingHorizontal: SPACING[2],
- paddingVertical: 2,
- borderRadius: RADIUS.SM,
- },
- savingsText: {
- fontSize: 10,
- fontWeight: '700',
- color: colors.text.primary,
- },
- planInfo: {
- flex: 1,
- },
- planTitle: {
- fontSize: 16,
- fontWeight: '600',
- },
- planPeriod: {
- fontSize: 13,
- marginTop: 2,
- },
- planPrice: {
- fontSize: 20,
- fontWeight: '700',
- },
- checkmark: {
- marginLeft: SPACING[2],
- },
- priceNote: {
- fontSize: 13,
- textAlign: 'center',
- marginTop: SPACING[3],
- },
- ctaButton: {
- borderRadius: RADIUS.LG,
- marginTop: SPACING[6],
- paddingVertical: SPACING[4],
- alignItems: 'center',
- backgroundColor: GREEN[500],
- },
- ctaButtonDisabled: {
- opacity: 0.6,
- },
- ctaText: {
- fontSize: 17,
- fontWeight: '600',
- },
- footer: {
- marginTop: SPACING[5],
- alignItems: 'center',
- gap: SPACING[4],
- },
- restoreText: {
- fontSize: 14,
- },
- termsText: {
- fontSize: 11,
- textAlign: 'center',
- lineHeight: 18,
- paddingHorizontal: SPACING[4],
- },
- legalLinks: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[1],
- },
- })
-}
diff --git a/app/player/CLAUDE.md b/app/player/CLAUDE.md
deleted file mode 100644
index ab2a248..0000000
--- a/app/player/CLAUDE.md
+++ /dev/null
@@ -1,27 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 19, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5000 | 9:35 AM | 🔵 | Reviewed Player Screen Implementation | ~522 |
-| #4912 | 8:16 AM | 🔵 | Found doneButton component in player screen | ~104 |
-
-### Apr 9, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5997 | 10:46 AM | 🟣 | Tabata Kine programs system fully implemented with four programs and specialized UI | ~563 |
-| #5996 | 10:41 AM | 🟣 | Tabata Kine programs system implementation completed | ~460 |
-| #5975 | 9:43 AM | 🟣 | Player screen updated to support kiné session detection and routing | ~316 |
-
-### Apr 10, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6005 | 10:02 AM | 🔵 | Player Screen Routing Between Kine and Legacy Workouts | ~335 |
-| #5998 | 9:52 AM | 🟣 | Tabata Kine programs system implementation completed | ~711 |
-
\ No newline at end of file
diff --git a/app/player/[id].tsx b/app/player/[id].tsx
deleted file mode 100644
index 30f52d1..0000000
--- a/app/player/[id].tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * TabataFit Player Screen
- * Loads a WorkoutProgram from Supabase and renders the Tabata player.
- */
-
-import React from 'react'
-import { View, Text } from 'react-native'
-import { useLocalSearchParams } from 'expo-router'
-import {
- isWorkoutProgramId,
- parseWorkoutProgramId,
- fetchProgramById,
- workoutProgramToTabataSession,
-} from '@/src/shared/data/workoutPrograms'
-import { TabataPlayerScreen } from '@/src/features/player/TabataPlayerScreen'
-import type { TabataSession } from '@/src/shared/types/program'
-import type { WorkoutProgram } from '@/src/shared/types/workoutProgram'
-import { NAVY, TEXT } from '@/src/shared/constants/colors'
-
-export default function PlayerScreen() {
- const { id } = useLocalSearchParams<{ id: string }>()
- const sessionId = id ?? ''
-
- if (!isWorkoutProgramId(sessionId)) {
- return
- }
-
- return
-}
-
-function WorkoutProgramPlayerScreen({ compositeId }: { compositeId: string }) {
- const [state, setState] = React.useState<
- | { status: 'loading' }
- | { status: 'error' }
- | { status: 'ready'; session: TabataSession; program: WorkoutProgram }
- >({ status: 'loading' })
-
- React.useEffect(() => {
- let cancelled = false
- async function load() {
- const parsed = parseWorkoutProgramId(compositeId)
- if (!parsed) {
- if (!cancelled) setState({ status: 'error' })
- return
- }
- const program = await fetchProgramById(parsed.programId)
- if (cancelled) return
- if (!program) {
- setState({ status: 'error' })
- return
- }
- setState({
- status: 'ready',
- session: workoutProgramToTabataSession(program),
- program,
- })
- }
- load()
- return () => {
- cancelled = true
- }
- }, [compositeId])
-
- if (state.status === 'loading') return
- if (state.status === 'error') return
- return
-}
-
-function Message({ text }: { text: string }) {
- return (
-
- {text}
-
- )
-}
diff --git a/app/privacy.tsx b/app/privacy.tsx
deleted file mode 100644
index 9f3d213..0000000
--- a/app/privacy.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-/**
- * TabataFit Privacy Policy Screen
- * Required for App Store submission
- */
-
-import React from 'react'
-import { View, ScrollView, StyleSheet, Text, Pressable } from 'react-native'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useRouter } from 'expo-router'
-import { Icon } from '@/src/shared/components/Icon'
-
-import { useTranslation } from 'react-i18next'
-import { useHaptics } from '@/src/shared/hooks'
-import { darkColors, BRAND } from '@/src/shared/theme'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-
-export default function PrivacyPolicyScreen() {
- const { t } = useTranslation('screens')
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const haptics = useHaptics()
-
- const handleClose = () => {
- haptics.selection()
- router.back()
- }
-
- return (
-
- {/* Header */}
-
-
-
-
- {t('privacy.title')}
-
-
-
-
-
-
-
- {t('privacy.intro.content')}
-
-
-
- {t('privacy.dataCollection.content')}
-
-
-
-
- {t('privacy.usage.content')}
-
-
-
- {t('privacy.sharing.content')}
-
-
-
- {t('privacy.security.content')}
-
-
-
- {t('privacy.rights.content')}
-
-
-
- {t('privacy.contact.content')}
- privacy@tabatafit.app
-
-
-
-
- TabataFit v1.0.0
-
-
-
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// HELPER COMPONENTS
-// ═══════════════════════════════════════════════════════════════════════════
-
-function Section({
- title,
- children,
-}: {
- title: string
- children?: React.ReactNode
-}) {
- return (
-
- {title}
- {children}
-
- )
-}
-
-function Paragraph({ children }: { children: string }) {
- return {children}
-}
-
-function BulletList({ items }: { items: string[] }) {
- return (
-
- {items.map((item, index) => (
-
- •
- {item}
-
- ))}
-
- )
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// STYLES
-// ═══════════════════════════════════════════════════════════════════════════
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: darkColors.bg.base,
- },
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: SPACING[4],
- paddingVertical: SPACING[3],
- borderBottomWidth: 1,
- borderBottomColor: darkColors.border.dim,
- },
- backButton: {
- width: 44,
- height: 44,
- alignItems: 'center',
- justifyContent: 'center',
- },
- headerTitle: {
- ...TYPOGRAPHY.HEADLINE,
- color: darkColors.text.primary,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: SPACING[5],
- paddingTop: SPACING[4],
- },
- section: {
- marginBottom: SPACING[6],
- },
- sectionTitle: {
- ...TYPOGRAPHY.TITLE_3,
- fontWeight: '700',
- color: darkColors.text.primary,
- marginBottom: SPACING[3],
- },
- paragraph: {
- ...TYPOGRAPHY.BODY,
- lineHeight: 22,
- color: darkColors.text.secondary,
- },
- bulletList: {
- marginTop: SPACING[3],
- },
- bulletItem: {
- flexDirection: 'row',
- marginBottom: SPACING[2],
- },
- bullet: {
- ...TYPOGRAPHY.BODY,
- color: BRAND.PRIMARY,
- marginRight: SPACING[2],
- },
- bulletText: {
- flex: 1,
- ...TYPOGRAPHY.BODY,
- lineHeight: 22,
- color: darkColors.text.secondary,
- },
- email: {
- ...TYPOGRAPHY.BODY,
- color: BRAND.PRIMARY,
- marginTop: SPACING[2],
- },
- footer: {
- marginTop: SPACING[8],
- alignItems: 'center',
- },
- footerText: {
- fontSize: 13,
- color: darkColors.text.tertiary,
- },
-})
diff --git a/app/program/CLAUDE.md b/app/program/CLAUDE.md
deleted file mode 100644
index f4ebf54..0000000
--- a/app/program/CLAUDE.md
+++ /dev/null
@@ -1,28 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 9, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5997 | 10:46 AM | 🟣 | Tabata Kine programs system fully implemented with four programs and specialized UI | ~563 |
-| #5996 | 10:41 AM | 🟣 | Tabata Kine programs system implementation completed | ~460 |
-| #5978 | 9:53 AM | 🟣 | Kine program detail screen implemented | ~452 |
-
-### Apr 10, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6027 | 10:08 AM | 🔵 | Program Detail Screen Re-Referenced for Kine Program Display | ~458 |
-| #6004 | 10:02 AM | 🔵 | Kine Program Detail Screen Architecture | ~337 |
-| #5998 | 9:52 AM | 🟣 | Tabata Kine programs system implementation completed | ~711 |
-
-### Apr 11, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6150 | 7:49 PM | 🔵 | Program detail unlock button contains hardcoded orange | ~253 |
-| #6134 | 7:43 PM | 🔄 | Program detail screen added withOpacity import | ~237 |
-
\ No newline at end of file
diff --git a/app/program/[id].tsx b/app/program/[id].tsx
deleted file mode 100644
index bdcb43e..0000000
--- a/app/program/[id].tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-/**
- * Workout Program Detail Screen
- * Shows Warmup → 3 Tabatas → Stretch preview, CTA to player.
- */
-
-import { useEffect, useState } from 'react'
-import { View, Text, StyleSheet, ScrollView, Pressable, ActivityIndicator } from 'react-native'
-import { Stack, useRouter, useLocalSearchParams } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useTranslation } from 'react-i18next'
-
-import { Icon } from '@/src/shared/components/Icon'
-import { fetchProgramById, buildWorkoutProgramId } from '@/src/shared/data/workoutPrograms'
-import type { WorkoutProgram } from '@/src/shared/types/workoutProgram'
-import { BODY_ZONE_META, LEVEL_META } from '@/src/shared/types/workoutProgram'
-import { useUserStore } from '@/src/shared/stores/userStore'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { TEXT, NAVY, GREEN, BORDER_COLORS, DARK } from '@/src/shared/constants/colors'
-import { withOpacity } from '@/src/shared/utils/color'
-
-const FALLBACK_ACCENT = '#FF6B35'
-
-export default function WorkoutProgramDetailScreen() {
- const { id } = useLocalSearchParams<{ id: string }>()
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const { t } = useTranslation()
-
- const [program, setProgram] = useState(null)
- const [loading, setLoading] = useState(true)
-
- const isPremium = useUserStore(s => s.profile.subscription) !== 'free'
-
- useEffect(() => {
- let cancelled = false
- setLoading(true)
- fetchProgramById(id)
- .then(p => {
- if (!cancelled) setProgram(p)
- })
- .finally(() => {
- if (!cancelled) setLoading(false)
- })
- return () => {
- cancelled = true
- }
- }, [id])
-
- if (loading) {
- return (
-
-
-
-
- )
- }
-
- if (!program) {
- return (
-
-
- {t('screens:program.notFound')}
-
- )
- }
-
- const accent = program.accentColor ?? BODY_ZONE_META[program.bodyZone].color ?? FALLBACK_ACCENT
- const level = LEVEL_META[program.level]
- const zone = BODY_ZONE_META[program.bodyZone]
- const canAccess = program.isFree || isPremium
-
- const handleStart = () => {
- if (!canAccess) {
- router.push('/paywall')
- return
- }
- router.push(`/player/${buildWorkoutProgramId(program.id)}`)
- }
-
- const warmupMinutes = Math.round(program.warmup.totalDuration / 60)
- const stretchMinutes = Math.round(program.stretch.totalDuration / 60)
-
- return (
-
-
-
-
- {/* Hero */}
-
-
-
-
- {program.title}
- {program.description && {program.description}}
-
-
-
- {level.label}
-
-
- {zone.label}
-
- {!program.isFree && (
-
- {t('screens:home.premiumBadge')}
-
- )}
-
-
-
-
-
-
-
-
-
- {/* Warmup */}
-
- {program.warmup.exercises.map((ex, i) => (
-
- ))}
-
-
- {/* Tabatas */}
- {program.tabatas.map((tabata, i) => (
-
- ))}
-
- {/* Stretch */}
-
- {program.stretch.exercises.map((ex, i) => (
-
- ))}
-
-
-
-
-
-
- {canAccess ? t('screens:program.startSession') : t('screens:program.unlockPremium')}
-
-
-
-
- )
-}
-
-function Stat({ value, label }: { value: number; label: string }) {
- return (
-
- {value}
- {label}
-
- )
-}
-
-function Section({
- title,
- subtitle,
- accent,
- children,
-}: {
- title: string
- subtitle: string
- accent: string
- children: React.ReactNode
-}) {
- return (
-
-
-
- {title}
- {subtitle}
-
- {children}
-
- )
-}
-
-function Row({ label, detail }: { label: string; detail: string }) {
- return (
-
-
- {label}
-
- {detail}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: NAVY[900] },
- center: { alignItems: 'center', justifyContent: 'center' },
- scroll: { flex: 1 },
- errorText: { color: TEXT.SECONDARY, ...TYPOGRAPHY.BODY },
-
- hero: { padding: SPACING[6], alignItems: 'center' },
- iconCircle: {
- width: 64,
- height: 64,
- borderRadius: 32,
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: SPACING[3],
- },
- title: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, textAlign: 'center' },
- description: {
- ...TYPOGRAPHY.BODY,
- color: TEXT.SECONDARY,
- textAlign: 'center',
- marginTop: SPACING[2],
- lineHeight: 22,
- },
-
- badgeRow: { flexDirection: 'row', gap: SPACING[2], marginTop: SPACING[3], flexWrap: 'wrap', justifyContent: 'center' },
- badge: { paddingHorizontal: SPACING[2], paddingVertical: 3, borderRadius: RADIUS.SM, borderWidth: 1 },
- badgeText: { ...TYPOGRAPHY.LABEL },
-
- statsRow: { flexDirection: 'row', marginTop: SPACING[6], gap: SPACING[8] },
- statItem: { alignItems: 'center' },
- statValue: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
- statLabel: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: 2 },
-
- section: { paddingHorizontal: SPACING[5], marginTop: SPACING[6] },
- sectionHeader: { flexDirection: 'row', alignItems: 'center', gap: SPACING[2], marginBottom: SPACING[3] },
- sectionDot: { width: 8, height: 8, borderRadius: 4 },
- sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, flex: 1 },
- sectionSubtitle: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY },
-
- sectionBody: {
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.MD,
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- overflow: 'hidden',
- },
- row: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: SPACING[3],
- paddingVertical: SPACING[3],
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: BORDER_COLORS.DIM,
- gap: SPACING[3],
- },
- rowLabel: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.PRIMARY, flex: 1 },
- rowDetail: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY },
-
- ctaContainer: {
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- paddingHorizontal: SPACING[5],
- paddingTop: SPACING[3],
- backgroundColor: DARK.SCRIM,
- borderTopWidth: 1,
- borderTopColor: BORDER_COLORS.DIM,
- },
- ctaButton: { height: 52, borderRadius: RADIUS.MD, alignItems: 'center', justifyContent: 'center' },
- ctaText: { ...TYPOGRAPHY.BUTTON_MEDIUM, color: NAVY[900], letterSpacing: 0.5 },
-})
diff --git a/app/settings.tsx b/app/settings.tsx
deleted file mode 100644
index 2289fa1..0000000
--- a/app/settings.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-/**
- * Settings FormSheet
- * Profile, preferences, premium, legal, reset progress.
- */
-
-import { View, Text, StyleSheet, ScrollView, Pressable, Switch, Alert } from 'react-native'
-import { useRouter } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useTranslation } from 'react-i18next'
-
-import { Icon, type IconName } from '@/src/shared/components/Icon'
-import { useUserStore } from '@/src/shared/stores/userStore'
-import { useProgressStore } from '@/src/shared/stores/progressStore'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { TEXT, NAVY, GREEN, BORDER_COLORS } from '@/src/shared/constants/colors'
-
-export default function SettingsScreen() {
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const { t } = useTranslation()
-
- const profile = useUserStore(s => s.profile)
- const settings = useUserStore(s => s.settings)
- const updateSettings = useUserStore(s => s.updateSettings)
- const resetProgress = useProgressStore(s => s.resetProgress)
-
- const isPremium = profile.subscription !== 'free'
-
- const handleResetProgress = () => {
- Alert.alert(
- t('screens:settings.resetTitle'),
- t('screens:settings.resetMessage'),
- [
- { text: t('common:cancel'), style: 'cancel' },
- {
- text: t('screens:settings.resetConfirm'),
- style: 'destructive',
- onPress: () => {
- resetProgress()
- },
- },
- ],
- )
- }
-
- return (
-
- {t('screens:settings.title')}
-
- {/* Profile */}
-
-
-
- {!isPremium && (
- router.push('/paywall')}
- accent={GREEN[500]}
- />
- )}
-
-
- {/* Preferences */}
-
- updateSettings({ haptics: v })}
- />
- updateSettings({ soundEffects: v })}
- />
- updateSettings({ voiceCoaching: v })}
- />
- updateSettings({ musicEnabled: v })}
- />
-
-
- {/* Legal */}
-
- router.push('/terms')} />
- router.push('/privacy')} />
-
-
- {/* Danger */}
-
-
-
- {t('screens:settings.resetProgress')}
-
-
-
- {t('screens:settings.version')}
-
- )
-}
-
-function Section({ title, children }: { title: string; children: React.ReactNode }) {
- return (
-
- {title}
- {children}
-
- )
-}
-
-function Row({ label, value, accent }: { label: string; value: string; accent?: string }) {
- return (
-
- {label}
- {value}
-
- )
-}
-
-function SwitchRow({
- label,
- value,
- onChange,
-}: {
- label: string
- value: boolean
- onChange: (value: boolean) => void
-}) {
- return (
-
- {label}
-
-
- )
-}
-
-function LinkRow({
- icon,
- label,
- onPress,
- accent,
-}: {
- icon: IconName
- label: string
- onPress: () => void
- accent?: string
-}) {
- return (
-
-
-
- {label}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: NAVY[900] },
- header: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, marginBottom: SPACING[4] },
-
- section: { marginBottom: SPACING[6] },
- sectionTitle: {
- ...TYPOGRAPHY.CAPTION_1,
- color: TEXT.TERTIARY,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- marginBottom: SPACING[2],
- paddingHorizontal: SPACING[2],
- },
- sectionBody: {
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.MD,
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- overflow: 'hidden',
- },
-
- row: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: SPACING[4],
- paddingVertical: SPACING[3],
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: BORDER_COLORS.DIM,
- gap: SPACING[3],
- },
- rowLabel: { ...TYPOGRAPHY.BODY, color: TEXT.PRIMARY },
- rowValue: { ...TYPOGRAPHY.BODY, color: TEXT.SECONDARY },
- linkLeft: { flexDirection: 'row', alignItems: 'center', gap: SPACING[3] },
-
- dangerRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- paddingHorizontal: SPACING[4],
- paddingVertical: SPACING[3],
- },
- dangerText: { ...TYPOGRAPHY.BODY, color: '#FF453A' },
-
- version: {
- ...TYPOGRAPHY.CAPTION_1,
- color: TEXT.TERTIARY,
- textAlign: 'center',
- marginTop: SPACING[4],
- },
-})
diff --git a/app/terms.tsx b/app/terms.tsx
deleted file mode 100644
index f695e00..0000000
--- a/app/terms.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * TabataFit Terms of Service Screen
- * Features: F-027, F-029, F-100, F-129
- */
-
-import { useMemo } from 'react'
-import { View, StyleSheet, ScrollView } from 'react-native'
-import { Stack } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useTranslation } from 'react-i18next'
-import { StyledText } from '@/src/shared/components/StyledText'
-import { useThemeColors } from '@/src/shared/theme'
-import type { ThemeColors } from '@/src/shared/theme/types'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { NAVY, BORDER_COLORS, GREEN } from '@/src/shared/constants/colors'
-
-const SECTIONS = [
- 'acceptance',
- 'service',
- 'subscriptions',
- 'cancellation',
- 'liability',
- 'contact',
-] as const
-
-export default function TermsScreen() {
- const { t } = useTranslation()
- const colors = useThemeColors()
- const insets = useSafeAreaInsets()
- const styles = useMemo(() => createStyles(colors), [colors])
-
- return (
- <>
-
-
-
- {t('screens:terms.lastUpdated')}
-
-
- {SECTIONS.map((section) => (
-
-
- {t(`screens:terms.${section}.title`)}
-
-
- {t(`screens:terms.${section}.content`)}
-
-
- ))}
-
-
- support@tabatafit.app
-
-
- >
- )
-}
-
-function createStyles(colors: ThemeColors) {
- return StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: colors.bg.base,
- },
- content: {
- paddingHorizontal: LAYOUT.SCREEN_PADDING,
- paddingTop: SPACING[4],
- gap: SPACING[6],
- },
- lastUpdated: {
- color: colors.text.tertiary,
- },
- section: {
- gap: SPACING[2],
- },
- sectionTitle: {
- color: colors.text.primary,
- },
- sectionContent: {
- color: colors.text.secondary,
- lineHeight: 24,
- },
- email: {
- color: GREEN[500],
- textAlign: 'center',
- marginTop: SPACING[4],
- },
- })
-}
diff --git a/app/zone/[bodyZone].tsx b/app/zone/[bodyZone].tsx
deleted file mode 100644
index 2575d0c..0000000
--- a/app/zone/[bodyZone].tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-/**
- * Body Zone Detail Screen
- * Segmented level selector (Beginner default) + program list filtered by zone+level.
- */
-
-import { useEffect, useMemo, useState } from 'react'
-import { View, Text, StyleSheet, ScrollView, Pressable, ActivityIndicator } from 'react-native'
-import { Stack, useRouter, useLocalSearchParams } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useTranslation } from 'react-i18next'
-
-import { Icon } from '@/src/shared/components/Icon'
-import { fetchProgramsByBodyZone } from '@/src/shared/data/workoutPrograms'
-import {
- BODY_ZONE_META,
- LEVEL_META,
- type BodyZone,
- type ProgramLevel,
- type WorkoutProgram,
-} from '@/src/shared/types/workoutProgram'
-import { useProgressStore } from '@/src/shared/stores/progressStore'
-import { useUserStore } from '@/src/shared/stores/userStore'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { TEXT, NAVY, GREEN, BORDER_COLORS } from '@/src/shared/constants/colors'
-import { withOpacity } from '@/src/shared/utils/color'
-
-const LEVELS: ProgramLevel[] = ['Beginner', 'Intermediate', 'Advanced']
-const VALID_ZONES: BodyZone[] = ['upper-body', 'lower-body', 'full-body']
-
-export default function BodyZoneScreen() {
- const { bodyZone } = useLocalSearchParams<{ bodyZone: string }>()
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const { t } = useTranslation()
-
- const zone = (VALID_ZONES.includes(bodyZone as BodyZone) ? bodyZone : 'full-body') as BodyZone
- const meta = BODY_ZONE_META[zone]
-
- const [programs, setPrograms] = useState([])
- const [loading, setLoading] = useState(true)
- const [selectedLevel, setSelectedLevel] = useState('Beginner')
-
- const isProgramCompleted = useProgressStore(s => s.isProgramCompleted)
- const isPremium = useUserStore(s => s.profile.subscription) !== 'free'
-
- useEffect(() => {
- let cancelled = false
- setLoading(true)
- fetchProgramsByBodyZone(zone)
- .then(list => {
- if (!cancelled) setPrograms(list)
- })
- .finally(() => {
- if (!cancelled) setLoading(false)
- })
- return () => {
- cancelled = true
- }
- }, [zone])
-
- const filtered = useMemo(
- () => programs.filter(p => p.level === selectedLevel).sort((a, b) => a.sortOrder - b.sortOrder),
- [programs, selectedLevel],
- )
-
- return (
-
-
-
-
- {/* Zone header */}
-
-
-
-
-
- {meta.label}
- {t('screens:zone.chooseLevel')}
-
-
-
- {/* Level segmented */}
-
- {LEVELS.map(level => {
- const active = selectedLevel === level
- const levelMeta = LEVEL_META[level]
- return (
- setSelectedLevel(level)}
- style={[
- styles.segment,
- active && {
- backgroundColor: withOpacity(levelMeta.color, 0.2),
- borderColor: levelMeta.color,
- },
- ]}
- >
-
- {levelMeta.label}
-
-
- )
- })}
-
-
- {/* Program list */}
- {loading ? (
-
- ) : filtered.length === 0 ? (
- {t('screens:zone.emptyPrograms')}
- ) : (
-
- {filtered.map(program => (
- router.push(`/program/${program.id}`)}
- />
- ))}
-
- )}
-
-
- )
-}
-
-function ProgramCard({
- program,
- completed,
- locked,
- onPress,
-}: {
- program: WorkoutProgram
- completed: boolean
- locked: boolean
- onPress: () => void
-}) {
- const accent = program.accentColor ?? BODY_ZONE_META[program.bodyZone].color
- return (
- [styles.programCard, pressed && { opacity: 0.85 }]}
- >
-
-
-
- {program.title}
-
-
- {program.estimatedDuration} min · {program.tabatas.length} tabatas · {program.estimatedCalories} cal
-
-
- {completed && }
- {locked && }
- {!completed && !locked && }
-
- )
-}
-
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: NAVY[900] },
- scroll: { flex: 1 },
-
- zoneHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- padding: SPACING[4],
- borderRadius: RADIUS.LG,
- marginBottom: SPACING[5],
- },
- iconCircle: {
- width: 56,
- height: 56,
- borderRadius: 28,
- alignItems: 'center',
- justifyContent: 'center',
- },
- zoneTitle: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
- zoneSubtitle: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY, marginTop: 2 },
-
- segmented: {
- flexDirection: 'row',
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.MD,
- padding: 4,
- gap: 4,
- marginBottom: SPACING[5],
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- },
- segment: {
- flex: 1,
- paddingVertical: SPACING[2],
- borderRadius: RADIUS.SM,
- alignItems: 'center',
- borderWidth: 1,
- borderColor: 'transparent',
- },
- segmentText: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY },
-
- programList: { gap: SPACING[3] },
- programCard: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- padding: SPACING[4],
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.MD,
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- },
- programDot: { width: 10, height: 10, borderRadius: 5 },
- programTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY },
- programMeta: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: 2 },
-
- empty: {
- ...TYPOGRAPHY.BODY,
- color: TEXT.SECONDARY,
- textAlign: 'center',
- marginTop: SPACING[8],
- },
-})
diff --git a/assets/audio/complete.wav b/assets/audio/complete.wav
deleted file mode 100644
index 8a4dac7..0000000
Binary files a/assets/audio/complete.wav and /dev/null differ
diff --git a/assets/audio/countdown.wav b/assets/audio/countdown.wav
deleted file mode 100644
index 4540ad3..0000000
Binary files a/assets/audio/countdown.wav and /dev/null differ
diff --git a/assets/audio/music/electro_high.mp3 b/assets/audio/music/electro_high.mp3
deleted file mode 100644
index 3650371..0000000
Binary files a/assets/audio/music/electro_high.mp3 and /dev/null differ
diff --git a/assets/audio/music/electro_low.mp3 b/assets/audio/music/electro_low.mp3
deleted file mode 100644
index 5bd61b5..0000000
Binary files a/assets/audio/music/electro_low.mp3 and /dev/null differ
diff --git a/assets/audio/phase-start.wav b/assets/audio/phase-start.wav
deleted file mode 100644
index 9db0128..0000000
Binary files a/assets/audio/phase-start.wav and /dev/null differ
diff --git a/assets/audio/sounds/beep_double.mp3 b/assets/audio/sounds/beep_double.mp3
deleted file mode 100644
index ce72c91..0000000
Binary files a/assets/audio/sounds/beep_double.mp3 and /dev/null differ
diff --git a/assets/audio/sounds/beep_long.mp3 b/assets/audio/sounds/beep_long.mp3
deleted file mode 100644
index 68232bf..0000000
Binary files a/assets/audio/sounds/beep_long.mp3 and /dev/null differ
diff --git a/assets/audio/sounds/beep_short.mp3 b/assets/audio/sounds/beep_short.mp3
deleted file mode 100644
index bf09b46..0000000
Binary files a/assets/audio/sounds/beep_short.mp3 and /dev/null differ
diff --git a/assets/audio/sounds/bell.mp3 b/assets/audio/sounds/bell.mp3
deleted file mode 100644
index d7df1f4..0000000
Binary files a/assets/audio/sounds/bell.mp3 and /dev/null differ
diff --git a/assets/audio/sounds/count_1.mp3 b/assets/audio/sounds/count_1.mp3
deleted file mode 100644
index 8e002bb..0000000
Binary files a/assets/audio/sounds/count_1.mp3 and /dev/null differ
diff --git a/assets/audio/sounds/count_2.mp3 b/assets/audio/sounds/count_2.mp3
deleted file mode 100644
index 984e7f8..0000000
Binary files a/assets/audio/sounds/count_2.mp3 and /dev/null differ
diff --git a/assets/audio/sounds/count_3.mp3 b/assets/audio/sounds/count_3.mp3
deleted file mode 100644
index 984e7f8..0000000
Binary files a/assets/audio/sounds/count_3.mp3 and /dev/null differ
diff --git a/assets/audio/sounds/fanfare.mp3 b/assets/audio/sounds/fanfare.mp3
deleted file mode 100644
index 1e36c05..0000000
Binary files a/assets/audio/sounds/fanfare.mp3 and /dev/null differ
diff --git a/assets/images/android-icon-background.png b/assets/images/android-icon-background.png
deleted file mode 100644
index 5ffefc5..0000000
Binary files a/assets/images/android-icon-background.png and /dev/null differ
diff --git a/assets/images/android-icon-foreground.png b/assets/images/android-icon-foreground.png
deleted file mode 100644
index 3a9e501..0000000
Binary files a/assets/images/android-icon-foreground.png and /dev/null differ
diff --git a/assets/images/android-icon-monochrome.png b/assets/images/android-icon-monochrome.png
deleted file mode 100644
index 77484eb..0000000
Binary files a/assets/images/android-icon-monochrome.png and /dev/null differ
diff --git a/assets/images/favicon.png b/assets/images/favicon.png
deleted file mode 100644
index 408bd74..0000000
Binary files a/assets/images/favicon.png and /dev/null differ
diff --git a/assets/images/icon.png b/assets/images/icon.png
deleted file mode 100644
index d21f091..0000000
Binary files a/assets/images/icon.png and /dev/null differ
diff --git a/assets/images/icon.svg b/assets/images/icon.svg
deleted file mode 100644
index 516dd2b..0000000
--- a/assets/images/icon.svg
+++ /dev/null
@@ -1,67 +0,0 @@
-
diff --git a/assets/images/partial-react-logo.png b/assets/images/partial-react-logo.png
deleted file mode 100644
index 66fd957..0000000
Binary files a/assets/images/partial-react-logo.png and /dev/null differ
diff --git a/assets/images/react-logo.png b/assets/images/react-logo.png
deleted file mode 100644
index 9d72a9f..0000000
Binary files a/assets/images/react-logo.png and /dev/null differ
diff --git a/assets/images/react-logo@2x.png b/assets/images/react-logo@2x.png
deleted file mode 100644
index 2229b13..0000000
Binary files a/assets/images/react-logo@2x.png and /dev/null differ
diff --git a/assets/images/react-logo@3x.png b/assets/images/react-logo@3x.png
deleted file mode 100644
index a99b203..0000000
Binary files a/assets/images/react-logo@3x.png and /dev/null differ
diff --git a/assets/images/splash-icon.png b/assets/images/splash-icon.png
deleted file mode 100644
index 03d6f6b..0000000
Binary files a/assets/images/splash-icon.png and /dev/null differ
diff --git a/assets/mascot.gif b/assets/mascot.gif
deleted file mode 100644
index 0c7f993..0000000
Binary files a/assets/mascot.gif and /dev/null differ
diff --git a/assets/mascot.png b/assets/mascot.png
deleted file mode 100644
index f34c32d..0000000
Binary files a/assets/mascot.png and /dev/null differ
diff --git a/assets/mascot_bak.gif b/assets/mascot_bak.gif
deleted file mode 100644
index bd9dcc5..0000000
Binary files a/assets/mascot_bak.gif and /dev/null differ
diff --git a/assets/model.glb b/assets/model.glb
deleted file mode 100644
index fc2a685..0000000
Binary files a/assets/model.glb and /dev/null differ
diff --git a/eas.json b/eas.json
deleted file mode 100644
index 78e3fa0..0000000
--- a/eas.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "cli": {
- "version": ">= 16.0.1",
- "appVersionSource": "remote"
- },
- "build": {
- "development": {
- "developmentClient": true,
- "distribution": "internal",
- "ios": {
- "simulator": true
- }
- },
- "preview": {
- "distribution": "internal",
- "ios": {
- "resourceClass": "m-medium"
- }
- },
- "production": {
- "autoIncrement": true,
- "ios": {
- "resourceClass": "m-medium"
- }
- }
- },
- "submit": {
- "production": {
- "ios": {
- "appleId": "millianlmx@icloud.com",
- "ascAppId": "REPLACE_WITH_APP_STORE_CONNECT_APP_ID",
- "appleTeamId": "REPLACE_WITH_APPLE_TEAM_ID"
- }
- }
- }
-}
diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
index 5025da6..0000000
--- a/eslint.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// https://docs.expo.dev/guides/using-eslint/
-const { defineConfig } = require('eslint/config');
-const expoConfig = require('eslint-config-expo/flat');
-
-module.exports = defineConfig([
- expoConfig,
- {
- ignores: ['dist/*'],
- },
-]);
diff --git a/fix_i18n.js b/fix_i18n.js
deleted file mode 100644
index 428df9e..0000000
--- a/fix_i18n.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const fs = require('fs');
-
-const langs = [
- { code: 'fr', text: 'Prêt à tout casser aujourd\'hui ?' },
- { code: 'es', text: '¿Listo para arrasar hoy?' },
- { code: 'de', text: 'Bereit, heute alles zu geben?' }
-];
-
-langs.forEach(({ code, text }) => {
- const path = `src/shared/i18n/locales/${code}/screens.json`;
- if (fs.existsSync(path)) {
- let content = fs.readFileSync(path, 'utf8');
- content = content.replace(
- /"home":\s*\{/,
- `"home": {\n "readyToCrush": "${text}",`
- );
- fs.writeFileSync(path, content);
- console.log(`Updated ${code}`);
- }
-});
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index abfbc96..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,15598 +0,0 @@
-{
- "name": "tabatafit",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "tabatafit",
- "version": "1.0.0",
- "dependencies": {
- "@expo-google-fonts/dm-mono": "^0.4.2",
- "@expo-google-fonts/dm-serif-display": "^0.4.2",
- "@expo-google-fonts/inter": "^0.4.2",
- "@expo-google-fonts/outfit": "^0.4.3",
- "@expo/ui": "~0.2.0-beta.9",
- "@react-native-async-storage/async-storage": "^2.2.0",
- "@react-navigation/bottom-tabs": "^7.4.0",
- "@react-navigation/elements": "^2.6.3",
- "@react-navigation/native": "^7.1.8",
- "@supabase/supabase-js": "^2.98.0",
- "@tanstack/query-async-storage-persister": "^5.90.24",
- "@tanstack/react-query": "^5.90.21",
- "@tanstack/react-query-persist-client": "^5.90.24",
- "expo": "~54.0.33",
- "expo-application": "~7.0.8",
- "expo-av": "~16.0.8",
- "expo-blur": "~15.0.8",
- "expo-constants": "~18.0.13",
- "expo-device": "~8.0.10",
- "expo-file-system": "~19.0.21",
- "expo-font": "~14.0.11",
- "expo-gl": "~16.0.10",
- "expo-haptics": "~15.0.8",
- "expo-image": "~3.0.11",
- "expo-keep-awake": "~15.0.8",
- "expo-linear-gradient": "~15.0.8",
- "expo-linking": "~8.0.11",
- "expo-localization": "~17.0.8",
- "expo-network": "~8.0.8",
- "expo-notifications": "~0.32.16",
- "expo-router": "~6.0.23",
- "expo-sharing": "~14.0.8",
- "expo-splash-screen": "~31.0.13",
- "expo-status-bar": "~3.0.9",
- "expo-store-review": "~9.0.9",
- "expo-symbols": "~1.0.8",
- "expo-system-ui": "~6.0.9",
- "expo-video": "~3.0.16",
- "expo-web-browser": "~15.0.10",
- "i18next": "^25.8.12",
- "posthog-react-native": "^4.36.0",
- "posthog-react-native-session-replay": "^1.5.0",
- "react": "19.1.0",
- "react-dom": "19.1.0",
- "react-i18next": "^16.5.4",
- "react-native": "0.81.5",
- "react-native-gesture-handler": "~2.28.0",
- "react-native-purchases": "^9.10.3",
- "react-native-reanimated": "~4.1.1",
- "react-native-safe-area-context": "~5.6.0",
- "react-native-screens": "~4.16.0",
- "react-native-svg": "15.12.1",
- "react-native-web": "~0.21.0",
- "react-native-worklets": "0.5.1",
- "zustand": "^5.0.11"
- },
- "devDependencies": {
- "@testing-library/jest-native": "^5.4.3",
- "@testing-library/react-native": "^13.3.3",
- "@types/react": "~19.1.0",
- "@vitest/coverage-v8": "^4.1.1",
- "eslint": "^9.25.0",
- "eslint-config-expo": "~10.0.0",
- "jsdom": "^29.0.1",
- "react-test-renderer": "^19.1.0",
- "typescript": "~5.9.2",
- "vitest": "^4.1.1"
- }
- },
- "node_modules/@0no-co/graphql.web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz",
- "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==",
- "license": "MIT",
- "peerDependencies": {
- "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
- },
- "peerDependenciesMeta": {
- "graphql": {
- "optional": true
- }
- }
- },
- "node_modules/@asamuzakjp/css-color": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
- "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@csstools/css-calc": "^3.1.1",
- "@csstools/css-color-parser": "^4.0.2",
- "@csstools/css-parser-algorithms": "^4.0.0",
- "@csstools/css-tokenizer": "^4.0.0",
- "lru-cache": "^11.2.6"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- }
- },
- "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
- "version": "11.2.7",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
- "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/@asamuzakjp/dom-selector": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz",
- "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@asamuzakjp/nwsapi": "^2.3.9",
- "bidi-js": "^1.0.3",
- "css-tree": "^3.2.1",
- "is-potential-custom-element-name": "^1.0.1",
- "lru-cache": "^11.2.7"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- }
- },
- "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
- "version": "11.2.7",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
- "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/@asamuzakjp/nwsapi": {
- "version": "2.3.9",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
- "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@babel/code-frame": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
- "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
- "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
- "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-module-transforms": "^7.28.6",
- "@babel/helpers": "^7.28.6",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/traverse": "^7.29.0",
- "@babel/types": "^7.29.0",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.29.1",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
- "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.0",
- "@babel/types": "^7.29.0",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure": {
- "version": "7.27.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
- "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.27.3"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
- "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.28.6",
- "@babel/helper-validator-option": "^7.27.1",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
- "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.3",
- "@babel/helper-member-expression-to-functions": "^7.28.5",
- "@babel/helper-optimise-call-expression": "^7.27.1",
- "@babel/helper-replace-supers": "^7.28.6",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
- "@babel/traverse": "^7.28.6",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-create-regexp-features-plugin": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz",
- "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.3",
- "regexpu-core": "^6.3.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-define-polyfill-provider": {
- "version": "0.6.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz",
- "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6",
- "debug": "^4.4.3",
- "lodash.debounce": "^4.0.8",
- "resolve": "^1.22.11"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
- }
- },
- "node_modules/@babel/helper-globals": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
- "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-member-expression-to-functions": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
- "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.28.5",
- "@babel/types": "^7.28.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
- "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
- "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-optimise-call-expression": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
- "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
- "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-remap-async-to-generator": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz",
- "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.1",
- "@babel/helper-wrap-function": "^7.27.1",
- "@babel/traverse": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-replace-supers": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
- "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-member-expression-to-functions": "^7.28.5",
- "@babel/helper-optimise-call-expression": "^7.27.1",
- "@babel/traverse": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
- "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.27.1",
- "@babel/types": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
- "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-wrap-function": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz",
- "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.28.6",
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
- "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz",
- "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "chalk": "^2.4.2",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/@babel/highlight/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "license": "MIT"
- },
- "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/@babel/highlight/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
- "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-proposal-decorators": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz",
- "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/plugin-syntax-decorators": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-export-default-from": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz",
- "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==",
- "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-syntax-async-generators": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
- "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-bigint": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
- "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-class-properties": {
- "version": "7.12.13",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
- "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.12.13"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-class-static-block": {
- "version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
- "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-decorators": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz",
- "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-dynamic-import": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
- "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-export-default-from": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz",
- "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-flow": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz",
- "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-import-attributes": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
- "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-import-meta": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
- "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-json-strings": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
- "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
- "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
- "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
- "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-numeric-separator": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
- "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-object-rest-spread": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
- "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-optional-catch-binding": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
- "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-optional-chaining": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
- "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-private-property-in-object": {
- "version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
- "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-top-level-await": {
- "version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
- "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
- "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-arrow-functions": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
- "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==",
- "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-async-generator-functions": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz",
- "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/helper-remap-async-to-generator": "^7.27.1",
- "@babel/traverse": "^7.29.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-async-to-generator": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz",
- "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/helper-remap-async-to-generator": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz",
- "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-class-properties": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz",
- "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-class-static-block": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
- "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.12.0"
- }
- },
- "node_modules/@babel/plugin-transform-classes": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz",
- "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.3",
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-globals": "^7.28.0",
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/helper-replace-supers": "^7.28.6",
- "@babel/traverse": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-computed-properties": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz",
- "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/template": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-destructuring": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz",
- "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1",
- "@babel/traverse": "^7.28.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-export-namespace-from": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz",
- "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==",
- "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-flow-strip-types": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz",
- "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1",
- "@babel/plugin-syntax-flow": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-for-of": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz",
- "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-function-name": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz",
- "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-compilation-targets": "^7.27.1",
- "@babel/helper-plugin-utils": "^7.27.1",
- "@babel/traverse": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-literals": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz",
- "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==",
- "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-logical-assignment-operators": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz",
- "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz",
- "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-transforms": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz",
- "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.28.5",
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz",
- "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-numeric-separator": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz",
- "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-object-rest-spread": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz",
- "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/plugin-transform-destructuring": "^7.28.5",
- "@babel/plugin-transform-parameters": "^7.27.7",
- "@babel/traverse": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-optional-catch-binding": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz",
- "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-optional-chaining": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz",
- "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-parameters": {
- "version": "7.27.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
- "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
- "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-private-methods": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz",
- "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-private-property-in-object": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz",
- "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.3",
- "@babel/helper-create-class-features-plugin": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-display-name": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz",
- "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==",
- "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-react-jsx": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz",
- "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.3",
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/plugin-syntax-jsx": "^7.28.6",
- "@babel/types": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-development": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz",
- "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/plugin-transform-react-jsx": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
- "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
- "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-react-jsx-source": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
- "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
- "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-react-pure-annotations": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz",
- "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.1",
- "@babel/helper-plugin-utils": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz",
- "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-runtime": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz",
- "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6",
- "babel-plugin-polyfill-corejs2": "^0.4.14",
- "babel-plugin-polyfill-corejs3": "^0.13.0",
- "babel-plugin-polyfill-regenerator": "^0.6.5",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "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",
- "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==",
- "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-spread": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz",
- "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-sticky-regex": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz",
- "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==",
- "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-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",
- "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.27.3",
- "@babel/helper-create-class-features-plugin": "^7.28.6",
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
- "@babel/plugin-syntax-typescript": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-unicode-regex": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz",
- "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.27.1",
- "@babel/helper-plugin-utils": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-react": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz",
- "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1",
- "@babel/helper-validator-option": "^7.27.1",
- "@babel/plugin-transform-react-display-name": "^7.28.0",
- "@babel/plugin-transform-react-jsx": "^7.27.1",
- "@babel/plugin-transform-react-jsx-development": "^7.27.1",
- "@babel/plugin-transform-react-pure-annotations": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-typescript": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz",
- "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1",
- "@babel/helper-validator-option": "^7.27.1",
- "@babel/plugin-syntax-jsx": "^7.27.1",
- "@babel/plugin-transform-modules-commonjs": "^7.27.1",
- "@babel/plugin-transform-typescript": "^7.28.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
- "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
- "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
- "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.29.0",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse--for-generate-function-map": {
- "name": "@babel/traverse",
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
- "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.29.0",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
- "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@bcoe/v8-coverage": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
- "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@bramus/specificity": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
- "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "css-tree": "^3.0.0"
- },
- "bin": {
- "specificity": "bin/cli.js"
- }
- },
- "node_modules/@csstools/color-helpers": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
- "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT-0",
- "engines": {
- "node": ">=20.19.0"
- }
- },
- "node_modules/@csstools/css-calc": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
- "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=20.19.0"
- },
- "peerDependencies": {
- "@csstools/css-parser-algorithms": "^4.0.0",
- "@csstools/css-tokenizer": "^4.0.0"
- }
- },
- "node_modules/@csstools/css-color-parser": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
- "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@csstools/color-helpers": "^6.0.2",
- "@csstools/css-calc": "^3.1.1"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "peerDependencies": {
- "@csstools/css-parser-algorithms": "^4.0.0",
- "@csstools/css-tokenizer": "^4.0.0"
- }
- },
- "node_modules/@csstools/css-parser-algorithms": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
- "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=20.19.0"
- },
- "peerDependencies": {
- "@csstools/css-tokenizer": "^4.0.0"
- }
- },
- "node_modules/@csstools/css-syntax-patches-for-csstree": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz",
- "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT-0",
- "peerDependencies": {
- "css-tree": "^3.2.1"
- },
- "peerDependenciesMeta": {
- "css-tree": {
- "optional": true
- }
- }
- },
- "node_modules/@csstools/css-tokenizer": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
- "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=20.19.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/@emnapi/core": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
- "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.1.0",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
- "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
- "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.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.21.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
- "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/object-schema": "^2.1.7",
- "debug": "^4.3.1",
- "minimatch": "^3.1.2"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/config-helpers": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
- "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.17.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/core": {
- "version": "0.17.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
- "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
- "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.1",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/js": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
- "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- }
- },
- "node_modules/@eslint/object-schema": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
- "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
- "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.17.0",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@exodus/bytes": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
- "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- },
- "peerDependencies": {
- "@noble/hashes": "^1.8.0 || ^2.0.0"
- },
- "peerDependenciesMeta": {
- "@noble/hashes": {
- "optional": true
- }
- }
- },
- "node_modules/@expo-google-fonts/dm-mono": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@expo-google-fonts/dm-mono/-/dm-mono-0.4.2.tgz",
- "integrity": "sha512-loMaZOkQRs1r7yt4rN39zcr9e0J+smwnSx929yuODkuiPfsY4PaW18C9SEZ0BvXfcBKoRhatGoIBl8V2MOYVPQ==",
- "license": "MIT AND OFL-1.1"
- },
- "node_modules/@expo-google-fonts/dm-serif-display": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@expo-google-fonts/dm-serif-display/-/dm-serif-display-0.4.2.tgz",
- "integrity": "sha512-onlO8xAzsgMbKcwUE+fAgJ5AFHhk06VtaDN7eQOJwjV65QIciDKTiSNu1ymHc4m6g/x6D9OqPIYPXdTNIfaEaA==",
- "license": "MIT AND OFL-1.1"
- },
- "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/outfit": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@expo-google-fonts/outfit/-/outfit-0.4.3.tgz",
- "integrity": "sha512-2uQmDVJencWLllxds6SG92E+SjxyZfvg7eZKZ5XLHggmm5AuUyQK7lzMAFOUzT6kheq2kJ7BAiubMdjKT32fJg==",
- "license": "MIT AND OFL-1.1"
- },
- "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",
- "integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==",
- "license": "MIT",
- "dependencies": {
- "node-forge": "^1.3.3"
- }
- },
- "node_modules/@expo/config": {
- "version": "12.0.13",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz",
- "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "~7.10.4",
- "@expo/config-plugins": "~54.0.4",
- "@expo/config-types": "^54.0.10",
- "@expo/json-file": "^10.0.8",
- "deepmerge": "^4.3.1",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "require-from-string": "^2.0.2",
- "resolve-from": "^5.0.0",
- "resolve-workspace-root": "^2.0.0",
- "semver": "^7.6.0",
- "slugify": "^1.3.4",
- "sucrase": "~3.35.1"
- }
- },
- "node_modules/@expo/config-plugins": {
- "version": "54.0.4",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz",
- "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==",
- "license": "MIT",
- "dependencies": {
- "@expo/config-types": "^54.0.10",
- "@expo/json-file": "~10.0.8",
- "@expo/plist": "^0.4.8",
- "@expo/sdk-runtime-versions": "^1.0.0",
- "chalk": "^4.1.2",
- "debug": "^4.3.5",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "resolve-from": "^5.0.0",
- "semver": "^7.5.4",
- "slash": "^3.0.0",
- "slugify": "^1.6.6",
- "xcode": "^3.0.1",
- "xml2js": "0.6.0"
- }
- },
- "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": "54.0.10",
- "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz",
- "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==",
- "license": "MIT"
- },
- "node_modules/@expo/config/node_modules/@babel/code-frame": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
- "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
- "license": "MIT",
- "dependencies": {
- "@babel/highlight": "^7.10.4"
- }
- },
- "node_modules/@expo/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",
- "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==",
- "license": "MIT",
- "dependencies": {
- "@expo/sudo-prompt": "^9.3.1",
- "debug": "^3.1.0"
- }
- },
- "node_modules/@expo/devcert/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/@expo/devtools": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-0.1.8.tgz",
- "integrity": "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==",
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.2"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
- }
- },
- "node_modules/@expo/env": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.8.tgz",
- "integrity": "sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==",
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.0.0",
- "debug": "^4.3.4",
- "dotenv": "~16.4.5",
- "dotenv-expand": "~11.0.6",
- "getenv": "^2.0.0"
- }
- },
- "node_modules/@expo/fingerprint": {
- "version": "0.15.4",
- "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.4.tgz",
- "integrity": "sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==",
- "license": "MIT",
- "dependencies": {
- "@expo/spawn-async": "^1.7.2",
- "arg": "^5.0.2",
- "chalk": "^4.1.2",
- "debug": "^4.3.4",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "ignore": "^5.3.1",
- "minimatch": "^9.0.0",
- "p-limit": "^3.1.0",
- "resolve-from": "^5.0.0",
- "semver": "^7.6.0"
- },
- "bin": {
- "fingerprint": "bin/cli.js"
- }
- },
- "node_modules/@expo/fingerprint/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@expo/fingerprint/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "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.8",
- "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.8.tgz",
- "integrity": "sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA==",
- "license": "MIT",
- "dependencies": {
- "@expo/spawn-async": "^1.7.2",
- "chalk": "^4.0.0",
- "getenv": "^2.0.0",
- "jimp-compact": "0.16.1",
- "parse-png": "^2.1.0",
- "resolve-from": "^5.0.0",
- "resolve-global": "^1.0.0",
- "semver": "^7.6.0",
- "temp-dir": "~2.0.0",
- "unique-string": "~2.0.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.8",
- "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.8.tgz",
- "integrity": "sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "~7.10.4",
- "json5": "^2.2.3"
- }
- },
- "node_modules/@expo/json-file/node_modules/@babel/code-frame": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
- "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
- "license": "MIT",
- "dependencies": {
- "@babel/highlight": "^7.10.4"
- }
- },
- "node_modules/@expo/metro": {
- "version": "54.2.0",
- "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz",
- "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==",
- "license": "MIT",
- "dependencies": {
- "metro": "0.83.3",
- "metro-babel-transformer": "0.83.3",
- "metro-cache": "0.83.3",
- "metro-cache-key": "0.83.3",
- "metro-config": "0.83.3",
- "metro-core": "0.83.3",
- "metro-file-map": "0.83.3",
- "metro-minify-terser": "0.83.3",
- "metro-resolver": "0.83.3",
- "metro-runtime": "0.83.3",
- "metro-source-map": "0.83.3",
- "metro-symbolicate": "0.83.3",
- "metro-transform-plugins": "0.83.3",
- "metro-transform-worker": "0.83.3"
- }
- },
- "node_modules/@expo/metro-config": {
- "version": "54.0.14",
- "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.14.tgz",
- "integrity": "sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.20.0",
- "@babel/core": "^7.20.0",
- "@babel/generator": "^7.20.5",
- "@expo/config": "~12.0.13",
- "@expo/env": "~2.0.8",
- "@expo/json-file": "~10.0.8",
- "@expo/metro": "~54.2.0",
- "@expo/spawn-async": "^1.7.2",
- "browserslist": "^4.25.0",
- "chalk": "^4.1.0",
- "debug": "^4.3.2",
- "dotenv": "~16.4.5",
- "dotenv-expand": "~11.0.6",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "hermes-parser": "^0.29.1",
- "jsc-safe-url": "^0.2.4",
- "lightningcss": "^1.30.1",
- "minimatch": "^9.0.0",
- "postcss": "~8.4.32",
- "resolve-from": "^5.0.0"
- },
- "peerDependencies": {
- "expo": "*"
- },
- "peerDependenciesMeta": {
- "expo": {
- "optional": true
- }
- }
- },
- "node_modules/@expo/metro-config/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@expo/metro-config/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@expo/metro-runtime": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz",
- "integrity": "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==",
- "license": "MIT",
- "dependencies": {
- "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.3.8",
- "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.8.tgz",
- "integrity": "sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w==",
- "license": "MIT",
- "dependencies": {
- "@expo/spawn-async": "^1.7.2",
- "exec-async": "^2.2.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@expo/package-manager": {
- "version": "1.9.10",
- "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.10.tgz",
- "integrity": "sha512-axJm+NOj3jVxep49va/+L3KkF3YW/dkV+RwzqUJedZrv4LeTqOG4rhrCaCPXHTvLqCTDKu6j0Xyd28N7mnxsGA==",
- "license": "MIT",
- "dependencies": {
- "@expo/json-file": "^10.0.8",
- "@expo/spawn-async": "^1.7.2",
- "chalk": "^4.0.0",
- "npm-package-arg": "^11.0.0",
- "ora": "^3.4.0",
- "resolve-workspace-root": "^2.0.0"
- }
- },
- "node_modules/@expo/plist": {
- "version": "0.4.8",
- "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz",
- "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==",
- "license": "MIT",
- "dependencies": {
- "@xmldom/xmldom": "^0.8.8",
- "base64-js": "^1.2.3",
- "xmlbuilder": "^15.1.1"
- }
- },
- "node_modules/@expo/prebuild-config": {
- "version": "54.0.8",
- "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz",
- "integrity": "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==",
- "license": "MIT",
- "dependencies": {
- "@expo/config": "~12.0.13",
- "@expo/config-plugins": "~54.0.4",
- "@expo/config-types": "^54.0.10",
- "@expo/image-utils": "^0.8.8",
- "@expo/json-file": "^10.0.8",
- "@react-native/normalize-colors": "0.81.5",
- "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/schema-utils": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz",
- "integrity": "sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==",
- "license": "MIT"
- },
- "node_modules/@expo/sdk-runtime-versions": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz",
- "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==",
- "license": "MIT"
- },
- "node_modules/@expo/spawn-async": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz",
- "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==",
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@expo/sudo-prompt": {
- "version": "9.3.2",
- "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz",
- "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==",
- "license": "MIT"
- },
- "node_modules/@expo/ui": {
- "version": "0.2.0-beta.9",
- "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-0.2.0-beta.9.tgz",
- "integrity": "sha512-RaBcp0cMe5GykQogJwRZGy4o4JHDLtrr+HaurDPhwPKqVATsV0rR11ysmFe4QX8XWLP/L3od7NOkXUi5ailvaw==",
- "license": "MIT",
- "dependencies": {
- "sf-symbols-typescript": "^2.1.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/@expo/vector-icons": {
- "version": "15.0.3",
- "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.0.3.tgz",
- "integrity": "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA==",
- "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",
- "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==",
- "license": "MIT"
- },
- "node_modules/@expo/xcpretty": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.0.tgz",
- "integrity": "sha512-o2qDlTqJ606h4xR36H2zWTywmZ3v3842K6TU8Ik2n1mfW0S580VHlt3eItVYdLYz+klaPp7CXqanja8eASZjRw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/code-frame": "^7.20.0",
- "chalk": "^4.1.0",
- "js-yaml": "^4.1.0"
- },
- "bin": {
- "excpretty": "build/cli.js"
- }
- },
- "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/@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": {
- "@humanfs/core": "^0.19.1",
- "@humanwhocodes/retry": "^0.4.0"
- },
- "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/@ide/backoff": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
- "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
- "license": "MIT"
- },
- "node_modules/@isaacs/cliui": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
- "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@isaacs/fs-minipass": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
- "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
- "license": "ISC",
- "dependencies": {
- "minipass": "^7.0.4"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@isaacs/ttlcache": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
- "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@istanbuljs/load-nyc-config": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
- "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
- "license": "ISC",
- "dependencies": {
- "camelcase": "^5.3.1",
- "find-up": "^4.1.0",
- "get-package-type": "^0.1.0",
- "js-yaml": "^3.13.1",
- "resolve-from": "^5.0.0"
- },
- "engines": {
- "node": ">=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",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
- "license": "MIT",
- "engines": {
- "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",
- "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jest/create-cache-key-function": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz",
- "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==",
- "license": "MIT",
- "dependencies": {
- "@jest/types": "^29.6.3"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/diff-sequences": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz",
- "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@jest/environment": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
- "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==",
- "license": "MIT",
- "dependencies": {
- "@jest/fake-timers": "^29.7.0",
- "@jest/types": "^29.6.3",
- "@types/node": "*",
- "jest-mock": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/fake-timers": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
- "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==",
- "license": "MIT",
- "dependencies": {
- "@jest/types": "^29.6.3",
- "@sinonjs/fake-timers": "^10.0.2",
- "@types/node": "*",
- "jest-message-util": "^29.7.0",
- "jest-mock": "^29.7.0",
- "jest-util": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/get-type": {
- "version": "30.1.0",
- "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz",
- "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@jest/schemas": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
- "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
- "license": "MIT",
- "dependencies": {
- "@sinclair/typebox": "^0.27.8"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/transform": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
- "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.11.6",
- "@jest/types": "^29.6.3",
- "@jridgewell/trace-mapping": "^0.3.18",
- "babel-plugin-istanbul": "^6.1.1",
- "chalk": "^4.0.0",
- "convert-source-map": "^2.0.0",
- "fast-json-stable-stringify": "^2.1.0",
- "graceful-fs": "^4.2.9",
- "jest-haste-map": "^29.7.0",
- "jest-regex-util": "^29.6.3",
- "jest-util": "^29.7.0",
- "micromatch": "^4.0.4",
- "pirates": "^4.0.4",
- "slash": "^3.0.0",
- "write-file-atomic": "^4.0.2"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/types": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
- "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.11",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
- "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "0.2.12",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
- "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.4.3",
- "@emnapi/runtime": "^1.4.3",
- "@tybys/wasm-util": "^0.10.0"
- }
- },
- "node_modules/@nolyfill/is-core-module": {
- "version": "1.0.39",
- "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
- "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.4.0"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.122.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
- "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/@posthog/core": {
- "version": "1.23.1",
- "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.1.tgz",
- "integrity": "sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==",
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.6"
- }
- },
- "node_modules/@radix-ui/primitive": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
- "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
- "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-context": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
- "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-direction": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
- "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
- "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-id": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
- "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-slot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
- "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "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",
- "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
- "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-effect-event": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
- "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
- "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@react-native-async-storage/async-storage": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
- "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
- "license": "MIT",
- "dependencies": {
- "merge-options": "^3.0.4"
- },
- "peerDependencies": {
- "react-native": "^0.0.0-0 || >=0.65 <1.0"
- }
- },
- "node_modules/@react-native/assets-registry": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
- "integrity": "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==",
- "license": "MIT",
- "engines": {
- "node": ">= 20.19.4"
- }
- },
- "node_modules/@react-native/babel-plugin-codegen": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.5.tgz",
- "integrity": "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.25.3",
- "@react-native/codegen": "0.81.5"
- },
- "engines": {
- "node": ">= 20.19.4"
- }
- },
- "node_modules/@react-native/babel-preset": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.81.5.tgz",
- "integrity": "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/plugin-proposal-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-syntax-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3",
- "@babel/plugin-transform-arrow-functions": "^7.24.7",
- "@babel/plugin-transform-async-generator-functions": "^7.25.4",
- "@babel/plugin-transform-async-to-generator": "^7.24.7",
- "@babel/plugin-transform-block-scoping": "^7.25.0",
- "@babel/plugin-transform-class-properties": "^7.25.4",
- "@babel/plugin-transform-classes": "^7.25.4",
- "@babel/plugin-transform-computed-properties": "^7.24.7",
- "@babel/plugin-transform-destructuring": "^7.24.8",
- "@babel/plugin-transform-flow-strip-types": "^7.25.2",
- "@babel/plugin-transform-for-of": "^7.24.7",
- "@babel/plugin-transform-function-name": "^7.25.1",
- "@babel/plugin-transform-literals": "^7.25.2",
- "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
- "@babel/plugin-transform-modules-commonjs": "^7.24.8",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
- "@babel/plugin-transform-numeric-separator": "^7.24.7",
- "@babel/plugin-transform-object-rest-spread": "^7.24.7",
- "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
- "@babel/plugin-transform-optional-chaining": "^7.24.8",
- "@babel/plugin-transform-parameters": "^7.24.7",
- "@babel/plugin-transform-private-methods": "^7.24.7",
- "@babel/plugin-transform-private-property-in-object": "^7.24.7",
- "@babel/plugin-transform-react-display-name": "^7.24.7",
- "@babel/plugin-transform-react-jsx": "^7.25.2",
- "@babel/plugin-transform-react-jsx-self": "^7.24.7",
- "@babel/plugin-transform-react-jsx-source": "^7.24.7",
- "@babel/plugin-transform-regenerator": "^7.24.7",
- "@babel/plugin-transform-runtime": "^7.24.7",
- "@babel/plugin-transform-shorthand-properties": "^7.24.7",
- "@babel/plugin-transform-spread": "^7.24.7",
- "@babel/plugin-transform-sticky-regex": "^7.24.7",
- "@babel/plugin-transform-typescript": "^7.25.2",
- "@babel/plugin-transform-unicode-regex": "^7.24.7",
- "@babel/template": "^7.25.0",
- "@react-native/babel-plugin-codegen": "0.81.5",
- "babel-plugin-syntax-hermes-parser": "0.29.1",
- "babel-plugin-transform-flow-enums": "^0.0.2",
- "react-refresh": "^0.14.0"
- },
- "engines": {
- "node": ">= 20.19.4"
- },
- "peerDependencies": {
- "@babel/core": "*"
- }
- },
- "node_modules/@react-native/codegen": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.5.tgz",
- "integrity": "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/parser": "^7.25.3",
- "glob": "^7.1.1",
- "hermes-parser": "0.29.1",
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1",
- "yargs": "^17.6.2"
- },
- "engines": {
- "node": ">= 20.19.4"
- },
- "peerDependencies": {
- "@babel/core": "*"
- }
- },
- "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/community-cli-plugin": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz",
- "integrity": "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==",
- "license": "MIT",
- "dependencies": {
- "@react-native/dev-middleware": "0.81.5",
- "debug": "^4.4.0",
- "invariant": "^2.2.4",
- "metro": "^0.83.1",
- "metro-config": "^0.83.1",
- "metro-core": "^0.83.1",
- "semver": "^7.1.3"
- },
- "engines": {
- "node": ">= 20.19.4"
- },
- "peerDependencies": {
- "@react-native-community/cli": "*",
- "@react-native/metro-config": "*"
- },
- "peerDependenciesMeta": {
- "@react-native-community/cli": {
- "optional": true
- },
- "@react-native/metro-config": {
- "optional": true
- }
- }
- },
- "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.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.81.5.tgz",
- "integrity": "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 20.19.4"
- }
- },
- "node_modules/@react-native/dev-middleware": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.5.tgz",
- "integrity": "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA==",
- "license": "MIT",
- "dependencies": {
- "@isaacs/ttlcache": "^1.4.1",
- "@react-native/debugger-frontend": "0.81.5",
- "chrome-launcher": "^0.15.2",
- "chromium-edge-launcher": "^0.2.0",
- "connect": "^3.6.5",
- "debug": "^4.4.0",
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1",
- "open": "^7.0.3",
- "serve-static": "^1.16.2",
- "ws": "^6.2.3"
- },
- "engines": {
- "node": ">= 20.19.4"
- }
- },
- "node_modules/@react-native/dev-middleware/node_modules/ws": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
- "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
- "license": "MIT",
- "dependencies": {
- "async-limiter": "~1.0.0"
- }
- },
- "node_modules/@react-native/gradle-plugin": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz",
- "integrity": "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==",
- "license": "MIT",
- "engines": {
- "node": ">= 20.19.4"
- }
- },
- "node_modules/@react-native/js-polyfills": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz",
- "integrity": "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==",
- "license": "MIT",
- "engines": {
- "node": ">= 20.19.4"
- }
- },
- "node_modules/@react-native/normalize-colors": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz",
- "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==",
- "license": "MIT"
- },
- "node_modules/@react-navigation/bottom-tabs": {
- "version": "7.14.0",
- "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.14.0.tgz",
- "integrity": "sha512-oG2VdoInuIyK0o9o90Yo47hTCS+sPyVE7k8eSB37vt3pq3uQxjh8V3xJpsQfOfNlRUXOPB/ejH93nSBlP7ZHmQ==",
- "license": "MIT",
- "dependencies": {
- "@react-navigation/elements": "^2.9.5",
- "color": "^4.2.3",
- "sf-symbols-typescript": "^2.1.0"
- },
- "peerDependencies": {
- "@react-navigation/native": "^7.1.28",
- "react": ">= 18.2.0",
- "react-native": "*",
- "react-native-safe-area-context": ">= 4.0.0",
- "react-native-screens": ">= 4.0.0"
- }
- },
- "node_modules/@react-navigation/core": {
- "version": "7.14.0",
- "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.14.0.tgz",
- "integrity": "sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g==",
- "license": "MIT",
- "dependencies": {
- "@react-navigation/routers": "^7.5.3",
- "escape-string-regexp": "^4.0.0",
- "fast-deep-equal": "^3.1.3",
- "nanoid": "^3.3.11",
- "query-string": "^7.1.3",
- "react-is": "^19.1.0",
- "use-latest-callback": "^0.2.4",
- "use-sync-external-store": "^1.5.0"
- },
- "peerDependencies": {
- "react": ">= 18.2.0"
- }
- },
- "node_modules/@react-navigation/elements": {
- "version": "2.9.5",
- "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.5.tgz",
- "integrity": "sha512-iHZU8rRN1014Upz73AqNVXDvSMZDh5/ktQ1CMe21rdgnOY79RWtHHBp9qOS3VtqlUVYGkuX5GEw5mDt4tKdl0g==",
- "license": "MIT",
- "dependencies": {
- "color": "^4.2.3",
- "use-latest-callback": "^0.2.4",
- "use-sync-external-store": "^1.5.0"
- },
- "peerDependencies": {
- "@react-native-masked-view/masked-view": ">= 0.2.0",
- "@react-navigation/native": "^7.1.28",
- "react": ">= 18.2.0",
- "react-native": "*",
- "react-native-safe-area-context": ">= 4.0.0"
- },
- "peerDependenciesMeta": {
- "@react-native-masked-view/masked-view": {
- "optional": true
- }
- }
- },
- "node_modules/@react-navigation/native": {
- "version": "7.1.28",
- "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz",
- "integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==",
- "license": "MIT",
- "dependencies": {
- "@react-navigation/core": "^7.14.0",
- "escape-string-regexp": "^4.0.0",
- "fast-deep-equal": "^3.1.3",
- "nanoid": "^3.3.11",
- "use-latest-callback": "^0.2.4"
- },
- "peerDependencies": {
- "react": ">= 18.2.0",
- "react-native": "*"
- }
- },
- "node_modules/@react-navigation/native-stack": {
- "version": "7.13.0",
- "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.13.0.tgz",
- "integrity": "sha512-5OOp1IKEd5woHl9hGBU0qCAfrQ4+7Tqej0HzDzGQeXzS8tg9gq84x1qUdRvFk5BXbhuAyvJliY9F1/I07d2X0A==",
- "license": "MIT",
- "dependencies": {
- "@react-navigation/elements": "^2.9.5",
- "color": "^4.2.3",
- "sf-symbols-typescript": "^2.1.0",
- "warn-once": "^0.1.1"
- },
- "peerDependencies": {
- "@react-navigation/native": "^7.1.28",
- "react": ">= 18.2.0",
- "react-native": "*",
- "react-native-safe-area-context": ">= 4.0.0",
- "react-native-screens": ">= 4.0.0"
- }
- },
- "node_modules/@react-navigation/routers": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz",
- "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==",
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11"
- }
- },
- "node_modules/@revenuecat/purchases-js": {
- "version": "1.26.1",
- "resolved": "https://registry.npmjs.org/@revenuecat/purchases-js/-/purchases-js-1.26.1.tgz",
- "integrity": "sha512-+RO6G/AW6/Tm7RFqz2sdkHBAAyV4T2Lj4KGQm8vVUvR8g9hxuDeenCAIKssAqAgLTyq8uqUl4dH6ncbCzHBA0g==",
- "license": "MIT"
- },
- "node_modules/@revenuecat/purchases-js-hybrid-mappings": {
- "version": "17.41.0",
- "resolved": "https://registry.npmjs.org/@revenuecat/purchases-js-hybrid-mappings/-/purchases-js-hybrid-mappings-17.41.0.tgz",
- "integrity": "sha512-rjPX+k1ryiSu//hYpSPU0+M5rryVoNaMxr6raFDyc5bB1My2aCH4b7zUnVBiyGa6ZUayUMJtZxOsz/THzS4WqA==",
- "license": "MIT",
- "dependencies": {
- "@revenuecat/purchases-js": "1.26.1"
- }
- },
- "node_modules/@revenuecat/purchases-typescript-internal": {
- "version": "17.41.0",
- "resolved": "https://registry.npmjs.org/@revenuecat/purchases-typescript-internal/-/purchases-typescript-internal-17.41.0.tgz",
- "integrity": "sha512-1UhQaQcHQ4TxTdMRXagWhN0ecFC61Wy6ula+aSHuUDLJwyKsMl4Dt4vocuo1Ja9o5Jc1Ty4AXx4MnqEnuLrr6A==",
- "license": "MIT"
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz",
- "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz",
- "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz",
- "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz",
- "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz",
- "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz",
- "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz",
- "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz",
- "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz",
- "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz",
- "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz",
- "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz",
- "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz",
- "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@napi-rs/wasm-runtime": "^1.1.1"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
- "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1",
- "@tybys/wasm-util": "^0.10.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz",
- "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz",
- "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz",
- "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@rtsao/scc": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
- "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@sinclair/typebox": {
- "version": "0.27.10",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
- "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
- "license": "MIT"
- },
- "node_modules/@sinonjs/commons": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
- "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "type-detect": "4.0.8"
- }
- },
- "node_modules/@sinonjs/fake-timers": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
- "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@sinonjs/commons": "^3.0.0"
- }
- },
- "node_modules/@standard-schema/spec": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
- "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@supabase/auth-js": {
- "version": "2.98.0",
- "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.98.0.tgz",
- "integrity": "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "2.8.1"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@supabase/functions-js": {
- "version": "2.98.0",
- "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.98.0.tgz",
- "integrity": "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "2.8.1"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@supabase/postgrest-js": {
- "version": "2.98.0",
- "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.98.0.tgz",
- "integrity": "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "2.8.1"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@supabase/realtime-js": {
- "version": "2.98.0",
- "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.98.0.tgz",
- "integrity": "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==",
- "license": "MIT",
- "dependencies": {
- "@types/phoenix": "^1.6.6",
- "@types/ws": "^8.18.1",
- "tslib": "2.8.1",
- "ws": "^8.18.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@supabase/realtime-js/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/@supabase/storage-js": {
- "version": "2.98.0",
- "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.98.0.tgz",
- "integrity": "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==",
- "license": "MIT",
- "dependencies": {
- "iceberg-js": "^0.8.1",
- "tslib": "2.8.1"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@supabase/supabase-js": {
- "version": "2.98.0",
- "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz",
- "integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==",
- "license": "MIT",
- "dependencies": {
- "@supabase/auth-js": "2.98.0",
- "@supabase/functions-js": "2.98.0",
- "@supabase/postgrest-js": "2.98.0",
- "@supabase/realtime-js": "2.98.0",
- "@supabase/storage-js": "2.98.0"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@tanstack/query-async-storage-persister": {
- "version": "5.90.24",
- "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.90.24.tgz",
- "integrity": "sha512-3mljhSqeyu4xqF6BzNAzCe5MbteWPlOWHegLLmgiyppAENjaE0HpJcJAHlKaGhrP5IUhh3zytxW0gSydjmgwIw==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-core": "5.90.20",
- "@tanstack/query-persist-client-core": "5.92.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/query-core": {
- "version": "5.90.20",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
- "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/query-persist-client-core": {
- "version": "5.92.1",
- "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.92.1.tgz",
- "integrity": "sha512-XGzB1lulFrGc8UwQnMI12r71R7ock/XOZvDaz3Fu3xrxCFwLHuFcABAOkIolS/6hFHe0pRdsBRXd4Q8ECqiCug==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-core": "5.90.20"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/react-query": {
- "version": "5.90.21",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
- "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-core": "5.90.20"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "react": "^18 || ^19"
- }
- },
- "node_modules/@tanstack/react-query-persist-client": {
- "version": "5.90.24",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.90.24.tgz",
- "integrity": "sha512-FkfU37vHq61Efr/qGiz+CUNmGfCky1jjsaZFuS5MsWwA9vPHudCwmdirgyTx+RfcQxyHON904q/pc48zrIEhxg==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-persist-client-core": "5.92.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "@tanstack/react-query": "^5.90.21",
- "react": "^18 || ^19"
- }
- },
- "node_modules/@testing-library/jest-native": {
- "version": "5.4.3",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-native/-/jest-native-5.4.3.tgz",
- "integrity": "sha512-/sSDGaOuE+PJ1Z9Kp4u7PQScSVVXGud59I/qsBFFJvIbcn4P6yYw6cBnBmbPF+X9aRIsTJRDl6gzw5ZkJNm66w==",
- "deprecated": "DEPRECATED: This package is no longer maintained.\nPlease use the built-in Jest matchers available in @testing-library/react-native v12.4+.\n\nSee migration guide: https://callstack.github.io/react-native-testing-library/docs/migration/jest-matchers",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.2",
- "jest-diff": "^29.0.1",
- "jest-matcher-utils": "^29.0.1",
- "pretty-format": "^29.0.3",
- "redent": "^3.0.0"
- },
- "peerDependencies": {
- "react": ">=16.0.0",
- "react-native": ">=0.59",
- "react-test-renderer": ">=16.0.0"
- }
- },
- "node_modules/@testing-library/react-native": {
- "version": "13.3.3",
- "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz",
- "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "jest-matcher-utils": "^30.0.5",
- "picocolors": "^1.1.1",
- "pretty-format": "^30.0.5",
- "redent": "^3.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "jest": ">=29.0.0",
- "react": ">=18.2.0",
- "react-native": ">=0.71",
- "react-test-renderer": ">=18.2.0"
- },
- "peerDependenciesMeta": {
- "jest": {
- "optional": true
- }
- }
- },
- "node_modules/@testing-library/react-native/node_modules/@jest/schemas": {
- "version": "30.0.5",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
- "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@sinclair/typebox": "^0.34.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@testing-library/react-native/node_modules/@sinclair/typebox": {
- "version": "0.34.48",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
- "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@testing-library/react-native/node_modules/ansi-styles": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@testing-library/react-native/node_modules/jest-diff": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
- "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@jest/diff-sequences": "30.3.0",
- "@jest/get-type": "30.1.0",
- "chalk": "^4.1.2",
- "pretty-format": "30.3.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@testing-library/react-native/node_modules/jest-matcher-utils": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz",
- "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@jest/get-type": "30.1.0",
- "chalk": "^4.1.2",
- "jest-diff": "30.3.0",
- "pretty-format": "30.3.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@testing-library/react-native/node_modules/pretty-format": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
- "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "30.0.5",
- "ansi-styles": "^5.2.0",
- "react-is": "^18.3.1"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@testing-library/react-native/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==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
- "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
- }
- },
- "node_modules/@types/babel__generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__traverse": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
- "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.28.2"
- }
- },
- "node_modules/@types/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
- }
- },
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "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",
- "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==",
- "license": "MIT",
- "dependencies": {
- "@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",
- "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
- "license": "MIT"
- },
- "node_modules/@types/istanbul-lib-report": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
- "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
- "license": "MIT",
- "dependencies": {
- "@types/istanbul-lib-coverage": "*"
- }
- },
- "node_modules/@types/istanbul-reports": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
- "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
- "license": "MIT",
- "dependencies": {
- "@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/json5": {
- "version": "0.0.29",
- "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "25.2.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
- "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~7.16.0"
- }
- },
- "node_modules/@types/phoenix": {
- "version": "1.6.7",
- "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
- "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "19.1.17",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
- "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/stack-utils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
- "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
- "license": "MIT"
- },
- "node_modules/@types/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/yargs": {
- "version": "17.0.35",
- "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
- "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/@types/yargs-parser": {
- "version": "21.0.3",
- "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
- "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
- "license": "MIT"
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
- "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.56.0",
- "@typescript-eslint/type-utils": "8.56.0",
- "@typescript-eslint/utils": "8.56.0",
- "@typescript-eslint/visitor-keys": "8.56.0",
- "ignore": "^7.0.5",
- "natural-compare": "^1.4.0",
- "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-eslint/parser": "^8.56.0",
- "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.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
- "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.56.0",
- "@typescript-eslint/types": "8.56.0",
- "@typescript-eslint/typescript-estree": "8.56.0",
- "@typescript-eslint/visitor-keys": "8.56.0",
- "debug": "^4.4.3"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.0.0"
- }
- },
- "node_modules/@typescript-eslint/project-service": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz",
- "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.56.0",
- "@typescript-eslint/types": "^8.56.0",
- "debug": "^4.4.3"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz",
- "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.56.0",
- "@typescript-eslint/visitor-keys": "8.56.0"
- },
- "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.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz",
- "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==",
- "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.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz",
- "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.56.0",
- "@typescript-eslint/typescript-estree": "8.56.0",
- "@typescript-eslint/utils": "8.56.0",
- "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.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz",
- "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==",
- "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.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz",
- "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/project-service": "8.56.0",
- "@typescript-eslint/tsconfig-utils": "8.56.0",
- "@typescript-eslint/types": "8.56.0",
- "@typescript-eslint/visitor-keys": "8.56.0",
- "debug": "^4.4.3",
- "minimatch": "^9.0.5",
- "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/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "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.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz",
- "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.56.0",
- "@typescript-eslint/types": "8.56.0",
- "@typescript-eslint/typescript-estree": "8.56.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/visitor-keys": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz",
- "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.56.0",
- "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/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz",
- "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- },
- "funding": {
- "url": "https://opencollective.com/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/@unrs/resolver-binding-android-arm-eabi": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
- "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@unrs/resolver-binding-android-arm64": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
- "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@unrs/resolver-binding-darwin-arm64": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
- "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@unrs/resolver-binding-darwin-x64": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
- "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@unrs/resolver-binding-freebsd-x64": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
- "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
- "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
- "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
- "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
- "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
- "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
- "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
- "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
- "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
- "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-x64-musl": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
- "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-wasm32-wasi": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
- "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@napi-rs/wasm-runtime": "^0.2.11"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
- "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
- "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
- "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@urql/core": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz",
- "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==",
- "license": "MIT",
- "dependencies": {
- "@0no-co/graphql.web": "^1.0.13",
- "wonka": "^6.3.2"
- }
- },
- "node_modules/@urql/exchange-retry": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz",
- "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==",
- "license": "MIT",
- "dependencies": {
- "@urql/core": "^5.1.2",
- "wonka": "^6.3.2"
- },
- "peerDependencies": {
- "@urql/core": "^5.0.0"
- }
- },
- "node_modules/@vitest/coverage-v8": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz",
- "integrity": "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@bcoe/v8-coverage": "^1.0.2",
- "@vitest/utils": "4.1.1",
- "ast-v8-to-istanbul": "^1.0.0",
- "istanbul-lib-coverage": "^3.2.2",
- "istanbul-lib-report": "^3.0.1",
- "istanbul-reports": "^3.2.0",
- "magicast": "^0.5.2",
- "obug": "^2.1.1",
- "std-env": "^4.0.0-rc.1",
- "tinyrainbow": "^3.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@vitest/browser": "4.1.1",
- "vitest": "4.1.1"
- },
- "peerDependenciesMeta": {
- "@vitest/browser": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/expect": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz",
- "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@standard-schema/spec": "^1.1.0",
- "@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.1",
- "@vitest/utils": "4.1.1",
- "chai": "^6.2.2",
- "tinyrainbow": "^3.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz",
- "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "4.1.1",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.21"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz",
- "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^3.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz",
- "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "4.1.1",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz",
- "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "4.1.1",
- "@vitest/utils": "4.1.1",
- "magic-string": "^0.30.21",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz",
- "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz",
- "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "4.1.1",
- "convert-source-map": "^2.0.0",
- "tinyrainbow": "^3.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "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": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
- "license": "MIT",
- "dependencies": {
- "event-target-shim": "^5.0.0"
- },
- "engines": {
- "node": ">=6.5"
- }
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "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",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/anser": {
- "version": "1.4.10",
- "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
- "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==",
- "license": "MIT"
- },
- "node_modules/ansi-escapes": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
- "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
- "license": "MIT",
- "dependencies": {
- "type-fest": "^0.21.3"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-escapes/node_modules/type-fest": {
- "version": "0.21.3",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
- "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/any-promise": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "license": "MIT"
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/arg": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "license": "MIT"
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "license": "Python-2.0"
- },
- "node_modules/aria-hidden": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
- "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/array-buffer-byte-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
- "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "is-array-buffer": "^3.0.5"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array-includes": {
- "version": "3.1.9",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
- "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.24.0",
- "es-object-atoms": "^1.1.1",
- "get-intrinsic": "^1.3.0",
- "is-string": "^1.1.1",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.findlast": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
- "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.findlastindex": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
- "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.9",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "es-shim-unscopables": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.flat": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
- "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.flatmap": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
- "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.tosorted": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
- "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.3",
- "es-errors": "^1.3.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/arraybuffer.prototype.slice": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
- "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-buffer-byte-length": "^1.0.1",
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "is-array-buffer": "^3.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
- "license": "MIT"
- },
- "node_modules/assert": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
- "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "is-nan": "^1.3.2",
- "object-is": "^1.1.5",
- "object.assign": "^4.1.4",
- "util": "^0.12.5"
- }
- },
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/ast-v8-to-istanbul": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
- "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.31",
- "estree-walker": "^3.0.3",
- "js-tokens": "^10.0.0"
- }
- },
- "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/async-function": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
- "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/async-limiter": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
- "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
- "license": "MIT"
- },
- "node_modules/available-typed-arrays": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
- "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
- "license": "MIT",
- "dependencies": {
- "possible-typed-array-names": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/babel-jest": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
- "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
- "license": "MIT",
- "dependencies": {
- "@jest/transform": "^29.7.0",
- "@types/babel__core": "^7.1.14",
- "babel-plugin-istanbul": "^6.1.1",
- "babel-preset-jest": "^29.6.3",
- "chalk": "^4.0.0",
- "graceful-fs": "^4.2.9",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.8.0"
- }
- },
- "node_modules/babel-plugin-istanbul": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
- "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.0.0",
- "@istanbuljs/load-nyc-config": "^1.0.0",
- "@istanbuljs/schema": "^0.1.2",
- "istanbul-lib-instrument": "^5.0.4",
- "test-exclude": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/babel-plugin-jest-hoist": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
- "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.3.3",
- "@babel/types": "^7.3.3",
- "@types/babel__core": "^7.1.14",
- "@types/babel__traverse": "^7.0.6"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/babel-plugin-polyfill-corejs2": {
- "version": "0.4.15",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz",
- "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==",
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.28.6",
- "@babel/helper-define-polyfill-provider": "^0.6.6",
- "semver": "^6.3.1"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
- }
- },
- "node_modules/babel-plugin-polyfill-corejs3": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
- "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.6.5",
- "core-js-compat": "^3.43.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
- }
- },
- "node_modules/babel-plugin-polyfill-regenerator": {
- "version": "0.6.6",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz",
- "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.6.6"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
- }
- },
- "node_modules/babel-plugin-react-compiler": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
- "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.26.0"
- }
- },
- "node_modules/babel-plugin-react-native-web": {
- "version": "0.21.2",
- "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz",
- "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==",
- "license": "MIT"
- },
- "node_modules/babel-plugin-syntax-hermes-parser": {
- "version": "0.29.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.29.1.tgz",
- "integrity": "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==",
- "license": "MIT",
- "dependencies": {
- "hermes-parser": "0.29.1"
- }
- },
- "node_modules/babel-plugin-transform-flow-enums": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz",
- "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/plugin-syntax-flow": "^7.12.1"
- }
- },
- "node_modules/babel-preset-current-node-syntax": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
- "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==",
- "license": "MIT",
- "dependencies": {
- "@babel/plugin-syntax-async-generators": "^7.8.4",
- "@babel/plugin-syntax-bigint": "^7.8.3",
- "@babel/plugin-syntax-class-properties": "^7.12.13",
- "@babel/plugin-syntax-class-static-block": "^7.14.5",
- "@babel/plugin-syntax-import-attributes": "^7.24.7",
- "@babel/plugin-syntax-import-meta": "^7.10.4",
- "@babel/plugin-syntax-json-strings": "^7.8.3",
- "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-syntax-numeric-separator": "^7.10.4",
- "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
- "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3",
- "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
- "@babel/plugin-syntax-top-level-await": "^7.14.5"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0 || ^8.0.0-0"
- }
- },
- "node_modules/babel-preset-expo": {
- "version": "54.0.10",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz",
- "integrity": "sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw==",
- "license": "MIT",
- "dependencies": {
- "@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.81.5",
- "babel-plugin-react-compiler": "^1.0.0",
- "babel-plugin-react-native-web": "~0.21.0",
- "babel-plugin-syntax-hermes-parser": "^0.29.1",
- "babel-plugin-transform-flow-enums": "^0.0.2",
- "debug": "^4.3.4",
- "resolve-from": "^5.0.0"
- },
- "peerDependencies": {
- "@babel/runtime": "^7.20.0",
- "expo": "*",
- "react-refresh": ">=0.14.0 <1.0.0"
- },
- "peerDependenciesMeta": {
- "@babel/runtime": {
- "optional": true
- },
- "expo": {
- "optional": true
- }
- }
- },
- "node_modules/babel-preset-jest": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
- "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
- "license": "MIT",
- "dependencies": {
- "babel-plugin-jest-hoist": "^29.6.3",
- "babel-preset-current-node-syntax": "^1.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/badgin": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
- "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
- "license": "MIT"
- },
- "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/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/baseline-browser-mapping": {
- "version": "2.9.19",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
- "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.js"
- }
- },
- "node_modules/better-opn": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz",
- "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==",
- "license": "MIT",
- "dependencies": {
- "open": "^8.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/better-opn/node_modules/open": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
- "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
- "license": "MIT",
- "dependencies": {
- "define-lazy-prop": "^2.0.0",
- "is-docker": "^2.1.1",
- "is-wsl": "^2.2.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/bidi-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
- "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "require-from-string": "^2.0.2"
- }
- },
- "node_modules/big-integer": {
- "version": "1.6.52",
- "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
- "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
- "license": "Unlicense",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/boolbase": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
- "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
- "license": "ISC"
- },
- "node_modules/bplist-creator": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
- "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==",
- "license": "MIT",
- "dependencies": {
- "stream-buffers": "2.2.x"
- }
- },
- "node_modules/bplist-parser": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz",
- "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==",
- "license": "MIT",
- "dependencies": {
- "big-integer": "1.6.x"
- },
- "engines": {
- "node": ">= 5.10.0"
- }
- },
- "node_modules/brace-expansion": {
- "version": "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/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.28.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
- "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "baseline-browser-mapping": "^2.9.0",
- "caniuse-lite": "^1.0.30001759",
- "electron-to-chromium": "^1.5.263",
- "node-releases": "^2.0.27",
- "update-browserslist-db": "^1.2.0"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/bser": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
- "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "node-int64": "^0.4.0"
- }
- },
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "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": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "license": "MIT"
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
- "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.0",
- "es-define-property": "^1.0.0",
- "get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/camelcase": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
- "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001770",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
- "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/chai": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
- "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/chownr": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
- "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/chrome-launcher": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
- "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@types/node": "*",
- "escape-string-regexp": "^4.0.0",
- "is-wsl": "^2.2.0",
- "lighthouse-logger": "^1.0.0"
- },
- "bin": {
- "print-chrome-path": "bin/print-chrome-path.js"
- },
- "engines": {
- "node": ">=12.13.0"
- }
- },
- "node_modules/chromium-edge-launcher": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz",
- "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@types/node": "*",
- "escape-string-regexp": "^4.0.0",
- "is-wsl": "^2.2.0",
- "lighthouse-logger": "^1.0.0",
- "mkdirp": "^1.0.4",
- "rimraf": "^3.0.2"
- }
- },
- "node_modules/ci-info": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
- "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
- "license": "MIT"
- },
- "node_modules/cli-cursor": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
- "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==",
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^2.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/cli-spinners": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
- "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/client-only": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
- "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
- "license": "MIT"
- },
- "node_modules/cliui": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.1",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
- "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/color": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
- "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1",
- "color-string": "^1.9.0"
- },
- "engines": {
- "node": ">=12.5.0"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT"
- },
- "node_modules/color-string": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
- "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
- "node_modules/commander": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
- "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
- "license": "MIT",
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/compressible": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
- "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
- "license": "MIT",
- "dependencies": {
- "mime-db": ">= 1.43.0 < 2"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/compression": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
- "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
- "license": "MIT",
- "dependencies": {
- "bytes": "3.1.2",
- "compressible": "~2.0.18",
- "debug": "2.6.9",
- "negotiator": "~0.6.4",
- "on-headers": "~1.1.0",
- "safe-buffer": "5.2.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/compression/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/compression/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/compression/node_modules/negotiator": {
- "version": "0.6.4",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
- "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "license": "MIT"
- },
- "node_modules/connect": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
- "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "finalhandler": "1.1.2",
- "parseurl": "~1.3.3",
- "utils-merge": "1.0.1"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/connect/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/connect/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "license": "MIT"
- },
- "node_modules/core-js-compat": {
- "version": "3.48.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
- "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==",
- "license": "MIT",
- "dependencies": {
- "browserslist": "^4.28.1"
- },
- "funding": {
- "type": "opencollective",
- "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",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/crypto-random-string": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
- "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/css-in-js-utils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
- "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
- "license": "MIT",
- "dependencies": {
- "hyphenate-style-name": "^1.0.3"
- }
- },
- "node_modules/css-select": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
- "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^6.1.0",
- "domhandler": "^5.0.2",
- "domutils": "^3.0.1",
- "nth-check": "^2.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/css-tree": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
- "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mdn-data": "2.27.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
- }
- },
- "node_modules/css-what": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
- "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">= 6"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/data-urls": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
- "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "whatwg-mimetype": "^5.0.0",
- "whatwg-url": "^16.0.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- }
- },
- "node_modules/data-urls/node_modules/tr46": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
- "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "punycode": "^2.3.1"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/data-urls/node_modules/webidl-conversions": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
- "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/data-urls/node_modules/whatwg-url": {
- "version": "16.0.1",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
- "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@exodus/bytes": "^1.11.0",
- "tr46": "^6.0.0",
- "webidl-conversions": "^8.0.1"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- }
- },
- "node_modules/data-view-buffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
- "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/data-view-byte-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
- "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/inspect-js"
- }
- },
- "node_modules/data-view-byte-offset": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
- "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/decimal.js": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
- "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/decode-uri-component": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
- "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "license": "MIT",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/deepmerge": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
- "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/defaults": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
- "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
- "license": "MIT",
- "dependencies": {
- "clone": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/define-data-property": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
- "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
- "license": "MIT",
- "dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "gopd": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/define-lazy-prop": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
- "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/define-properties": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
- "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
- "license": "MIT",
- "dependencies": {
- "define-data-property": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/detect-node-es": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
- "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
- "license": "MIT"
- },
- "node_modules/diff-sequences": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
- "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/dom-serializer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
- "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
- "license": "MIT",
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.2",
- "entities": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/domelementtype": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
- "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "license": "BSD-2-Clause"
- },
- "node_modules/domhandler": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
- "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "domelementtype": "^2.3.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/domutils": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
- "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "dom-serializer": "^2.0.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/dotenv": {
- "version": "16.4.7",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
- "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "node_modules/dotenv-expand": {
- "version": "11.0.7",
- "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
- "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "dotenv": "^16.4.5"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.286",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
- "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
- "license": "ISC"
- },
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT"
- },
- "node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/env-editor": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
- "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/error-stack-parser": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
- "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
- "license": "MIT",
- "dependencies": {
- "stackframe": "^1.3.4"
- }
- },
- "node_modules/es-abstract": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
- "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-buffer-byte-length": "^1.0.2",
- "arraybuffer.prototype.slice": "^1.0.4",
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "data-view-buffer": "^1.0.2",
- "data-view-byte-length": "^1.0.2",
- "data-view-byte-offset": "^1.0.1",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "es-set-tostringtag": "^2.1.0",
- "es-to-primitive": "^1.3.0",
- "function.prototype.name": "^1.1.8",
- "get-intrinsic": "^1.3.0",
- "get-proto": "^1.0.1",
- "get-symbol-description": "^1.1.0",
- "globalthis": "^1.0.4",
- "gopd": "^1.2.0",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "internal-slot": "^1.1.0",
- "is-array-buffer": "^3.0.5",
- "is-callable": "^1.2.7",
- "is-data-view": "^1.0.2",
- "is-negative-zero": "^2.0.3",
- "is-regex": "^1.2.1",
- "is-set": "^2.0.3",
- "is-shared-array-buffer": "^1.0.4",
- "is-string": "^1.1.1",
- "is-typed-array": "^1.1.15",
- "is-weakref": "^1.1.1",
- "math-intrinsics": "^1.1.0",
- "object-inspect": "^1.13.4",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.7",
- "own-keys": "^1.0.1",
- "regexp.prototype.flags": "^1.5.4",
- "safe-array-concat": "^1.1.3",
- "safe-push-apply": "^1.0.0",
- "safe-regex-test": "^1.1.0",
- "set-proto": "^1.0.0",
- "stop-iteration-iterator": "^1.1.0",
- "string.prototype.trim": "^1.2.10",
- "string.prototype.trimend": "^1.0.9",
- "string.prototype.trimstart": "^1.0.8",
- "typed-array-buffer": "^1.0.3",
- "typed-array-byte-length": "^1.0.3",
- "typed-array-byte-offset": "^1.0.4",
- "typed-array-length": "^1.0.7",
- "unbox-primitive": "^1.1.0",
- "which-typed-array": "^1.1.19"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-iterator-helpers": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
- "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.24.1",
- "es-errors": "^1.3.0",
- "es-set-tostringtag": "^2.1.0",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.3.0",
- "globalthis": "^1.0.4",
- "gopd": "^1.2.0",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.2.0",
- "has-symbols": "^1.1.0",
- "internal-slot": "^1.1.0",
- "iterator.prototype": "^1.1.5",
- "safe-array-concat": "^1.1.3"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-module-lexer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
- "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-shim-unscopables": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
- "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-to-primitive": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
- "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-callable": "^1.2.7",
- "is-date-object": "^1.0.5",
- "is-symbol": "^1.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
- "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.8.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.1",
- "@eslint/config-helpers": "^0.4.2",
- "@eslint/core": "^0.17.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.39.2",
- "@eslint/plugin-kit": "^0.4.1",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.2",
- "@types/estree": "^1.0.6",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.4.0",
- "eslint-visitor-keys": "^4.2.1",
- "espree": "^10.4.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-config-expo": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-10.0.0.tgz",
- "integrity": "sha512-/XC/DvniUWTzU7Ypb/cLDhDD4DXqEio4lug1ObD/oQ9Hcx3OVOR8Mkp4u6U4iGoZSJyIQmIk3WVHe/P1NYUXKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "^8.18.2",
- "@typescript-eslint/parser": "^8.18.2",
- "eslint-import-resolver-typescript": "^3.6.3",
- "eslint-plugin-expo": "^1.0.0",
- "eslint-plugin-import": "^2.30.0",
- "eslint-plugin-react": "^7.37.3",
- "eslint-plugin-react-hooks": "^5.1.0",
- "globals": "^16.0.0"
- },
- "peerDependencies": {
- "eslint": ">=8.10"
- }
- },
- "node_modules/eslint-config-expo/node_modules/globals": {
- "version": "16.5.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
- "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint-import-resolver-node": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
- "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^3.2.7",
- "is-core-module": "^2.13.0",
- "resolve": "^1.22.4"
- }
- },
- "node_modules/eslint-import-resolver-node/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-import-resolver-typescript": {
- "version": "3.10.1",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
- "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "@nolyfill/is-core-module": "1.0.39",
- "debug": "^4.4.0",
- "get-tsconfig": "^4.10.0",
- "is-bun-module": "^2.0.0",
- "stable-hash": "^0.0.5",
- "tinyglobby": "^0.2.13",
- "unrs-resolver": "^1.6.2"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint-import-resolver-typescript"
- },
- "peerDependencies": {
- "eslint": "*",
- "eslint-plugin-import": "*",
- "eslint-plugin-import-x": "*"
- },
- "peerDependenciesMeta": {
- "eslint-plugin-import": {
- "optional": true
- },
- "eslint-plugin-import-x": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-module-utils": {
- "version": "2.12.1",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
- "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^3.2.7"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-module-utils/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-plugin-expo": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.0.0.tgz",
- "integrity": "sha512-qLtunR+cNFtC+jwYCBia5c/PJurMjSLMOV78KrEOyQK02ohZapU4dCFFnS2hfrJuw0zxfsjVkjqg3QBqi933QA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "^8.29.1",
- "@typescript-eslint/utils": "^8.29.1",
- "eslint": "^9.24.0"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "peerDependencies": {
- "eslint": ">=8.10"
- }
- },
- "node_modules/eslint-plugin-import": {
- "version": "2.32.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
- "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@rtsao/scc": "^1.1.0",
- "array-includes": "^3.1.9",
- "array.prototype.findlastindex": "^1.2.6",
- "array.prototype.flat": "^1.3.3",
- "array.prototype.flatmap": "^1.3.3",
- "debug": "^3.2.7",
- "doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.9",
- "eslint-module-utils": "^2.12.1",
- "hasown": "^2.0.2",
- "is-core-module": "^2.16.1",
- "is-glob": "^4.0.3",
- "minimatch": "^3.1.2",
- "object.fromentries": "^2.0.8",
- "object.groupby": "^1.0.3",
- "object.values": "^1.2.1",
- "semver": "^6.3.1",
- "string.prototype.trimend": "^1.0.9",
- "tsconfig-paths": "^3.15.0"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-plugin-react": {
- "version": "7.37.5",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
- "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-includes": "^3.1.8",
- "array.prototype.findlast": "^1.2.5",
- "array.prototype.flatmap": "^1.3.3",
- "array.prototype.tosorted": "^1.1.4",
- "doctrine": "^2.1.0",
- "es-iterator-helpers": "^1.2.1",
- "estraverse": "^5.3.0",
- "hasown": "^2.0.2",
- "jsx-ast-utils": "^2.4.1 || ^3.0.0",
- "minimatch": "^3.1.2",
- "object.entries": "^1.1.9",
- "object.fromentries": "^2.0.8",
- "object.values": "^1.2.1",
- "prop-types": "^15.8.1",
- "resolve": "^2.0.0-next.5",
- "semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.12",
- "string.prototype.repeat": "^1.0.0"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
- "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/resolve": {
- "version": "2.0.0-next.6",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
- "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "is-core-module": "^2.16.1",
- "node-exports-info": "^1.6.0",
- "object-keys": "^1.1.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/eslint-scope": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
- "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/espree": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
- "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.15.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "license": "BSD-2-Clause",
- "bin": {
- "esparse": "bin/esparse.js",
- "esvalidate": "bin/esvalidate.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/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/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.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",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/exec-async": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
- "integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==",
- "license": "MIT"
- },
- "node_modules/expect-type": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
- "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/expo": {
- "version": "54.0.33",
- "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
- "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.20.0",
- "@expo/cli": "54.0.23",
- "@expo/config": "~12.0.13",
- "@expo/config-plugins": "~54.0.4",
- "@expo/devtools": "0.1.8",
- "@expo/fingerprint": "0.15.4",
- "@expo/metro": "~54.2.0",
- "@expo/metro-config": "54.0.14",
- "@expo/vector-icons": "^15.0.3",
- "@ungap/structured-clone": "^1.3.0",
- "babel-preset-expo": "~54.0.10",
- "expo-asset": "~12.0.12",
- "expo-constants": "~18.0.13",
- "expo-file-system": "~19.0.21",
- "expo-font": "~14.0.11",
- "expo-keep-awake": "~15.0.8",
- "expo-modules-autolinking": "3.0.24",
- "expo-modules-core": "3.0.29",
- "pretty-format": "^29.7.0",
- "react-refresh": "^0.14.2",
- "whatwg-url-without-unicode": "8.0.0-3"
- },
- "bin": {
- "expo": "bin/cli",
- "expo-modules-autolinking": "bin/autolinking",
- "fingerprint": "bin/fingerprint"
- },
- "peerDependencies": {
- "@expo/dom-webview": "*",
- "@expo/metro-runtime": "*",
- "react": "*",
- "react-native": "*",
- "react-native-webview": "*"
- },
- "peerDependenciesMeta": {
- "@expo/dom-webview": {
- "optional": true
- },
- "@expo/metro-runtime": {
- "optional": true
- },
- "react-native-webview": {
- "optional": true
- }
- }
- },
- "node_modules/expo-application": {
- "version": "7.0.8",
- "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
- "integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "node_modules/expo-asset": {
- "version": "12.0.12",
- "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
- "integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==",
- "license": "MIT",
- "dependencies": {
- "@expo/image-utils": "^0.8.8",
- "expo-constants": "~18.0.12"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-av": {
- "version": "16.0.8",
- "resolved": "https://registry.npmjs.org/expo-av/-/expo-av-16.0.8.tgz",
- "integrity": "sha512-cmVPftGR/ca7XBgs7R6ky36lF3OC0/MM/lpgX/yXqfv0jASTsh7AYX9JxHCwFmF+Z6JEB1vne9FDx4GiLcGreQ==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*",
- "react-native-web": "*"
- },
- "peerDependenciesMeta": {
- "react-native-web": {
- "optional": true
- }
- }
- },
- "node_modules/expo-blur": {
- "version": "15.0.8",
- "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-15.0.8.tgz",
- "integrity": "sha512-rWyE1NBRZEu9WD+X+5l7gyPRszw7n12cW3IRNAb5i6KFzaBp8cxqT5oeaphJapqURvcqhkOZn2k5EtBSbsuU7w==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-constants": {
- "version": "18.0.13",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
- "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
- "license": "MIT",
- "dependencies": {
- "@expo/config": "~12.0.13",
- "@expo/env": "~2.0.8"
- },
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-device": {
- "version": "8.0.10",
- "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
- "integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
- "license": "MIT",
- "dependencies": {
- "ua-parser-js": "^0.7.33"
- },
- "peerDependencies": {
- "expo": "*"
- }
- },
- "node_modules/expo-device/node_modules/ua-parser-js": {
- "version": "0.7.41",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
- "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
- "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/expo-file-system": {
- "version": "19.0.21",
- "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
- "integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-font": {
- "version": "14.0.11",
- "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
- "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
- "license": "MIT",
- "dependencies": {
- "fontfaceobserver": "^2.1.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-gl": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/expo-gl/-/expo-gl-16.0.10.tgz",
- "integrity": "sha512-/pPlSJvfmrGuW+UXBRVADr52nhiHFwRGXB8shhQb+b6KKreCuTmQZUASznAXS6YaHNjkOghmkaUW0hRnyiAwBQ==",
- "license": "MIT",
- "dependencies": {
- "invariant": "^2.2.4"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-dom": "*",
- "react-native": "*",
- "react-native-reanimated": "*",
- "react-native-web": "*"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- },
- "react-native-reanimated": {
- "optional": true
- },
- "react-native-web": {
- "optional": true
- }
- }
- },
- "node_modules/expo-haptics": {
- "version": "15.0.8",
- "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.8.tgz",
- "integrity": "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "node_modules/expo-image": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-3.0.11.tgz",
- "integrity": "sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*",
- "react-native-web": "*"
- },
- "peerDependenciesMeta": {
- "react-native-web": {
- "optional": true
- }
- }
- },
- "node_modules/expo-keep-awake": {
- "version": "15.0.8",
- "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz",
- "integrity": "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*"
- }
- },
- "node_modules/expo-linear-gradient": {
- "version": "15.0.8",
- "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.8.tgz",
- "integrity": "sha512-V2d8Wjn0VzhPHO+rrSBtcl+Fo+jUUccdlmQ6OoL9/XQB7Qk3d9lYrqKDJyccwDxmQT10JdST3Tmf2K52NLc3kw==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-linking": {
- "version": "8.0.11",
- "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz",
- "integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==",
- "license": "MIT",
- "dependencies": {
- "expo-constants": "~18.0.12",
- "invariant": "^2.2.4"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-localization": {
- "version": "17.0.8",
- "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.8.tgz",
- "integrity": "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g==",
- "license": "MIT",
- "dependencies": {
- "rtl-detect": "^1.0.2"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*"
- }
- },
- "node_modules/expo-modules-autolinking": {
- "version": "3.0.24",
- "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
- "integrity": "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==",
- "license": "MIT",
- "dependencies": {
- "@expo/spawn-async": "^1.7.2",
- "chalk": "^4.1.0",
- "commander": "^7.2.0",
- "require-from-string": "^2.0.2",
- "resolve-from": "^5.0.0"
- },
- "bin": {
- "expo-modules-autolinking": "bin/expo-modules-autolinking.js"
- }
- },
- "node_modules/expo-modules-core": {
- "version": "3.0.29",
- "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.29.tgz",
- "integrity": "sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q==",
- "license": "MIT",
- "dependencies": {
- "invariant": "^2.2.4"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-network": {
- "version": "8.0.8",
- "resolved": "https://registry.npmjs.org/expo-network/-/expo-network-8.0.8.tgz",
- "integrity": "sha512-dgrL8UHAmWofqeY4UEjWskCl/RoQAM0DG6PZR8xz2WZt+6aQEboQgFRXowCfhbKZ71d16sNuKXtwBEsp2DtdNw==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*"
- }
- },
- "node_modules/expo-notifications": {
- "version": "0.32.16",
- "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz",
- "integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==",
- "license": "MIT",
- "dependencies": {
- "@expo/image-utils": "^0.8.8",
- "@ide/backoff": "^1.0.0",
- "abort-controller": "^3.0.0",
- "assert": "^2.0.0",
- "badgin": "^1.1.5",
- "expo-application": "~7.0.8",
- "expo-constants": "~18.0.13"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-router": {
- "version": "6.0.23",
- "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.23.tgz",
- "integrity": "sha512-qCxVAiCrCyu0npky6azEZ6dJDMt77OmCzEbpF6RbUTlfkaCA417LvY14SBkk0xyGruSxy/7pvJOI6tuThaUVCA==",
- "license": "MIT",
- "dependencies": {
- "@expo/metro-runtime": "^6.1.2",
- "@expo/schema-utils": "^0.1.8",
- "@radix-ui/react-slot": "1.2.0",
- "@radix-ui/react-tabs": "^1.1.12",
- "@react-navigation/bottom-tabs": "^7.4.0",
- "@react-navigation/native": "^7.1.8",
- "@react-navigation/native-stack": "^7.3.16",
- "client-only": "^0.0.1",
- "debug": "^4.3.4",
- "escape-string-regexp": "^4.0.0",
- "expo-server": "^1.0.5",
- "fast-deep-equal": "^3.1.3",
- "invariant": "^2.2.4",
- "nanoid": "^3.3.8",
- "query-string": "^7.1.3",
- "react-fast-compare": "^3.2.2",
- "react-native-is-edge-to-edge": "^1.1.6",
- "semver": "~7.6.3",
- "server-only": "^0.0.1",
- "sf-symbols-typescript": "^2.1.0",
- "shallowequal": "^1.1.0",
- "use-latest-callback": "^0.2.1",
- "vaul": "^1.1.2"
- },
- "peerDependencies": {
- "@expo/metro-runtime": "^6.1.2",
- "@react-navigation/drawer": "^7.5.0",
- "@testing-library/react-native": ">= 12.0.0",
- "expo": "*",
- "expo-constants": "^18.0.13",
- "expo-linking": "^8.0.11",
- "react": "*",
- "react-dom": "*",
- "react-native": "*",
- "react-native-gesture-handler": "*",
- "react-native-reanimated": "*",
- "react-native-safe-area-context": ">= 5.4.0",
- "react-native-screens": "*",
- "react-native-web": "*",
- "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4"
- },
- "peerDependenciesMeta": {
- "@react-navigation/drawer": {
- "optional": true
- },
- "@testing-library/react-native": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "react-native-gesture-handler": {
- "optional": true
- },
- "react-native-reanimated": {
- "optional": true
- },
- "react-native-web": {
- "optional": true
- },
- "react-server-dom-webpack": {
- "optional": true
- }
- }
- },
- "node_modules/expo-router/node_modules/@radix-ui/react-collection": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
- "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3"
- },
- "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/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/expo-router/node_modules/@radix-ui/react-presence": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
- "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "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/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.3"
- },
- "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/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/expo-router/node_modules/@radix-ui/react-roving-focus": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
- "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@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/@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-server": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz",
- "integrity": "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==",
- "license": "MIT",
- "engines": {
- "node": ">=20.16.0"
- }
- },
- "node_modules/expo-sharing": {
- "version": "14.0.8",
- "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.8.tgz",
- "integrity": "sha512-A1pPr2iBrxypFDCWVAESk532HK+db7MFXbvO2sCV9ienaFXAk7lIBm6bkqgE6vzRd9O3RGdEGzYx80cYlc089Q==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "node_modules/expo-splash-screen": {
- "version": "31.0.13",
- "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.13.tgz",
- "integrity": "sha512-1epJLC1cDlwwj089R2h8cxaU5uk4ONVAC+vzGiTZH4YARQhL4Stlz1MbR6yAS173GMosvkE6CAeihR7oIbCkDA==",
- "license": "MIT",
- "dependencies": {
- "@expo/prebuild-config": "^54.0.8"
- },
- "peerDependencies": {
- "expo": "*"
- }
- },
- "node_modules/expo-status-bar": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz",
- "integrity": "sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw==",
- "license": "MIT",
- "dependencies": {
- "react-native-is-edge-to-edge": "^1.2.1"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-store-review": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/expo-store-review/-/expo-store-review-9.0.9.tgz",
- "integrity": "sha512-99vS7edXlKzPcdjrzVlMQWc4zOyq4khQfFjhNqJgpGP+AgRn4U0LaZkHIrVjmzolryD3rcHJSiUQH9Vi0sD0MQ==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-symbols": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-1.0.8.tgz",
- "integrity": "sha512-7bNjK350PaQgxBf0owpmSYkdZIpdYYmaPttDBb2WIp6rIKtcEtdzdfmhsc2fTmjBURHYkg36+eCxBFXO25/1hw==",
- "license": "MIT",
- "dependencies": {
- "sf-symbols-typescript": "^2.0.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-system-ui": {
- "version": "6.0.9",
- "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-6.0.9.tgz",
- "integrity": "sha512-eQTYGzw1V4RYiYHL9xDLYID3Wsec2aZS+ypEssmF64D38aDrqbDgz1a2MSlHLQp2jHXSs3FvojhZ9FVela1Zcg==",
- "license": "MIT",
- "dependencies": {
- "@react-native/normalize-colors": "0.81.5",
- "debug": "^4.3.2"
- },
- "peerDependencies": {
- "expo": "*",
- "react-native": "*",
- "react-native-web": "*"
- },
- "peerDependenciesMeta": {
- "react-native-web": {
- "optional": true
- }
- }
- },
- "node_modules/expo-video": {
- "version": "3.0.16",
- "resolved": "https://registry.npmjs.org/expo-video/-/expo-video-3.0.16.tgz",
- "integrity": "sha512-H1HlxcHGomZItqisGfW3YL/G9BHtNBfVSimDJcLuyxyU87wFnV8loO9tCjuhufkfh/aTa2sW5BYAjLjg9DvnBQ==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-web-browser": {
- "version": "15.0.10",
- "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz",
- "integrity": "sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo/node_modules/@expo/cli": {
- "version": "54.0.23",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz",
- "integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==",
- "license": "MIT",
- "dependencies": {
- "@0no-co/graphql.web": "^1.0.8",
- "@expo/code-signing-certificates": "^0.0.6",
- "@expo/config": "~12.0.13",
- "@expo/config-plugins": "~54.0.4",
- "@expo/devcert": "^1.2.1",
- "@expo/env": "~2.0.8",
- "@expo/image-utils": "^0.8.8",
- "@expo/json-file": "^10.0.8",
- "@expo/metro": "~54.2.0",
- "@expo/metro-config": "~54.0.14",
- "@expo/osascript": "^2.3.8",
- "@expo/package-manager": "^1.9.10",
- "@expo/plist": "^0.4.8",
- "@expo/prebuild-config": "^54.0.8",
- "@expo/schema-utils": "^0.1.8",
- "@expo/spawn-async": "^1.7.2",
- "@expo/ws-tunnel": "^1.0.1",
- "@expo/xcpretty": "^4.3.0",
- "@react-native/dev-middleware": "0.81.5",
- "@urql/core": "^5.0.6",
- "@urql/exchange-retry": "^1.3.0",
- "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",
- "env-editor": "^0.4.1",
- "expo-server": "^1.0.5",
- "freeport-async": "^2.0.0",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "lan-network": "^0.1.6",
- "minimatch": "^9.0.0",
- "node-forge": "^1.3.3",
- "npm-package-arg": "^11.0.0",
- "ora": "^3.4.0",
- "picomatch": "^3.0.1",
- "pretty-bytes": "^5.6.0",
- "pretty-format": "^29.7.0",
- "progress": "^2.0.3",
- "prompts": "^2.3.2",
- "qrcode-terminal": "0.11.0",
- "require-from-string": "^2.0.2",
- "requireg": "^0.2.2",
- "resolve": "^1.22.2",
- "resolve-from": "^5.0.0",
- "resolve.exports": "^2.0.3",
- "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",
- "tar": "^7.5.2",
- "terminal-link": "^2.1.1",
- "undici": "^6.18.2",
- "wrap-ansi": "^7.0.0",
- "ws": "^8.12.1"
- },
- "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/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "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/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/expo/node_modules/picomatch": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
- "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/expo/node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "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",
- "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
- "license": "Apache-2.0"
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "license": "MIT"
- },
- "node_modules/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/fb-watchman": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
- "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
- "license": "Apache-2.0",
- "dependencies": {
- "bser": "2.1.1"
- }
- },
- "node_modules/fbjs": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
- "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
- "license": "MIT",
- "dependencies": {
- "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/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",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/filter-obj": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
- "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/finalhandler": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
- "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "statuses": "~1.5.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/finalhandler/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/finalhandler/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "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.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/flow-enums-runtime": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
- "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==",
- "license": "MIT"
- },
- "node_modules/fontfaceobserver": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz",
- "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==",
- "license": "BSD-2-Clause"
- },
- "node_modules/for-each": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
- "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
- "license": "MIT",
- "dependencies": {
- "is-callable": "^1.2.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/freeport-async": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz",
- "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "license": "ISC"
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/function.prototype.name": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
- "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "define-properties": "^1.2.1",
- "functions-have-names": "^1.2.3",
- "hasown": "^2.0.2",
- "is-callable": "^1.2.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/functions-have-names": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
- "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/generator-function": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
- "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "license": "ISC",
- "engines": {
- "node": "6.* || 8.* || >= 10.*"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-nonce": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
- "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/get-package-type": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
- "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
- "license": "MIT",
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/get-symbol-description": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
- "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-tsconfig": {
- "version": "4.13.6",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
- "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
- "node_modules/getenv": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
- "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/glob": {
- "version": "13.0.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.5.tgz",
- "integrity": "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "minimatch": "^10.2.1",
- "minipass": "^7.1.2",
- "path-scurry": "^2.0.0"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "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": "4.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
- "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
- "license": "MIT",
- "dependencies": {
- "jackspeak": "^4.2.3"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
- "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/glob/node_modules/minimatch": {
- "version": "10.2.1",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz",
- "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/global-dirs": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz",
- "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==",
- "license": "MIT",
- "dependencies": {
- "ini": "^1.3.4"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/globalthis": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
- "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "define-properties": "^1.2.1",
- "gopd": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "license": "ISC"
- },
- "node_modules/has-bigints": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
- "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-property-descriptors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
- "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
- "license": "MIT",
- "dependencies": {
- "es-define-property": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-proto": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
- "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/hermes-estree": {
- "version": "0.29.1",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz",
- "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==",
- "license": "MIT"
- },
- "node_modules/hermes-parser": {
- "version": "0.29.1",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz",
- "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==",
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.29.1"
- }
- },
- "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",
- "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
- "license": "ISC",
- "dependencies": {
- "lru-cache": "^10.0.1"
- },
- "engines": {
- "node": "^16.14.0 || >=18.0.0"
- }
- },
- "node_modules/hosted-git-info/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "license": "ISC"
- },
- "node_modules/html-encoding-sniffer": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
- "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@exodus/bytes": "^1.6.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- }
- },
- "node_modules/html-escaper": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
- "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/html-parse-stringify": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
- "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
- "license": "MIT",
- "dependencies": {
- "void-elements": "3.1.0"
- }
- },
- "node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/http-errors/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.2",
- "debug": "4"
- },
- "engines": {
- "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/i18next": {
- "version": "25.8.12",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.12.tgz",
- "integrity": "sha512-hw59RF5QaH9i3l47hTXjeLLtzgVO8OtznlTJZbulmaLbz+itA1hIKWHTEiajY9W2SNPzvL8U5nTBVt7SsOGNRA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://locize.com"
- },
- {
- "type": "individual",
- "url": "https://locize.com/i18next.html"
- },
- {
- "type": "individual",
- "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.4"
- },
- "peerDependencies": {
- "typescript": "^5"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/iceberg-js": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
- "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
- "license": "MIT",
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "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": "BSD-3-Clause"
- },
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/image-size": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
- "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
- "license": "MIT",
- "dependencies": {
- "queue": "6.0.2"
- },
- "bin": {
- "image-size": "bin/image-size.js"
- },
- "engines": {
- "node": ">=16.x"
- }
- },
- "node_modules/import-fresh": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
- "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/import-fresh/node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/indent-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
- "license": "ISC",
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "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/internal-slot": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
- "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "hasown": "^2.0.2",
- "side-channel": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/invariant": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
- "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.0.0"
- }
- },
- "node_modules/is-arguments": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
- "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-array-buffer": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
- "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "get-intrinsic": "^1.2.6"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-arrayish": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
- "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
- "license": "MIT"
- },
- "node_modules/is-async-function": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
- "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "async-function": "^1.0.0",
- "call-bound": "^1.0.3",
- "get-proto": "^1.0.1",
- "has-tostringtag": "^1.0.2",
- "safe-regex-test": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-bigint": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
- "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-bigints": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-boolean-object": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
- "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-bun-module": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
- "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.7.1"
- }
- },
- "node_modules/is-bun-module/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/is-callable": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
- "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-core-module": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-data-view": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
- "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "get-intrinsic": "^1.2.6",
- "is-typed-array": "^1.1.13"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-date-object": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
- "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-docker": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
- "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
- "license": "MIT",
- "bin": {
- "is-docker": "cli.js"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "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-finalizationregistry": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
- "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-generator-function": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
- "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.4",
- "generator-function": "^2.0.0",
- "get-proto": "^1.0.1",
- "has-tostringtag": "^1.0.2",
- "safe-regex-test": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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-map": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
- "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-nan": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
- "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-negative-zero": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
- "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/is-number-object": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
- "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-plain-obj": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
- "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-potential-custom-element-name": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
- "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/is-regex": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
- "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "gopd": "^1.2.0",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-set": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
- "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-shared-array-buffer": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
- "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-string": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
- "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-symbol": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
- "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "has-symbols": "^1.1.0",
- "safe-regex-test": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-typed-array": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
- "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
- "license": "MIT",
- "dependencies": {
- "which-typed-array": "^1.1.16"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-weakmap": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
- "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-weakref": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
- "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-weakset": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
- "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "get-intrinsic": "^1.2.6"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-wsl": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
- "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
- "license": "MIT",
- "dependencies": {
- "is-docker": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/istanbul-lib-coverage": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
- "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/istanbul-lib-instrument": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
- "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/core": "^7.12.3",
- "@babel/parser": "^7.14.7",
- "@istanbuljs/schema": "^0.1.2",
- "istanbul-lib-coverage": "^3.2.0",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/istanbul-lib-report": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
- "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "istanbul-lib-coverage": "^3.0.0",
- "make-dir": "^4.0.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-reports": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
- "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "html-escaper": "^2.0.0",
- "istanbul-lib-report": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/iterator.prototype": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
- "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "define-data-property": "^1.1.4",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.6",
- "get-proto": "^1.0.0",
- "has-symbols": "^1.1.0",
- "set-function-name": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/jackspeak": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
- "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^9.0.0"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/jest-diff": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
- "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.0.0",
- "diff-sequences": "^29.6.3",
- "jest-get-type": "^29.6.3",
- "pretty-format": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-environment-node": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
- "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
- "license": "MIT",
- "dependencies": {
- "@jest/environment": "^29.7.0",
- "@jest/fake-timers": "^29.7.0",
- "@jest/types": "^29.6.3",
- "@types/node": "*",
- "jest-mock": "^29.7.0",
- "jest-util": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-get-type": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
- "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
- "license": "MIT",
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-haste-map": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz",
- "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==",
- "license": "MIT",
- "dependencies": {
- "@jest/types": "^29.6.3",
- "@types/graceful-fs": "^4.1.3",
- "@types/node": "*",
- "anymatch": "^3.0.3",
- "fb-watchman": "^2.0.0",
- "graceful-fs": "^4.2.9",
- "jest-regex-util": "^29.6.3",
- "jest-util": "^29.7.0",
- "jest-worker": "^29.7.0",
- "micromatch": "^4.0.4",
- "walker": "^1.0.8"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "^2.3.2"
- }
- },
- "node_modules/jest-matcher-utils": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
- "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.0.0",
- "jest-diff": "^29.7.0",
- "jest-get-type": "^29.6.3",
- "pretty-format": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-message-util": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
- "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.12.13",
- "@jest/types": "^29.6.3",
- "@types/stack-utils": "^2.0.0",
- "chalk": "^4.0.0",
- "graceful-fs": "^4.2.9",
- "micromatch": "^4.0.4",
- "pretty-format": "^29.7.0",
- "slash": "^3.0.0",
- "stack-utils": "^2.0.3"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-mock": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
- "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
- "license": "MIT",
- "dependencies": {
- "@jest/types": "^29.6.3",
- "@types/node": "*",
- "jest-util": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-regex-util": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
- "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
- "license": "MIT",
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-util": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
- "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
- "license": "MIT",
- "dependencies": {
- "@jest/types": "^29.6.3",
- "@types/node": "*",
- "chalk": "^4.0.0",
- "ci-info": "^3.2.0",
- "graceful-fs": "^4.2.9",
- "picomatch": "^2.2.3"
- },
- "engines": {
- "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"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/jest-validate": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz",
- "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==",
- "license": "MIT",
- "dependencies": {
- "@jest/types": "^29.6.3",
- "camelcase": "^6.2.0",
- "chalk": "^4.0.0",
- "jest-get-type": "^29.6.3",
- "leven": "^3.1.0",
- "pretty-format": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-worker": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
- "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "jest-util": "^29.7.0",
- "merge-stream": "^2.0.0",
- "supports-color": "^8.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-worker/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/jimp-compact": {
- "version": "0.16.1",
- "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz",
- "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==",
- "license": "MIT"
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
- "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",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/jsc-safe-url": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz",
- "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==",
- "license": "0BSD"
- },
- "node_modules/jsdom": {
- "version": "29.0.1",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz",
- "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@asamuzakjp/css-color": "^5.0.1",
- "@asamuzakjp/dom-selector": "^7.0.3",
- "@bramus/specificity": "^2.4.2",
- "@csstools/css-syntax-patches-for-csstree": "^1.1.1",
- "@exodus/bytes": "^1.15.0",
- "css-tree": "^3.2.1",
- "data-urls": "^7.0.0",
- "decimal.js": "^10.6.0",
- "html-encoding-sniffer": "^6.0.0",
- "is-potential-custom-element-name": "^1.0.1",
- "lru-cache": "^11.2.7",
- "parse5": "^8.0.0",
- "saxes": "^6.0.0",
- "symbol-tree": "^3.2.4",
- "tough-cookie": "^6.0.1",
- "undici": "^7.24.5",
- "w3c-xmlserializer": "^5.0.0",
- "webidl-conversions": "^8.0.1",
- "whatwg-mimetype": "^5.0.0",
- "whatwg-url": "^16.0.1",
- "xml-name-validator": "^5.0.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
- },
- "peerDependencies": {
- "canvas": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "canvas": {
- "optional": true
- }
- }
- },
- "node_modules/jsdom/node_modules/lru-cache": {
- "version": "11.2.7",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
- "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/jsdom/node_modules/tr46": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
- "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "punycode": "^2.3.1"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/jsdom/node_modules/undici": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
- "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20.18.1"
- }
- },
- "node_modules/jsdom/node_modules/webidl-conversions": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
- "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/jsdom/node_modules/whatwg-url": {
- "version": "16.0.1",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
- "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@exodus/bytes": "^1.11.0",
- "tr46": "^6.0.0",
- "webidl-conversions": "^8.0.1"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- }
- },
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "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",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/jsx-ast-utils": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
- "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flat": "^1.3.1",
- "object.assign": "^4.1.4",
- "object.values": "^1.1.6"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "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",
- "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/lan-network": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.1.7.tgz",
- "integrity": "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==",
- "license": "MIT",
- "bin": {
- "lan-network": "dist/lan-network-cli.js"
- }
- },
- "node_modules/leven": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
- "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
- "license": "MIT",
- "engines": {
- "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",
- "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==",
- "license": "Apache-2.0",
- "dependencies": {
- "debug": "^2.6.9",
- "marky": "^1.2.2"
- }
- },
- "node_modules/lighthouse-logger/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/lighthouse-logger/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
- "cpu": [
- "arm"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/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==",
- "license": "MIT"
- },
- "node_modules/locate-path": {
- "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": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "license": "MIT"
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lodash.throttle": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
- "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
- "license": "MIT"
- },
- "node_modules/log-symbols": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
- "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
- "license": "MIT",
- "dependencies": {
- "chalk": "^2.0.1"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/log-symbols/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/log-symbols/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/log-symbols/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/log-symbols/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "license": "MIT"
- },
- "node_modules/log-symbols/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/log-symbols/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/log-symbols/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/magicast": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
- "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.0",
- "@babel/types": "^7.29.0",
- "source-map-js": "^1.2.1"
- }
- },
- "node_modules/make-dir": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
- "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.5.3"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/make-dir/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/makeerror": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
- "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "tmpl": "1.0.5"
- }
- },
- "node_modules/marky": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
- "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
- "license": "Apache-2.0"
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/mdn-data": {
- "version": "2.27.1",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
- "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
- "dev": true,
- "license": "CC0-1.0"
- },
- "node_modules/memoize-one": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
- "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
- "license": "MIT"
- },
- "node_modules/merge-options": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
- "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
- "license": "MIT",
- "dependencies": {
- "is-plain-obj": "^2.1.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "license": "MIT"
- },
- "node_modules/metro": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz",
- "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.24.7",
- "@babel/core": "^7.25.2",
- "@babel/generator": "^7.25.0",
- "@babel/parser": "^7.25.3",
- "@babel/template": "^7.25.0",
- "@babel/traverse": "^7.25.3",
- "@babel/types": "^7.25.2",
- "accepts": "^1.3.7",
- "chalk": "^4.0.0",
- "ci-info": "^2.0.0",
- "connect": "^3.6.5",
- "debug": "^4.4.0",
- "error-stack-parser": "^2.0.6",
- "flow-enums-runtime": "^0.0.6",
- "graceful-fs": "^4.2.4",
- "hermes-parser": "0.32.0",
- "image-size": "^1.0.2",
- "invariant": "^2.2.4",
- "jest-worker": "^29.7.0",
- "jsc-safe-url": "^0.2.2",
- "lodash.throttle": "^4.1.1",
- "metro-babel-transformer": "0.83.3",
- "metro-cache": "0.83.3",
- "metro-cache-key": "0.83.3",
- "metro-config": "0.83.3",
- "metro-core": "0.83.3",
- "metro-file-map": "0.83.3",
- "metro-resolver": "0.83.3",
- "metro-runtime": "0.83.3",
- "metro-source-map": "0.83.3",
- "metro-symbolicate": "0.83.3",
- "metro-transform-plugins": "0.83.3",
- "metro-transform-worker": "0.83.3",
- "mime-types": "^2.1.27",
- "nullthrows": "^1.1.1",
- "serialize-error": "^2.1.0",
- "source-map": "^0.5.6",
- "throat": "^5.0.0",
- "ws": "^7.5.10",
- "yargs": "^17.6.2"
- },
- "bin": {
- "metro": "src/cli.js"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-babel-transformer": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz",
- "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.25.2",
- "flow-enums-runtime": "^0.0.6",
- "hermes-parser": "0.32.0",
- "nullthrows": "^1.1.1"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-babel-transformer/node_modules/hermes-estree": {
- "version": "0.32.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz",
- "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==",
- "license": "MIT"
- },
- "node_modules/metro-babel-transformer/node_modules/hermes-parser": {
- "version": "0.32.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz",
- "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==",
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.32.0"
- }
- },
- "node_modules/metro-cache": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz",
- "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==",
- "license": "MIT",
- "dependencies": {
- "exponential-backoff": "^3.1.1",
- "flow-enums-runtime": "^0.0.6",
- "https-proxy-agent": "^7.0.5",
- "metro-core": "0.83.3"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-cache-key": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz",
- "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==",
- "license": "MIT",
- "dependencies": {
- "flow-enums-runtime": "^0.0.6"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-config": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz",
- "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==",
- "license": "MIT",
- "dependencies": {
- "connect": "^3.6.5",
- "flow-enums-runtime": "^0.0.6",
- "jest-validate": "^29.7.0",
- "metro": "0.83.3",
- "metro-cache": "0.83.3",
- "metro-core": "0.83.3",
- "metro-runtime": "0.83.3",
- "yaml": "^2.6.1"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-core": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz",
- "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==",
- "license": "MIT",
- "dependencies": {
- "flow-enums-runtime": "^0.0.6",
- "lodash.throttle": "^4.1.1",
- "metro-resolver": "0.83.3"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-file-map": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz",
- "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "fb-watchman": "^2.0.0",
- "flow-enums-runtime": "^0.0.6",
- "graceful-fs": "^4.2.4",
- "invariant": "^2.2.4",
- "jest-worker": "^29.7.0",
- "micromatch": "^4.0.4",
- "nullthrows": "^1.1.1",
- "walker": "^1.0.7"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-minify-terser": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz",
- "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==",
- "license": "MIT",
- "dependencies": {
- "flow-enums-runtime": "^0.0.6",
- "terser": "^5.15.0"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-resolver": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz",
- "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==",
- "license": "MIT",
- "dependencies": {
- "flow-enums-runtime": "^0.0.6"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-runtime": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz",
- "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.25.0",
- "flow-enums-runtime": "^0.0.6"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-source-map": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz",
- "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.25.3",
- "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3",
- "@babel/types": "^7.25.2",
- "flow-enums-runtime": "^0.0.6",
- "invariant": "^2.2.4",
- "metro-symbolicate": "0.83.3",
- "nullthrows": "^1.1.1",
- "ob1": "0.83.3",
- "source-map": "^0.5.6",
- "vlq": "^1.0.0"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-symbolicate": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz",
- "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==",
- "license": "MIT",
- "dependencies": {
- "flow-enums-runtime": "^0.0.6",
- "invariant": "^2.2.4",
- "metro-source-map": "0.83.3",
- "nullthrows": "^1.1.1",
- "source-map": "^0.5.6",
- "vlq": "^1.0.0"
- },
- "bin": {
- "metro-symbolicate": "src/index.js"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-transform-plugins": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz",
- "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/generator": "^7.25.0",
- "@babel/template": "^7.25.0",
- "@babel/traverse": "^7.25.3",
- "flow-enums-runtime": "^0.0.6",
- "nullthrows": "^1.1.1"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro-transform-worker": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz",
- "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/generator": "^7.25.0",
- "@babel/parser": "^7.25.3",
- "@babel/types": "^7.25.2",
- "flow-enums-runtime": "^0.0.6",
- "metro": "0.83.3",
- "metro-babel-transformer": "0.83.3",
- "metro-cache": "0.83.3",
- "metro-cache-key": "0.83.3",
- "metro-minify-terser": "0.83.3",
- "metro-source-map": "0.83.3",
- "metro-transform-plugins": "0.83.3",
- "nullthrows": "^1.1.1"
- },
- "engines": {
- "node": ">=20.19.4"
- }
- },
- "node_modules/metro/node_modules/hermes-estree": {
- "version": "0.32.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz",
- "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==",
- "license": "MIT"
- },
- "node_modules/metro/node_modules/hermes-parser": {
- "version": "0.32.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz",
- "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==",
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.32.0"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "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/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "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",
- "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/min-indent": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
- "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/minizlib": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
- "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
- "license": "MIT",
- "dependencies": {
- "minipass": "^7.1.2"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/mkdirp": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
- "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
- "license": "MIT",
- "bin": {
- "mkdirp": "bin/cmd.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "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==",
- "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",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/napi-postinstall": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
- "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "napi-postinstall": "lib/cli.js"
- },
- "engines": {
- "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/napi-postinstall"
- }
- },
- "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",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/nested-error-stacks": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz",
- "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
- "license": "MIT"
- },
- "node_modules/node-exports-info": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
- "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array.prototype.flatmap": "^1.3.3",
- "es-errors": "^1.3.0",
- "object.entries": "^1.1.9",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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==",
- "license": "(BSD-3-Clause OR GPL-2.0)",
- "engines": {
- "node": ">= 6.13.0"
- }
- },
- "node_modules/node-int64": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
- "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
- "license": "MIT"
- },
- "node_modules/node-releases": {
- "version": "2.0.27",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
- "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
- "license": "MIT"
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/npm-package-arg": {
- "version": "11.0.3",
- "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
- "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==",
- "license": "ISC",
- "dependencies": {
- "hosted-git-info": "^7.0.0",
- "proc-log": "^4.0.0",
- "semver": "^7.3.5",
- "validate-npm-package-name": "^5.0.0"
- },
- "engines": {
- "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/nth-check": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
- "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "boolbase": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/fb55/nth-check?sponsor=1"
- }
- },
- "node_modules/nullthrows": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
- "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==",
- "license": "MIT"
- },
- "node_modules/ob1": {
- "version": "0.83.3",
- "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz",
- "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==",
- "license": "MIT",
- "dependencies": {
- "flow-enums-runtime": "^0.0.6"
- },
- "engines": {
- "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-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object-is": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
- "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.assign": {
- "version": "4.1.7",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
- "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0",
- "has-symbols": "^1.1.0",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.entries": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
- "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.fromentries": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
- "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.groupby": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
- "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.values": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
- "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/obug": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
- "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT"
- },
- "node_modules/on-finished": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
- "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/on-headers": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
- "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/onetime": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
- "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==",
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^1.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/open": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
- "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
- "license": "MIT",
- "dependencies": {
- "is-docker": "^2.0.0",
- "is-wsl": "^2.1.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "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",
- "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==",
- "license": "MIT",
- "dependencies": {
- "chalk": "^2.4.2",
- "cli-cursor": "^2.1.0",
- "cli-spinners": "^2.0.0",
- "log-symbols": "^2.2.0",
- "strip-ansi": "^5.2.0",
- "wcwidth": "^1.0.1"
- },
- "engines": {
- "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",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/ora/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/ora/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/ora/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "license": "MIT"
- },
- "node_modules/ora/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/ora/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/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",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/own-keys": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
- "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.2.6",
- "object-keys": "^1.1.1",
- "safe-push-apply": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "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": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/parse-png": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz",
- "integrity": "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==",
- "license": "MIT",
- "dependencies": {
- "pngjs": "^3.3.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/parse5": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
- "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "entities": "^6.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parse5/node_modules/entities": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
- "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "license": "MIT"
- },
- "node_modules/path-scurry": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
- "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^11.0.0",
- "minipass": "^7.1.2"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/path-scurry/node_modules/lru-cache": {
- "version": "11.2.6",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
- "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pirates": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
- "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/plist": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
- "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
- "license": "MIT",
- "dependencies": {
- "@xmldom/xmldom": "^0.8.8",
- "base64-js": "^1.5.1",
- "xmlbuilder": "^15.1.1"
- },
- "engines": {
- "node": ">=10.4.0"
- }
- },
- "node_modules/pngjs": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
- "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
- "license": "MIT",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/possible-typed-array-names": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
- "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/postcss": {
- "version": "8.4.49",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
- "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "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/posthog-react-native": {
- "version": "4.36.0",
- "resolved": "https://registry.npmjs.org/posthog-react-native/-/posthog-react-native-4.36.0.tgz",
- "integrity": "sha512-BiwUXNa5+y5SzZGaCeowca86b7D9MOrqdERzILTBYcF0FbiQS3XhM4AWmxuzdfpH4MPyGtuaCSRxJOmyna7NEA==",
- "license": "MIT",
- "dependencies": {
- "@posthog/core": "1.23.1"
- },
- "peerDependencies": {
- "@react-native-async-storage/async-storage": ">=1.0.0",
- "@react-navigation/native": ">= 5.0.0",
- "expo-application": ">= 4.0.0",
- "expo-device": ">= 4.0.0",
- "expo-file-system": ">= 13.0.0",
- "expo-localization": ">= 11.0.0",
- "posthog-react-native-session-replay": ">= 1.3.0",
- "react-native-device-info": ">= 10.0.0",
- "react-native-localize": ">= 3.0.0",
- "react-native-navigation": ">= 6.0.0",
- "react-native-safe-area-context": ">= 4.0.0",
- "react-native-svg": ">= 15.0.0"
- },
- "peerDependenciesMeta": {
- "@react-native-async-storage/async-storage": {
- "optional": true
- },
- "@react-navigation/native": {
- "optional": true
- },
- "expo-application": {
- "optional": true
- },
- "expo-device": {
- "optional": true
- },
- "expo-file-system": {
- "optional": true
- },
- "expo-localization": {
- "optional": true
- },
- "posthog-react-native-session-replay": {
- "optional": true
- },
- "react-native-device-info": {
- "optional": true
- },
- "react-native-localize": {
- "optional": true
- },
- "react-native-navigation": {
- "optional": true
- },
- "react-native-safe-area-context": {
- "optional": true
- }
- }
- },
- "node_modules/posthog-react-native-session-replay": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/posthog-react-native-session-replay/-/posthog-react-native-session-replay-1.5.0.tgz",
- "integrity": "sha512-3XYGSpaWDfB0s4WrZlekN+dNO/kVSWCPAUBDmayIbFfL7SJ1OTCoYQrJp+JJdm8Wf+wJmrAv7LoPOvl/mY5A0g==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "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-bytes": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
- "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/pretty-format": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
- "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "ansi-styles": "^5.0.0",
- "react-is": "^18.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/pretty-format/node_modules/ansi-styles": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "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",
- "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==",
- "license": "ISC",
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
- "node_modules/progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/promise": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz",
- "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==",
- "license": "MIT",
- "dependencies": {
- "asap": "~2.0.6"
- }
- },
- "node_modules/prompts": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
- "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
- "license": "MIT",
- "dependencies": {
- "kleur": "^3.0.3",
- "sisteransi": "^1.0.5"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/prop-types/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==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/qrcode-terminal": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz",
- "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==",
- "bin": {
- "qrcode-terminal": "bin/qrcode-terminal.js"
- }
- },
- "node_modules/query-string": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
- "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
- "license": "MIT",
- "dependencies": {
- "decode-uri-component": "^0.2.2",
- "filter-obj": "^1.1.0",
- "split-on-first": "^1.0.0",
- "strict-uri-encode": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/queue": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
- "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "~2.0.3"
- }
- },
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "node_modules/rc/node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
- "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-devtools-core": {
- "version": "6.1.5",
- "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz",
- "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
- "license": "MIT",
- "dependencies": {
- "shell-quote": "^1.6.1",
- "ws": "^7"
- }
- },
- "node_modules/react-dom": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
- "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
- "license": "MIT",
- "dependencies": {
- "scheduler": "^0.26.0"
- },
- "peerDependencies": {
- "react": "^19.1.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-i18next": {
- "version": "16.5.4",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
- "integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.4",
- "html-parse-stringify": "^3.0.1",
- "use-sync-external-store": "^1.6.0"
- },
- "peerDependencies": {
- "i18next": ">= 25.6.2",
- "react": ">= 16.8.0",
- "typescript": "^5"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- },
- "typescript": {
- "optional": true
- }
- }
- },
- "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-native": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
- "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
- "license": "MIT",
- "dependencies": {
- "@jest/create-cache-key-function": "^29.7.0",
- "@react-native/assets-registry": "0.81.5",
- "@react-native/codegen": "0.81.5",
- "@react-native/community-cli-plugin": "0.81.5",
- "@react-native/gradle-plugin": "0.81.5",
- "@react-native/js-polyfills": "0.81.5",
- "@react-native/normalize-colors": "0.81.5",
- "@react-native/virtualized-lists": "0.81.5",
- "abort-controller": "^3.0.0",
- "anser": "^1.4.9",
- "ansi-regex": "^5.0.0",
- "babel-jest": "^29.7.0",
- "babel-plugin-syntax-hermes-parser": "0.29.1",
- "base64-js": "^1.5.1",
- "commander": "^12.0.0",
- "flow-enums-runtime": "^0.0.6",
- "glob": "^7.1.1",
- "invariant": "^2.2.4",
- "jest-environment-node": "^29.7.0",
- "memoize-one": "^5.0.0",
- "metro-runtime": "^0.83.1",
- "metro-source-map": "^0.83.1",
- "nullthrows": "^1.1.1",
- "pretty-format": "^29.7.0",
- "promise": "^8.3.0",
- "react-devtools-core": "^6.1.5",
- "react-refresh": "^0.14.0",
- "regenerator-runtime": "^0.13.2",
- "scheduler": "0.26.0",
- "semver": "^7.1.3",
- "stacktrace-parser": "^0.1.10",
- "whatwg-fetch": "^3.0.0",
- "ws": "^6.2.3",
- "yargs": "^17.6.2"
- },
- "bin": {
- "react-native": "cli.js"
- },
- "engines": {
- "node": ">= 20.19.4"
- },
- "peerDependencies": {
- "@types/react": "^19.1.0",
- "react": "^19.1.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-native-gesture-handler": {
- "version": "2.28.0",
- "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
- "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
- "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.2.1",
- "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
- "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-purchases": {
- "version": "9.10.3",
- "resolved": "https://registry.npmjs.org/react-native-purchases/-/react-native-purchases-9.10.3.tgz",
- "integrity": "sha512-YMdueV9SNTT8bLeFN7pFe27tBMusUP6dXit7yU0opjN2XJw//bSJYrk1snBv8e33At2i6Yq1wNXyHipth8dBgg==",
- "license": "MIT",
- "workspaces": [
- "examples/purchaseTesterTypescript",
- "react-native-purchases-ui"
- ],
- "dependencies": {
- "@revenuecat/purchases-js-hybrid-mappings": "17.41.0",
- "@revenuecat/purchases-typescript-internal": "17.41.0"
- },
- "peerDependencies": {
- "react": ">= 16.6.3",
- "react-native": ">= 0.73.0",
- "react-native-web": "*"
- },
- "peerDependenciesMeta": {
- "react-native-web": {
- "optional": true
- }
- }
- },
- "node_modules/react-native-reanimated": {
- "version": "4.1.6",
- "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
- "integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==",
- "license": "MIT",
- "dependencies": {
- "react-native-is-edge-to-edge": "^1.2.1",
- "semver": "7.7.2"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0",
- "react": "*",
- "react-native": "*",
- "react-native-worklets": ">=0.5.0"
- }
- },
- "node_modules/react-native-reanimated/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/react-native-safe-area-context": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
- "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-screens": {
- "version": "4.16.0",
- "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
- "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
- "license": "MIT",
- "dependencies": {
- "react-freeze": "^1.0.0",
- "react-native-is-edge-to-edge": "^1.2.1",
- "warn-once": "^0.1.0"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-svg": {
- "version": "15.12.1",
- "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
- "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
- "license": "MIT",
- "dependencies": {
- "css-select": "^5.1.0",
- "css-tree": "^1.1.3",
- "warn-once": "0.1.1"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-svg/node_modules/css-tree": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
- "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
- "license": "MIT",
- "dependencies": {
- "mdn-data": "2.0.14",
- "source-map": "^0.6.1"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/react-native-svg/node_modules/mdn-data": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
- "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
- "license": "CC0-1.0"
- },
- "node_modules/react-native-svg/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/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-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.5.1",
- "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
- "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
- "license": "MIT",
- "dependencies": {
- "@babel/plugin-transform-arrow-functions": "^7.0.0-0",
- "@babel/plugin-transform-class-properties": "^7.0.0-0",
- "@babel/plugin-transform-classes": "^7.0.0-0",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0",
- "@babel/plugin-transform-optional-chaining": "^7.0.0-0",
- "@babel/plugin-transform-shorthand-properties": "^7.0.0-0",
- "@babel/plugin-transform-template-literals": "^7.0.0-0",
- "@babel/plugin-transform-unicode-regex": "^7.0.0-0",
- "@babel/preset-typescript": "^7.16.7",
- "convert-source-map": "^2.0.0",
- "semver": "7.7.2"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-worklets/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
- "version": "0.81.5",
- "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
- "integrity": "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==",
- "license": "MIT",
- "dependencies": {
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1"
- },
- "engines": {
- "node": ">= 20.19.4"
- },
- "peerDependencies": {
- "@types/react": "^19.1.0",
- "react": "*",
- "react-native": "*"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-native/node_modules/commander": {
- "version": "12.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
- "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
- "license": "MIT",
- "engines": {
- "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",
- "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/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/ws": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
- "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
- "license": "MIT",
- "dependencies": {
- "async-limiter": "~1.0.0"
- }
- },
- "node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-remove-scroll": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
- "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
- "license": "MIT",
- "dependencies": {
- "react-remove-scroll-bar": "^2.3.7",
- "react-style-singleton": "^2.2.3",
- "tslib": "^2.1.0",
- "use-callback-ref": "^1.3.3",
- "use-sidecar": "^1.1.3"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-remove-scroll-bar": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
- "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
- "license": "MIT",
- "dependencies": {
- "react-style-singleton": "^2.2.2",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-style-singleton": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
- "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
- "license": "MIT",
- "dependencies": {
- "get-nonce": "^1.0.0",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-test-renderer": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
- "integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "react-is": "^19.1.0",
- "scheduler": "^0.26.0"
- },
- "peerDependencies": {
- "react": "^19.1.0"
- }
- },
- "node_modules/redent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
- "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "indent-string": "^4.0.0",
- "strip-indent": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/reflect.getprototypeof": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
- "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.9",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.7",
- "get-proto": "^1.0.1",
- "which-builtin-type": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/regenerate": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
- "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
- "license": "MIT"
- },
- "node_modules/regenerate-unicode-properties": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
- "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
- "license": "MIT",
- "dependencies": {
- "regenerate": "^1.4.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
- "license": "MIT"
- },
- "node_modules/regexp.prototype.flags": {
- "version": "1.5.4",
- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
- "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-errors": "^1.3.0",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "set-function-name": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/regexpu-core": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
- "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
- "license": "MIT",
- "dependencies": {
- "regenerate": "^1.4.2",
- "regenerate-unicode-properties": "^10.2.2",
- "regjsgen": "^0.8.0",
- "regjsparser": "^0.13.0",
- "unicode-match-property-ecmascript": "^2.0.0",
- "unicode-match-property-value-ecmascript": "^2.2.1"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/regjsgen": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
- "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
- "license": "MIT"
- },
- "node_modules/regjsparser": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
- "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "jsesc": "~3.1.0"
- },
- "bin": {
- "regjsparser": "bin/parser"
- }
- },
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/requireg": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz",
- "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==",
- "dependencies": {
- "nested-error-stacks": "~2.0.1",
- "rc": "~1.2.7",
- "resolve": "~1.7.1"
- },
- "engines": {
- "node": ">= 4.0.0"
- }
- },
- "node_modules/requireg/node_modules/resolve": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz",
- "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==",
- "license": "MIT",
- "dependencies": {
- "path-parse": "^1.0.5"
- }
- },
- "node_modules/resolve": {
- "version": "1.22.11",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
- "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
- "license": "MIT",
- "dependencies": {
- "is-core-module": "^2.16.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/resolve-from": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
- "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/resolve-global": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz",
- "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==",
- "license": "MIT",
- "dependencies": {
- "global-dirs": "^0.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
- "node_modules/resolve-workspace-root": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz",
- "integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==",
- "license": "MIT"
- },
- "node_modules/resolve.exports": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
- "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/restore-cursor": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
- "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==",
- "license": "MIT",
- "dependencies": {
- "onetime": "^2.0.0",
- "signal-exit": "^3.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "deprecated": "Rimraf versions prior to v4 are no longer supported",
- "license": "ISC",
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "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": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rolldown": {
- "version": "1.0.0-rc.11",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
- "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.122.0",
- "@rolldown/pluginutils": "1.0.0-rc.11"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.0-rc.11",
- "@rolldown/binding-darwin-arm64": "1.0.0-rc.11",
- "@rolldown/binding-darwin-x64": "1.0.0-rc.11",
- "@rolldown/binding-freebsd-x64": "1.0.0-rc.11",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11",
- "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11",
- "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11",
- "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11",
- "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11",
- "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11",
- "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11",
- "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11",
- "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11",
- "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11"
- }
- },
- "node_modules/rtl-detect": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz",
- "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/safe-array-concat": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
- "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.2",
- "get-intrinsic": "^1.2.6",
- "has-symbols": "^1.1.0",
- "isarray": "^2.0.5"
- },
- "engines": {
- "node": ">=0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "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/safe-push-apply": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
- "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "isarray": "^2.0.5"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/safe-regex-test": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
- "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "is-regex": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/sax": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
- "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=11.0.0"
- }
- },
- "node_modules/saxes": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "xmlchars": "^2.2.0"
- },
- "engines": {
- "node": ">=v12.22.7"
- }
- },
- "node_modules/scheduler": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
- "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
- "license": "MIT"
- },
- "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/send": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
- "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "~0.5.2",
- "http-errors": "~2.0.1",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "~2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "~2.0.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/send/node_modules/debug/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/send/node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/send/node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/send/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/serialize-error": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz",
- "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/serve-static": {
- "version": "1.16.3",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
- "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "~0.19.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/serve-static/node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/server-only": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
- "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
- "license": "MIT"
- },
- "node_modules/set-function-length": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
- "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
- "license": "MIT",
- "dependencies": {
- "define-data-property": "^1.1.4",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.4",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/set-function-name": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
- "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "define-data-property": "^1.1.4",
- "es-errors": "^1.3.0",
- "functions-have-names": "^1.2.3",
- "has-property-descriptors": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/set-proto": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
- "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "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",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
- "node_modules/sf-symbols-typescript": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz",
- "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/shallowequal": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
- "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
- "license": "MIT"
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shell-quote": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
- "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/signal-exit": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "license": "ISC"
- },
- "node_modules/simple-plist": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz",
- "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==",
- "license": "MIT",
- "dependencies": {
- "bplist-creator": "0.1.0",
- "bplist-parser": "0.3.1",
- "plist": "^3.0.5"
- }
- },
- "node_modules/simple-swizzle": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
- "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
- "license": "MIT",
- "dependencies": {
- "is-arrayish": "^0.3.1"
- }
- },
- "node_modules/sisteransi": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
- "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
- "license": "MIT"
- },
- "node_modules/slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/slugify": {
- "version": "1.6.6",
- "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
- "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
- "license": "MIT",
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "license": "MIT",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "node_modules/source-map-support/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/split-on-first": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
- "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "license": "BSD-3-Clause"
- },
- "node_modules/stable-hash": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
- "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/stack-utils": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
- "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
- "license": "MIT",
- "dependencies": {
- "escape-string-regexp": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/stack-utils/node_modules/escape-string-regexp": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
- "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/stackframe": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
- "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
- "license": "MIT"
- },
- "node_modules/stacktrace-parser": {
- "version": "0.1.11",
- "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
- "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==",
- "license": "MIT",
- "dependencies": {
- "type-fest": "^0.7.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/std-env": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
- "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/stop-iteration-iterator": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
- "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "internal-slot": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/stream-buffers": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
- "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==",
- "license": "Unlicense",
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/strict-uri-encode": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
- "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string.prototype.matchall": {
- "version": "4.0.12",
- "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
- "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.6",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.6",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "internal-slot": "^1.1.0",
- "regexp.prototype.flags": "^1.5.3",
- "set-function-name": "^2.0.2",
- "side-channel": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.repeat": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
- "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.5"
- }
- },
- "node_modules/string.prototype.trim": {
- "version": "1.2.10",
- "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
- "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.2",
- "define-data-property": "^1.1.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-object-atoms": "^1.0.0",
- "has-property-descriptors": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trimend": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
- "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.2",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trimstart": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
- "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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/strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/strip-indent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
- "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "min-indent": "^1.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "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==",
- "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==",
- "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",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/supports-hyperlinks": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
- "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0",
- "supports-color": "^7.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/symbol-tree": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
- "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tar": {
- "version": "7.5.9",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
- "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/fs-minipass": "^4.0.0",
- "chownr": "^3.0.0",
- "minipass": "^7.1.2",
- "minizlib": "^3.1.0",
- "yallist": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tar/node_modules/yallist": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
- "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/temp-dir": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
- "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/terminal-link": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
- "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-escapes": "^4.2.1",
- "supports-hyperlinks": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/terser": {
- "version": "5.46.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
- "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.15.0",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "bin": {
- "terser": "bin/terser"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/terser/node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "license": "MIT"
- },
- "node_modules/test-exclude": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
- "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
- "license": "ISC",
- "dependencies": {
- "@istanbuljs/schema": "^0.1.2",
- "glob": "^7.1.4",
- "minimatch": "^3.0.4"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/test-exclude/node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
- "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/thenify": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
- "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "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==",
- "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/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
- "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
- "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/tinyglobby/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/tinyglobby/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/tinyrainbow": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
- "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tldts": {
- "version": "7.0.27",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz",
- "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tldts-core": "^7.0.27"
- },
- "bin": {
- "tldts": "bin/cli.js"
- }
- },
- "node_modules/tldts-core": {
- "version": "7.0.27",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz",
- "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tmpl": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
- "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
- "license": "BSD-3-Clause"
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/tough-cookie": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
- "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "tldts": "^7.0.5"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "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.4.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
- "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
- "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==",
- "license": "Apache-2.0"
- },
- "node_modules/tsconfig-paths": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
- "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/json5": "^0.0.29",
- "json5": "^1.0.2",
- "minimist": "^1.2.6",
- "strip-bom": "^3.0.0"
- }
- },
- "node_modules/tsconfig-paths/node_modules/json5": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
- "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "minimist": "^1.2.0"
- },
- "bin": {
- "json5": "lib/cli.js"
- }
- },
- "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",
- "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/type-fest": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
- "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/typed-array-buffer": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
- "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-typed-array": "^1.1.14"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/typed-array-byte-length": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
- "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "for-each": "^0.3.3",
- "gopd": "^1.2.0",
- "has-proto": "^1.2.0",
- "is-typed-array": "^1.1.14"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/typed-array-byte-offset": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
- "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.8",
- "for-each": "^0.3.3",
- "gopd": "^1.2.0",
- "has-proto": "^1.2.0",
- "is-typed-array": "^1.1.15",
- "reflect.getprototypeof": "^1.0.9"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/typed-array-length": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
- "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "is-typed-array": "^1.1.13",
- "possible-typed-array-names": "^1.0.0",
- "reflect.getprototypeof": "^1.0.6"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "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/unbox-primitive": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
- "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "has-bigints": "^1.0.2",
- "has-symbols": "^1.1.0",
- "which-boxed-primitive": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/undici": {
- "version": "6.23.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
- "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
- "license": "MIT",
- "engines": {
- "node": ">=18.17"
- }
- },
- "node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
- "license": "MIT"
- },
- "node_modules/unicode-canonical-property-names-ecmascript": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
- "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unicode-match-property-ecmascript": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
- "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
- "license": "MIT",
- "dependencies": {
- "unicode-canonical-property-names-ecmascript": "^2.0.0",
- "unicode-property-aliases-ecmascript": "^2.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unicode-match-property-value-ecmascript": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
- "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unicode-property-aliases-ecmascript": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
- "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unique-string": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
- "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
- "license": "MIT",
- "dependencies": {
- "crypto-random-string": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/unrs-resolver": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
- "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "dependencies": {
- "napi-postinstall": "^0.3.0"
- },
- "funding": {
- "url": "https://opencollective.com/unrs-resolver"
- },
- "optionalDependencies": {
- "@unrs/resolver-binding-android-arm-eabi": "1.11.1",
- "@unrs/resolver-binding-android-arm64": "1.11.1",
- "@unrs/resolver-binding-darwin-arm64": "1.11.1",
- "@unrs/resolver-binding-darwin-x64": "1.11.1",
- "@unrs/resolver-binding-freebsd-x64": "1.11.1",
- "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
- "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
- "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
- "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
- "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-x64-musl": "1.11.1",
- "@unrs/resolver-binding-wasm32-wasi": "1.11.1",
- "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
- "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
- "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
- }
- },
- "node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "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",
- "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-latest-callback": {
- "version": "0.2.6",
- "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz",
- "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=16.8"
- }
- },
- "node_modules/use-sidecar": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
- "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
- "license": "MIT",
- "dependencies": {
- "detect-node-es": "^1.1.0",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/util": {
- "version": "0.12.5",
- "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
- "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "is-arguments": "^1.0.4",
- "is-generator-function": "^1.0.7",
- "is-typed-array": "^1.1.3",
- "which-typed-array": "^1.1.2"
- }
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/uuid": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
- "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/validate-npm-package-name": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
- "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
- "license": "ISC",
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/vaul": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
- "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-dialog": "^1.1.1"
- },
- "peerDependencies": {
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
- }
- },
- "node_modules/vaul/node_modules/@radix-ui/react-dialog": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
- "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-focus-guards": "1.1.3",
- "@radix-ui/react-focus-scope": "1.1.7",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "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/vaul/node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
- "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-escape-keydown": "1.1.1"
- },
- "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/vaul/node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
- "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "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/vaul/node_modules/@radix-ui/react-portal": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
- "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "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/vaul/node_modules/@radix-ui/react-presence": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
- "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "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/vaul/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.3"
- },
- "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/vaul/node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/vite": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
- "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lightningcss": "^1.32.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.8",
- "rolldown": "1.0.0-rc.11",
- "tinyglobby": "^0.2.15"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.1.0",
- "esbuild": "^0.27.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vite/node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/vite/node_modules/postcss": {
- "version": "8.5.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
- "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/vitest": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz",
- "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/expect": "4.1.1",
- "@vitest/mocker": "4.1.1",
- "@vitest/pretty-format": "4.1.1",
- "@vitest/runner": "4.1.1",
- "@vitest/snapshot": "4.1.1",
- "@vitest/spy": "4.1.1",
- "@vitest/utils": "4.1.1",
- "es-module-lexer": "^2.0.0",
- "expect-type": "^1.3.0",
- "magic-string": "^0.30.21",
- "obug": "^2.1.1",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.3",
- "std-env": "^4.0.0-rc.1",
- "tinybench": "^2.9.0",
- "tinyexec": "^1.0.2",
- "tinyglobby": "^0.2.15",
- "tinyrainbow": "^3.0.3",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@opentelemetry/api": "^1.9.0",
- "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.1",
- "@vitest/browser-preview": "4.1.1",
- "@vitest/browser-webdriverio": "4.1.1",
- "@vitest/ui": "4.1.1",
- "happy-dom": "*",
- "jsdom": "*",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser-playwright": {
- "optional": true
- },
- "@vitest/browser-preview": {
- "optional": true
- },
- "@vitest/browser-webdriverio": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- },
- "vite": {
- "optional": false
- }
- }
- },
- "node_modules/vitest/node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/vlq": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz",
- "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
- "license": "MIT"
- },
- "node_modules/void-elements": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
- "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/w3c-xmlserializer": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
- "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "xml-name-validator": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/walker": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
- "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "makeerror": "1.0.12"
- }
- },
- "node_modules/warn-once": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz",
- "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
- "license": "MIT"
- },
- "node_modules/wcwidth": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
- "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
- "license": "MIT",
- "dependencies": {
- "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-mimetype": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
- "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20"
- }
- },
- "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-without-unicode": {
- "version": "8.0.0-3",
- "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz",
- "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==",
- "license": "MIT",
- "dependencies": {
- "buffer": "^5.4.3",
- "punycode": "^2.1.1",
- "webidl-conversions": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
- "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/which-boxed-primitive": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
- "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-bigint": "^1.1.0",
- "is-boolean-object": "^1.2.1",
- "is-number-object": "^1.1.1",
- "is-string": "^1.1.1",
- "is-symbol": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-builtin-type": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
- "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "function.prototype.name": "^1.1.6",
- "has-tostringtag": "^1.0.2",
- "is-async-function": "^2.0.0",
- "is-date-object": "^1.1.0",
- "is-finalizationregistry": "^1.1.0",
- "is-generator-function": "^1.0.10",
- "is-regex": "^1.2.1",
- "is-weakref": "^1.0.2",
- "isarray": "^2.0.5",
- "which-boxed-primitive": "^1.1.0",
- "which-collection": "^1.0.2",
- "which-typed-array": "^1.1.16"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-collection": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
- "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-map": "^2.0.3",
- "is-set": "^2.0.3",
- "is-weakmap": "^2.0.2",
- "is-weakset": "^2.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-typed-array": {
- "version": "1.1.20",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
- "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
- "license": "MIT",
- "dependencies": {
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "for-each": "^0.3.5",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wonka": {
- "version": "6.3.5",
- "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",
- "integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==",
- "license": "MIT"
- },
- "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",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
- },
- "node_modules/write-file-atomic": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
- "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
- "license": "ISC",
- "dependencies": {
- "imurmurhash": "^0.1.4",
- "signal-exit": "^3.0.7"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
- }
- },
- "node_modules/ws": {
- "version": "7.5.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/xcode": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz",
- "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==",
- "license": "Apache-2.0",
- "dependencies": {
- "simple-plist": "^1.1.0",
- "uuid": "^7.0.3"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/xml-name-validator": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
- "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/xml2js": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz",
- "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==",
- "license": "MIT",
- "dependencies": {
- "sax": ">=0.6.0",
- "xmlbuilder": "~11.0.0"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/xml2js/node_modules/xmlbuilder": {
- "version": "11.0.1",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
- "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/xmlbuilder": {
- "version": "15.1.1",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
- "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
- "license": "MIT",
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/xmlchars": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
- "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/y18n": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "license": "ISC"
- },
- "node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- },
- "node_modules/yargs": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
- "license": "MIT",
- "dependencies": {
- "cliui": "^8.0.1",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.3",
- "y18n": "^5.0.5",
- "yargs-parser": "^21.1.1"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/yargs-parser": {
- "version": "21.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
- "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
- "license": "ISC",
- "engines": {
- "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==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/zustand": {
- "version": "5.0.11",
- "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
- "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- },
- "peerDependencies": {
- "@types/react": ">=18.0.0",
- "immer": ">=9.0.6",
- "react": ">=18.0.0",
- "use-sync-external-store": ">=1.2.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "immer": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "use-sync-external-store": {
- "optional": true
- }
- }
- }
- }
-}
diff --git a/package.json b/package.json
deleted file mode 100644
index 3574219..0000000
--- a/package.json
+++ /dev/null
@@ -1,97 +0,0 @@
-{
- "name": "tabatafit",
- "main": "expo-router/entry",
- "version": "1.0.0",
- "scripts": {
- "start": "expo start",
- "reset-project": "node ./scripts/reset-project.js",
- "android": "expo run:android",
- "ios": "expo run:ios",
- "web": "expo start --web",
- "lint": "expo lint",
- "test": "vitest run",
- "test:watch": "vitest",
- "test:coverage": "vitest run --coverage",
- "test:render": "vitest run --config vitest.config.render.ts",
- "test:maestro": "maestro test .maestro/flows",
- "test:maestro:onboarding": "maestro test .maestro/flows/onboarding.yaml",
- "test:maestro:programs": "maestro test .maestro/flows/program-browse.yaml",
- "test:maestro:tabs": "maestro test .maestro/flows/tab-navigation.yaml",
- "test:maestro:paywall": "maestro test .maestro/flows/subscription.yaml",
- "test:maestro:player": "maestro test .maestro/flows/workout-player.yaml",
- "test:maestro:activity": "maestro test .maestro/flows/activity-tab.yaml",
- "test:maestro:profile": "maestro test .maestro/flows/profile-settings.yaml",
- "test:maestro:all": "maestro test .maestro/flows/all-tests.yaml",
- "test:maestro:reset": "maestro test .maestro/flows/reset-state.yaml"
- },
- "dependencies": {
- "@expo-google-fonts/dm-mono": "^0.4.2",
- "@expo-google-fonts/dm-serif-display": "^0.4.2",
- "@expo-google-fonts/inter": "^0.4.2",
- "@expo-google-fonts/outfit": "^0.4.3",
- "@expo/ui": "~0.2.0-beta.9",
- "@react-native-async-storage/async-storage": "^2.2.0",
- "@react-navigation/bottom-tabs": "^7.4.0",
- "@react-navigation/elements": "^2.6.3",
- "@react-navigation/native": "^7.1.8",
- "@supabase/supabase-js": "^2.98.0",
- "@tanstack/query-async-storage-persister": "^5.90.24",
- "@tanstack/react-query": "^5.90.21",
- "@tanstack/react-query-persist-client": "^5.90.24",
- "expo": "~54.0.33",
- "expo-application": "~7.0.8",
- "expo-av": "~16.0.8",
- "expo-blur": "~15.0.8",
- "expo-constants": "~18.0.13",
- "expo-device": "~8.0.10",
- "expo-file-system": "~19.0.21",
- "expo-font": "~14.0.11",
- "expo-gl": "~16.0.10",
- "expo-haptics": "~15.0.8",
- "expo-image": "~3.0.11",
- "expo-keep-awake": "~15.0.8",
- "expo-linear-gradient": "~15.0.8",
- "expo-linking": "~8.0.11",
- "expo-localization": "~17.0.8",
- "expo-network": "~8.0.8",
- "expo-notifications": "~0.32.16",
- "expo-router": "~6.0.23",
- "expo-sharing": "~14.0.8",
- "expo-splash-screen": "~31.0.13",
- "expo-status-bar": "~3.0.9",
- "expo-store-review": "~9.0.9",
- "expo-symbols": "~1.0.8",
- "expo-system-ui": "~6.0.9",
- "expo-video": "~3.0.16",
- "expo-web-browser": "~15.0.10",
- "i18next": "^25.8.12",
- "posthog-react-native": "^4.36.0",
- "posthog-react-native-session-replay": "^1.5.0",
- "react": "19.1.0",
- "react-dom": "19.1.0",
- "react-i18next": "^16.5.4",
- "react-native": "0.81.5",
- "react-native-gesture-handler": "~2.28.0",
- "react-native-purchases": "^9.10.3",
- "react-native-reanimated": "~4.1.1",
- "react-native-safe-area-context": "~5.6.0",
- "react-native-screens": "~4.16.0",
- "react-native-svg": "15.12.1",
- "react-native-web": "~0.21.0",
- "react-native-worklets": "0.5.1",
- "zustand": "^5.0.11"
- },
- "devDependencies": {
- "@testing-library/jest-native": "^5.4.3",
- "@testing-library/react-native": "^13.3.3",
- "@types/react": "~19.1.0",
- "@vitest/coverage-v8": "^4.1.1",
- "eslint": "^9.25.0",
- "eslint-config-expo": "~10.0.0",
- "jsdom": "^29.0.1",
- "react-test-renderer": "^19.1.0",
- "typescript": "~5.9.2",
- "vitest": "^4.1.1"
- },
- "private": true
-}
diff --git a/plugins/CLAUDE.md b/plugins/CLAUDE.md
deleted file mode 100644
index adfdcb1..0000000
--- a/plugins/CLAUDE.md
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# Recent Activity
-
-
-
-*No recent activity*
-
\ No newline at end of file
diff --git a/plugins/withStoreKitConfig.js b/plugins/withStoreKitConfig.js
deleted file mode 100644
index 11fb6a9..0000000
--- a/plugins/withStoreKitConfig.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/**
- * Expo Config Plugin: StoreKit Configuration for Sandbox Testing
- *
- * Adds TabataFit.storekit to the Xcode project and configures the scheme
- * to use it for StoreKit testing. This enables purchase testing in the
- * iOS simulator without real Apple Pay charges.
- */
-
-const {
- withXcodeProject,
- withDangerousMod,
-} = require('@expo/config-plugins')
-const fs = require('fs')
-const path = require('path')
-
-const STOREKIT_FILENAME = 'TabataFit.storekit'
-
-/**
- * Step 1: Copy the .storekit file and add it to the Xcode project (project.pbxproj)
- */
-function withStoreKitXcodeProject(config) {
- return withXcodeProject(config, (config) => {
- const project = config.modResults
- const projectName = config.modRequest.projectName
- const platformRoot = config.modRequest.platformProjectRoot
-
- // Copy .storekit file into the iOS app directory
- const sourceFile = path.resolve(__dirname, '..', 'storekit', STOREKIT_FILENAME)
- const destDir = path.join(platformRoot, projectName)
- const destFile = path.join(destDir, STOREKIT_FILENAME)
- fs.copyFileSync(sourceFile, destFile)
-
- // Add file to the Xcode project manually via pbxproj APIs
- // Find the app's PBXGroup
- const mainGroupKey = project.getFirstProject().firstProject.mainGroup
- const mainGroup = project.getPBXGroupByKey(mainGroupKey)
-
- // Find the app target group (e.g., "tabatago")
- let appGroupKey = null
- if (mainGroup && mainGroup.children) {
- for (const child of mainGroup.children) {
- if (child.comment === projectName) {
- appGroupKey = child.value
- break
- }
- }
- }
-
- if (!appGroupKey) {
- console.warn('[withStoreKitConfig] Could not find app group in Xcode project')
- return config
- }
-
- const appGroup = project.getPBXGroupByKey(appGroupKey)
-
- // Check if already added
- const alreadyExists = appGroup.children?.some(
- (child) => child.comment === STOREKIT_FILENAME
- )
-
- if (!alreadyExists) {
- // Generate a unique UUID for the file reference
- const fileRefUuid = project.generateUuid()
-
- // Add PBXFileReference — NOT added to any build phase
- // .storekit files are testing configs, not app resources
- const pbxFileRef = project.hash.project.objects['PBXFileReference']
- pbxFileRef[fileRefUuid] = {
- isa: 'PBXFileReference',
- lastKnownFileType: 'text.json',
- path: STOREKIT_FILENAME,
- sourceTree: '""',
- }
- pbxFileRef[`${fileRefUuid}_comment`] = STOREKIT_FILENAME
-
- // Add to the app group's children (visible in Xcode navigator)
- appGroup.children.push({
- value: fileRefUuid,
- comment: STOREKIT_FILENAME,
- })
-
- console.log('[withStoreKitConfig] Added', STOREKIT_FILENAME, 'to Xcode project')
- }
-
- return config
- })
-}
-
-/**
- * Step 2: Configure the Xcode scheme to use the StoreKit configuration
- */
-function withStoreKitScheme(config) {
- return withDangerousMod(config, [
- 'ios',
- (config) => {
- const projectName = config.modRequest.projectName
- const schemePath = path.join(
- config.modRequest.platformProjectRoot,
- `${projectName}.xcodeproj`,
- 'xcshareddata',
- 'xcschemes',
- `${projectName}.xcscheme`
- )
-
- if (!fs.existsSync(schemePath)) {
- console.warn('[withStoreKitConfig] Scheme not found:', schemePath)
- return config
- }
-
- let scheme = fs.readFileSync(schemePath, 'utf8')
-
- // Skip if already configured
- if (scheme.includes('storeKitConfigurationFileReference')) {
- console.log('[withStoreKitConfig] StoreKit config already in scheme')
- return config
- }
-
- // Insert StoreKitConfigurationFileReference as a child element of LaunchAction
- // The identifier path is relative to the workspace root (ios/ directory)
- const storeKitRef = `
-
- `
-
- // Insert before the closing tag
- scheme = scheme.replace(
- '',
- `${storeKitRef}\n `
- )
-
- fs.writeFileSync(schemePath, scheme, 'utf8')
- console.log('[withStoreKitConfig] Added StoreKit config to scheme')
-
- return config
- },
- ])
-}
-
-/**
- * Main plugin
- */
-function withStoreKitConfig(config) {
- config = withStoreKitXcodeProject(config)
- config = withStoreKitScheme(config)
- return config
-}
-
-module.exports = withStoreKitConfig
diff --git a/skills-lock.json b/skills-lock.json
deleted file mode 100644
index cf87c09..0000000
--- a/skills-lock.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "version": 1,
- "skills": {
- "building-native-ui": {
- "source": "expo/skills",
- "sourceType": "github",
- "computedHash": "342df93f481a0dba919f372d6c7b40d2b4bf5b51dd24363aea2e5d0bae27a6fa"
- },
- "expo-api-routes": {
- "source": "expo/skills",
- "sourceType": "github",
- "computedHash": "015c6b849507fda73fcc32d2448f033aaaaa21f5229085342b8421727a90cafb"
- },
- "expo-cicd-workflows": {
- "source": "expo/skills",
- "sourceType": "github",
- "computedHash": "700b20b575fcbe75ad238b41a0bd57938abe495e62dc53e05400712ab01ee7c0"
- },
- "expo-deployment": {
- "source": "expo/skills",
- "sourceType": "github",
- "computedHash": "9ea9f16374765c1b16764a51bd43a64098921b33f48e94d9c5c1cce24b335c10"
- },
- "expo-dev-client": {
- "source": "expo/skills",
- "sourceType": "github",
- "computedHash": "234e2633b7fbcef2d479f8fe8ab20d53d08ed3e4beec7c965da4aff5b43affe7"
- },
- "expo-tailwind-setup": {
- "source": "expo/skills",
- "sourceType": "github",
- "computedHash": "d39e806942fe880347f161056729b588a3cb0f1796270eebf52633fe11cfdce1"
- },
- "native-data-fetching": {
- "source": "expo/skills",
- "sourceType": "github",
- "computedHash": "6c14e4efb34a9c4759e8b959f82dec328f87dd89a022957c6737086984b9b106"
- }
- }
-}
diff --git a/src/__tests__/components/StyledText.test.tsx b/src/__tests__/components/StyledText.test.tsx
deleted file mode 100644
index 7e3a7a2..0000000
--- a/src/__tests__/components/StyledText.test.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { describe, it, expect } from 'vitest'
-
-type FontWeight = 'regular' | 'medium' | 'semibold' | 'bold'
-
-const WEIGHT_MAP: Record = {
- regular: '400',
- medium: '500',
- semibold: '600',
- bold: '700',
-}
-
-describe('StyledText', () => {
- describe('weight mapping', () => {
- it('should map regular to 400', () => {
- expect(WEIGHT_MAP['regular']).toBe('400')
- })
-
- it('should map medium to 500', () => {
- expect(WEIGHT_MAP['medium']).toBe('500')
- })
-
- it('should map semibold to 600', () => {
- expect(WEIGHT_MAP['semibold']).toBe('600')
- })
-
- it('should map bold to 700', () => {
- expect(WEIGHT_MAP['bold']).toBe('700')
- })
- })
-
- describe('default values', () => {
- it('should have default size of 17', () => {
- const defaultSize = 17
- expect(defaultSize).toBe(17)
- })
-
- it('should have default weight of regular', () => {
- const defaultWeight: FontWeight = 'regular'
- expect(WEIGHT_MAP[defaultWeight]).toBe('400')
- })
- })
-
- describe('style computation', () => {
- const computeTextStyle = (size: number, weight: FontWeight, color: string) => ({
- fontSize: size,
- fontWeight: WEIGHT_MAP[weight],
- color,
- })
-
- it('should compute correct style with defaults', () => {
- const style = computeTextStyle(17, 'regular', '#FFFFFF')
- expect(style.fontSize).toBe(17)
- expect(style.fontWeight).toBe('400')
- expect(style.color).toBe('#FFFFFF')
- })
-
- it('should compute correct style with custom size', () => {
- const style = computeTextStyle(24, 'regular', '#FFFFFF')
- expect(style.fontSize).toBe(24)
- })
-
- it('should compute correct style with bold weight', () => {
- const style = computeTextStyle(17, 'bold', '#FFFFFF')
- expect(style.fontWeight).toBe('700')
- })
-
- it('should compute correct style with custom color', () => {
- const style = computeTextStyle(17, 'regular', '#FF0000')
- expect(style.color).toBe('#FF0000')
- })
-
- it('should compute correct style with all custom props', () => {
- const style = computeTextStyle(20, 'semibold', '#5AC8FA')
- expect(style.fontSize).toBe(20)
- expect(style.fontWeight).toBe('600')
- expect(style.color).toBe('#5AC8FA')
- })
- })
-
- describe('numberOfLines handling', () => {
- it('should accept numberOfLines prop', () => {
- const numberOfLines = 2
- expect(numberOfLines).toBe(2)
- })
-
- it('should handle undefined numberOfLines', () => {
- const numberOfLines: number | undefined = undefined
- expect(numberOfLines).toBeUndefined()
- })
- })
-
- describe('style merging', () => {
- const mergeStyles = (baseStyle: object, customStyle: object | undefined) => {
- return customStyle ? [baseStyle, customStyle] : [baseStyle]
- }
-
- it('should merge custom style with base style', () => {
- const base = { fontSize: 17, fontWeight: '400' }
- const custom = { marginTop: 10 }
- const merged = mergeStyles(base, custom)
-
- expect(merged).toHaveLength(2)
- expect(merged[0]).toEqual(base)
- expect(merged[1]).toEqual(custom)
- })
-
- it('should return only base style when no custom style', () => {
- const base = { fontSize: 17, fontWeight: '400' }
- const merged = mergeStyles(base, undefined)
-
- expect(merged).toHaveLength(1)
- expect(merged[0]).toEqual(base)
- })
- })
-
- describe('theme color integration', () => {
- const mockThemeColors = {
- text: {
- primary: '#FFFFFF',
- secondary: '#8E8E93',
- tertiary: '#636366',
- },
- }
-
- it('should use primary text color as default', () => {
- const defaultColor = mockThemeColors.text.primary
- expect(defaultColor).toBe('#FFFFFF')
- })
-
- it('should allow color override', () => {
- const customColor = '#FF0000'
- const resolvedColor = customColor || mockThemeColors.text.primary
- expect(resolvedColor).toBe('#FF0000')
- })
-
- it('should fallback to theme color when no override', () => {
- const customColor: string | undefined = undefined
- const resolvedColor = customColor || mockThemeColors.text.primary
- expect(resolvedColor).toBe('#FFFFFF')
- })
- })
-})
diff --git a/src/__tests__/components/VideoPlayer.test.tsx b/src/__tests__/components/VideoPlayer.test.tsx
deleted file mode 100644
index e205e9e..0000000
--- a/src/__tests__/components/VideoPlayer.test.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import { BRAND } from '../../shared/constants/colors'
-
-type VideoPlayerMode = 'preview' | 'background'
-
-interface VideoPlayerConfig {
- loop: boolean
- muted: boolean
- volume: number
-}
-
-function getVideoPlayerConfig(mode: VideoPlayerMode): VideoPlayerConfig {
- return {
- loop: true,
- muted: mode === 'preview',
- volume: mode === 'background' ? 0.3 : 0,
- }
-}
-
-function shouldShowGradient(videoUrl: string | undefined): boolean {
- return !videoUrl
-}
-
-function shouldPlayVideo(isPlaying: boolean, videoUrl: string | undefined): boolean {
- return isPlaying && !!videoUrl
-}
-
-describe('VideoPlayer', () => {
- describe('video player configuration', () => {
- it('should configure preview mode with muted audio', () => {
- const config = getVideoPlayerConfig('preview')
- expect(config.loop).toBe(true)
- expect(config.muted).toBe(true)
- expect(config.volume).toBe(0)
- })
-
- it('should configure background mode with low volume', () => {
- const config = getVideoPlayerConfig('background')
- expect(config.loop).toBe(true)
- expect(config.muted).toBe(false)
- expect(config.volume).toBe(0.3)
- })
-
- it('should always loop regardless of mode', () => {
- expect(getVideoPlayerConfig('preview').loop).toBe(true)
- expect(getVideoPlayerConfig('background').loop).toBe(true)
- })
- })
-
- describe('gradient fallback', () => {
- it('should show gradient when no video URL', () => {
- expect(shouldShowGradient(undefined)).toBe(true)
- expect(shouldShowGradient('')).toBe(true)
- })
-
- it('should not show gradient when video URL exists', () => {
- expect(shouldShowGradient('https://example.com/video.m3u8')).toBe(false)
- expect(shouldShowGradient('https://example.com/video.mp4')).toBe(false)
- })
- })
-
- describe('playback control', () => {
- it('should play when isPlaying is true and video exists', () => {
- expect(shouldPlayVideo(true, 'https://example.com/video.mp4')).toBe(true)
- })
-
- it('should not play when isPlaying is false', () => {
- expect(shouldPlayVideo(false, 'https://example.com/video.mp4')).toBe(false)
- })
-
- it('should not play when no video URL', () => {
- expect(shouldPlayVideo(true, undefined)).toBe(false)
- expect(shouldPlayVideo(true, '')).toBe(false)
- })
- })
-
- describe('default gradient colors', () => {
- it('should use brand colors as default gradient', () => {
- const defaultColors = [BRAND.PRIMARY, BRAND.SECONDARY]
- expect(defaultColors[0]).toBe(BRAND.PRIMARY)
- expect(defaultColors[1]).toBe(BRAND.SECONDARY)
- })
- })
-
- describe('video URL validation', () => {
- it('should accept HLS streams', () => {
- const hlsUrl = 'https://example.com/video.m3u8'
- expect(shouldShowGradient(hlsUrl)).toBe(false)
- })
-
- it('should accept MP4 files', () => {
- const mp4Url = 'https://example.com/video.mp4'
- expect(shouldShowGradient(mp4Url)).toBe(false)
- })
-
- it('should handle null/undefined', () => {
- expect(shouldShowGradient(null as any)).toBe(true)
- expect(shouldShowGradient(undefined)).toBe(true)
- })
- })
-
- describe('mode-specific behavior', () => {
- it('preview mode should be silent', () => {
- const previewConfig = getVideoPlayerConfig('preview')
- expect(previewConfig.muted || previewConfig.volume === 0).toBe(true)
- })
-
- it('background mode should have audible audio', () => {
- const bgConfig = getVideoPlayerConfig('background')
- expect(bgConfig.volume).toBeGreaterThan(0)
- })
- })
-})
diff --git a/src/__tests__/components/rendering/DataDeletionModal.test.tsx b/src/__tests__/components/rendering/DataDeletionModal.test.tsx
deleted file mode 100644
index 04eab39..0000000
--- a/src/__tests__/components/rendering/DataDeletionModal.test.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import React from 'react'
-import { render, screen, fireEvent, waitFor, act } from '@testing-library/react-native'
-import { DataDeletionModal } from '@/src/shared/components/DataDeletionModal'
-
-describe('DataDeletionModal', () => {
- const defaultProps = {
- visible: true,
- onDelete: vi.fn().mockResolvedValue(undefined),
- onCancel: vi.fn(),
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('renders when visible is true', () => {
- render()
- // Title key from i18n mock
- expect(screen.getByText('dataDeletion.title')).toBeTruthy()
- })
-
- it('renders warning icon', () => {
- render()
- expect(screen.getByTestId('icon-warning')).toBeTruthy()
- })
-
- it('renders description and note text', () => {
- render()
- expect(screen.getByText('dataDeletion.description')).toBeTruthy()
- expect(screen.getByText('dataDeletion.note')).toBeTruthy()
- })
-
- it('renders delete and cancel buttons', () => {
- render()
- expect(screen.getByText('dataDeletion.deleteButton')).toBeTruthy()
- expect(screen.getByText('dataDeletion.cancelButton')).toBeTruthy()
- })
-
- it('calls onCancel when cancel button is pressed', () => {
- render()
- fireEvent.press(screen.getByText('dataDeletion.cancelButton'))
- expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
- })
-
- it('calls onDelete when delete button is pressed', async () => {
- render()
- await act(async () => {
- fireEvent.press(screen.getByText('dataDeletion.deleteButton'))
- })
- expect(defaultProps.onDelete).toHaveBeenCalledTimes(1)
- })
-
- it('shows loading text while deleting', async () => {
- let resolveDelete: () => void
- const slowDelete = new Promise((resolve) => {
- resolveDelete = resolve
- })
- const onDelete = vi.fn(() => slowDelete)
-
- render()
-
- // Start delete
- await act(async () => {
- fireEvent.press(screen.getByText('dataDeletion.deleteButton'))
- })
-
- // Should show 'Deleting...' while in progress
- expect(screen.getByText('Deleting...')).toBeTruthy()
-
- // Complete the delete
- await act(async () => {
- resolveDelete!()
- })
- })
-
- it('does not render content when visible is false', () => {
- render()
- // Modal with visible=false won't render its children
- expect(screen.queryByText('dataDeletion.title')).toBeNull()
- })
-
- it('full modal structure snapshot', () => {
- const { toJSON } = render()
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('delete button shows disabled state while deleting', async () => {
- let resolveDelete: () => void
- const slowDelete = new Promise((resolve) => {
- resolveDelete = resolve
- })
- const onDelete = vi.fn(() => slowDelete)
-
- const { toJSON } = render(
-
- )
-
- await act(async () => {
- fireEvent.press(screen.getByText('dataDeletion.deleteButton'))
- })
-
- // While deleting, the button text changes to loading state
- expect(screen.getByText('Deleting...')).toBeTruthy()
-
- // Verify the tree has the disabled styling applied (opacity: 0.6)
- const tree = toJSON()
- const treeStr = JSON.stringify(tree)
- expect(treeStr).toContain('"opacity":0.6')
-
- await act(async () => {
- resolveDelete!()
- })
- })
-})
diff --git a/src/__tests__/components/rendering/GlassCard.test.tsx b/src/__tests__/components/rendering/GlassCard.test.tsx
deleted file mode 100644
index 1f7bf4c..0000000
--- a/src/__tests__/components/rendering/GlassCard.test.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import React from 'react'
-import { render, screen } from '@testing-library/react-native'
-import { Text } from 'react-native'
-import { Card, CardAccent, CardTip, GlassCard, GlassCardElevated, GlassCardInset, GlassCardTinted } from '@/src/shared/components/GlassCard'
-
-describe('Card', () => {
- it('renders children', () => {
- render(
-
- Hello
-
- )
- expect(screen.getByTestId('child')).toBeTruthy()
- })
-
- it('applies custom style prop to root container', () => {
- const customStyle = { padding: 20 }
- const { toJSON } = render(
-
- Content
-
- )
- const tree = toJSON()
- const rootStyle = tree?.props?.style
- expect(rootStyle).toBeDefined()
- const flatStyles = Array.isArray(rootStyle) ? rootStyle : [rootStyle]
- const hasPadding = flatStyles.some(
- (s: any) => s && typeof s === 'object' && s.padding === 20
- )
- expect(hasPadding).toBe(true)
- })
-})
-
-describe('Card variants', () => {
- it('renders default variant (snapshot)', () => {
- const { toJSON } = render(
-
- Default
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('renders accent variant (snapshot)', () => {
- const { toJSON } = render(
-
- Accent
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('renders tip variant (snapshot)', () => {
- const { toJSON } = render(
-
- Tip
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
-})
-
-describe('Backward-compatible aliases', () => {
- it('GlassCard renders children', () => {
- render(
-
- Backward compat
-
- )
- expect(screen.getByTestId('bc-child')).toBeTruthy()
- })
-
- it('GlassCardElevated renders children', () => {
- render(
-
- Elevated
-
- )
- expect(screen.getByTestId('elevated-child')).toBeTruthy()
- })
-
- it('GlassCardInset renders children', () => {
- render(
-
- Inset
-
- )
- expect(screen.getByTestId('inset-child')).toBeTruthy()
- })
-
- it('GlassCardTinted renders children', () => {
- render(
-
- Tinted
-
- )
- expect(screen.getByTestId('tinted-child')).toBeTruthy()
- })
-})
diff --git a/src/__tests__/components/rendering/OnboardingStep.test.tsx b/src/__tests__/components/rendering/OnboardingStep.test.tsx
deleted file mode 100644
index e78601d..0000000
--- a/src/__tests__/components/rendering/OnboardingStep.test.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import React from 'react'
-import { render, screen } from '@testing-library/react-native'
-import { Text } from 'react-native'
-import { OnboardingStep } from '@/src/shared/components/OnboardingStep'
-
-/**
- * Helper to recursively find a node in the rendered tree by its element type name.
- * Returns the first match or null.
- */
-function findByType(tree: any, typeName: string): any {
- if (!tree) return null
- if (tree.type === typeName) return tree
- if (tree.children && Array.isArray(tree.children)) {
- for (const child of tree.children) {
- if (typeof child === 'object') {
- const found = findByType(child, typeName)
- if (found) return found
- }
- }
- }
- return null
-}
-
-/**
- * Helper to count nodes of a given type in the tree
- */
-function countByType(tree: any, typeName: string): number {
- if (!tree) return 0
- let count = tree.type === typeName ? 1 : 0
- if (tree.children && Array.isArray(tree.children)) {
- for (const child of tree.children) {
- if (typeof child === 'object') {
- count += countByType(child, typeName)
- }
- }
- }
- return count
-}
-
-describe('OnboardingStep', () => {
- it('renders children', () => {
- render(
-
- Welcome
-
- )
- expect(screen.getByTestId('child-content')).toBeTruthy()
- })
-
- it('renders progress bar with track and fill Views', () => {
- const { toJSON } = render(
-
- Step 1
-
- )
- const tree = toJSON()
- // OnboardingStep should have:
- // - A root View (container)
- // - A View (progressTrack)
- // - An Animated.View (progressFill) — rendered as View by mock
- // - An Animated.View (content wrapper)
- expect(tree).toBeTruthy()
- expect(tree?.type).toBe('View') // root container
- expect(tree?.children).toBeDefined()
- expect(tree!.children!.length).toBeGreaterThanOrEqual(2) // progress track + content
- })
-
- it('step 1 of 6 snapshot', () => {
- const { toJSON } = render(
-
- First step
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('step 6 of 6 (final step) snapshot', () => {
- const { toJSON } = render(
-
- Final step
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('renders multiple children inside content area', () => {
- render(
-
- Title
- Description
-
- )
- expect(screen.getByTestId('title')).toBeTruthy()
- expect(screen.getByTestId('description')).toBeTruthy()
- })
-
- it('does not crash with step 0 (edge case snapshot)', () => {
- const { toJSON } = render(
-
- Edge case
-
- )
- // Should render without error — snapshot captures structure
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('container uses safe area top inset for paddingTop', () => {
- const { toJSON } = render(
-
- Check padding
-
- )
- const tree = toJSON()
- // Root container should have paddingTop accounting for safe area (mock returns top=47)
- const rootStyle = tree?.props?.style
- const flatStyles = Array.isArray(rootStyle) ? rootStyle : [rootStyle]
- const hasPaddingTop = flatStyles.some(
- (s: any) => s && typeof s === 'object' && typeof s.paddingTop === 'number' && s.paddingTop > 0
- )
- expect(hasPaddingTop).toBe(true)
- })
-})
diff --git a/src/__tests__/components/rendering/SyncConsentModal.test.tsx b/src/__tests__/components/rendering/SyncConsentModal.test.tsx
deleted file mode 100644
index 36a4b5b..0000000
--- a/src/__tests__/components/rendering/SyncConsentModal.test.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import React from 'react'
-import { render, screen, fireEvent, act } from '@testing-library/react-native'
-import { SyncConsentModal } from '@/src/shared/components/SyncConsentModal'
-
-describe('SyncConsentModal', () => {
- const defaultProps = {
- visible: true,
- onAccept: vi.fn().mockResolvedValue(undefined),
- onDecline: vi.fn(),
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('renders when visible is true', () => {
- render()
- expect(screen.getByText('sync.title')).toBeTruthy()
- })
-
- it('renders sparkles icon', () => {
- render()
- expect(screen.getByTestId('icon-sparkles')).toBeTruthy()
- })
-
- it('renders benefit rows', () => {
- render()
- expect(screen.getByText('sync.benefits.recommendations')).toBeTruthy()
- expect(screen.getByText('sync.benefits.adaptive')).toBeTruthy()
- expect(screen.getByText('sync.benefits.sync')).toBeTruthy()
- expect(screen.getByText('sync.benefits.secure')).toBeTruthy()
- })
-
- it('renders benefit icons', () => {
- render()
- expect(screen.getByTestId('icon-trending-up')).toBeTruthy()
- expect(screen.getByTestId('icon-fitness')).toBeTruthy()
- expect(screen.getByTestId('icon-sync')).toBeTruthy()
- expect(screen.getByTestId('icon-shield-checkmark')).toBeTruthy()
- })
-
- it('renders privacy note', () => {
- render()
- expect(screen.getByText('sync.privacy')).toBeTruthy()
- })
-
- it('renders primary and secondary buttons', () => {
- render()
- expect(screen.getByText('sync.primaryButton')).toBeTruthy()
- expect(screen.getByText('sync.secondaryButton')).toBeTruthy()
- })
-
- it('calls onDecline when secondary button is pressed', () => {
- render()
- fireEvent.press(screen.getByText('sync.secondaryButton'))
- expect(defaultProps.onDecline).toHaveBeenCalledTimes(1)
- })
-
- it('calls onAccept when primary button is pressed', async () => {
- render()
- await act(async () => {
- fireEvent.press(screen.getByText('sync.primaryButton'))
- })
- expect(defaultProps.onAccept).toHaveBeenCalledTimes(1)
- })
-
- it('shows loading text while accepting', async () => {
- let resolveAccept: () => void
- const slowAccept = new Promise((resolve) => {
- resolveAccept = resolve
- })
- const onAccept = vi.fn(() => slowAccept)
-
- render()
-
- await act(async () => {
- fireEvent.press(screen.getByText('sync.primaryButton'))
- })
-
- expect(screen.getByText('Setting up...')).toBeTruthy()
-
- await act(async () => {
- resolveAccept!()
- })
- })
-
- it('does not render content when visible is false', () => {
- render()
- expect(screen.queryByText('sync.title')).toBeNull()
- })
-
- it('full modal structure snapshot', () => {
- const { toJSON } = render()
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('primary button shows disabled state while loading', async () => {
- let resolveAccept: () => void
- const slowAccept = new Promise((resolve) => {
- resolveAccept = resolve
- })
- const onAccept = vi.fn(() => slowAccept)
-
- const { toJSON } = render(
-
- )
-
- await act(async () => {
- fireEvent.press(screen.getByText('sync.primaryButton'))
- })
-
- // While loading, button text changes to loading state
- expect(screen.getByText('Setting up...')).toBeTruthy()
-
- // Verify the tree has the disabled styling applied (opacity: 0.6)
- const tree = toJSON()
- const treeStr = JSON.stringify(tree)
- expect(treeStr).toContain('"opacity":0.6')
-
- await act(async () => {
- resolveAccept!()
- })
- })
-})
diff --git a/src/__tests__/components/rendering/VideoPlayerPreview.test.tsx b/src/__tests__/components/rendering/VideoPlayerPreview.test.tsx
deleted file mode 100644
index ab788a8..0000000
--- a/src/__tests__/components/rendering/VideoPlayerPreview.test.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import React from 'react'
-import { render } from '@testing-library/react-native'
-import { VideoPlayer } from '@/src/shared/components/VideoPlayer'
-
-describe('VideoPlayer rendering', () => {
- describe('preview mode', () => {
- it('renders gradient fallback when no videoUrl', () => {
- const { toJSON } = render(
-
- )
- const tree = toJSON()
- expect(tree).toBeTruthy()
- expect(tree).toMatchSnapshot()
- })
-
- it('renders video view when videoUrl is provided', () => {
- const { toJSON } = render(
-
- )
- const tree = toJSON()
- expect(tree).toBeTruthy()
- expect(tree).toMatchSnapshot()
- })
-
- it('renders with custom style', () => {
- const { toJSON } = render(
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('renders with testID prop', () => {
- const { getByTestId } = render(
-
- )
- expect(getByTestId('my-video-player')).toBeTruthy()
- })
- })
-
- describe('background mode', () => {
- it('renders gradient fallback when no videoUrl', () => {
- const { toJSON } = render(
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
-
- it('renders video view when videoUrl is provided', () => {
- const { toJSON } = render(
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
- })
-
- describe('custom gradient colors', () => {
- it('renders with custom gradient colors when no video', () => {
- const { toJSON } = render(
-
- )
- expect(toJSON()).toMatchSnapshot()
- })
- })
-})
diff --git a/src/__tests__/components/rendering/__snapshots__/CollectionCard.test.tsx.snap b/src/__tests__/components/rendering/__snapshots__/CollectionCard.test.tsx.snap
deleted file mode 100644
index 9444e64..0000000
--- a/src/__tests__/components/rendering/__snapshots__/CollectionCard.test.tsx.snap
+++ /dev/null
@@ -1,324 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`CollectionCard > renders without onPress (no crash) 1`] = `
-
-
-
-
-
-
-
-
- 💪
-
-
-
- Upper Body Blast
-
-
- 3
- workouts
-
-
-
-`;
-
-exports[`CollectionCard > snapshot with imageUrl (different rendering path) 1`] = `
-
-
-
-
-
-
-
-
-
-
- 💪
-
-
-
- Upper Body Blast
-
-
- 3
- workouts
-
-
-
-`;
diff --git a/src/__tests__/components/rendering/__snapshots__/GlassCard.test.tsx.snap b/src/__tests__/components/rendering/__snapshots__/GlassCard.test.tsx.snap
deleted file mode 100644
index 52ff0e4..0000000
--- a/src/__tests__/components/rendering/__snapshots__/GlassCard.test.tsx.snap
+++ /dev/null
@@ -1,283 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`GlassCard presets > GlassCardElevated snapshot 1`] = `
-
-
-
-
- Elevated preset
-
-
-
-`;
-
-exports[`GlassCard variants > renders base variant (snapshot) 1`] = `
-
-
-
-
- Base
-
-
-
-`;
-
-exports[`GlassCard variants > renders elevated variant (snapshot) 1`] = `
-
-
-
-
- Elevated
-
-
-
-`;
-
-exports[`GlassCard variants > renders inset variant (snapshot) 1`] = `
-
-
-
-
- Inset
-
-
-
-`;
-
-exports[`GlassCard variants > renders tinted variant (snapshot) 1`] = `
-
-
-
-
- Tinted
-
-
-
-`;
diff --git a/src/__tests__/components/rendering/__snapshots__/OnboardingStep.test.tsx.snap b/src/__tests__/components/rendering/__snapshots__/OnboardingStep.test.tsx.snap
deleted file mode 100644
index 345b769..0000000
--- a/src/__tests__/components/rendering/__snapshots__/OnboardingStep.test.tsx.snap
+++ /dev/null
@@ -1,277 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`OnboardingStep > does not crash with step 0 (edge case snapshot) 1`] = `
-
-
-
-
-
-
- Edge case
-
-
-
-`;
-
-exports[`OnboardingStep > step 1 of 6 snapshot 1`] = `
-
-
-
-
-
-
- First step
-
-
-
-`;
-
-exports[`OnboardingStep > step 6 of 6 (final step) snapshot 1`] = `
-
-
-
-
-
-
- Final step
-
-
-
-`;
diff --git a/src/__tests__/components/rendering/__snapshots__/Skeleton.test.tsx.snap b/src/__tests__/components/rendering/__snapshots__/Skeleton.test.tsx.snap
deleted file mode 100644
index 30299eb..0000000
--- a/src/__tests__/components/rendering/__snapshots__/Skeleton.test.tsx.snap
+++ /dev/null
@@ -1,799 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`CollectionCardSkeleton > renders correct structure (snapshot) 1`] = `
-
-
-
-
-
-
-
-
-`;
-
-exports[`Skeleton > renders with default dimensions (snapshot) 1`] = `
-
-
-
-`;
-
-exports[`StatsCardSkeleton > renders correct structure (snapshot) 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`TrainerCardSkeleton > renders correct structure (snapshot) 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`WorkoutCardSkeleton > renders correct structure (snapshot) 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/__tests__/components/rendering/__snapshots__/VideoPlayerPreview.test.tsx.snap b/src/__tests__/components/rendering/__snapshots__/VideoPlayerPreview.test.tsx.snap
deleted file mode 100644
index 29e569a..0000000
--- a/src/__tests__/components/rendering/__snapshots__/VideoPlayerPreview.test.tsx.snap
+++ /dev/null
@@ -1,292 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`VideoPlayer rendering > background mode > renders gradient fallback when no videoUrl 1`] = `
-
-
-
-`;
-
-exports[`VideoPlayer rendering > background mode > renders video view when videoUrl is provided 1`] = `
-
-
-
-`;
-
-exports[`VideoPlayer rendering > custom gradient colors > renders with custom gradient colors when no video 1`] = `
-
-
-
-`;
-
-exports[`VideoPlayer rendering > preview mode > renders gradient fallback when no videoUrl 1`] = `
-
-
-
-`;
-
-exports[`VideoPlayer rendering > preview mode > renders video view when videoUrl is provided 1`] = `
-
-
-
-`;
-
-exports[`VideoPlayer rendering > preview mode > renders with custom style 1`] = `
-
-
-
-`;
diff --git a/src/__tests__/features/player.test.ts b/src/__tests__/features/player.test.ts
deleted file mode 100644
index 5e1f469..0000000
--- a/src/__tests__/features/player.test.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-/**
- * Player feature unit tests
- * Tests constants, getCoachMessage, and component prop contracts
- */
-
-import { describe, it, expect } from 'vitest'
-import {
- TIMER_RING_SIZE,
- TIMER_RING_STROKE,
- COACH_MESSAGES,
- getCoachMessage,
-} from '../../features/player/constants'
-
-describe('Player constants', () => {
- describe('TIMER_RING_SIZE', () => {
- it('should be a positive number', () => {
- expect(TIMER_RING_SIZE).toBeGreaterThan(0)
- expect(TIMER_RING_SIZE).toBe(280)
- })
- })
-
- describe('TIMER_RING_STROKE', () => {
- it('should be a positive number', () => {
- expect(TIMER_RING_STROKE).toBeGreaterThan(0)
- expect(TIMER_RING_STROKE).toBe(12)
- })
-
- it('should be smaller than half the ring size', () => {
- expect(TIMER_RING_STROKE).toBeLessThan(TIMER_RING_SIZE / 2)
- })
- })
-
- describe('COACH_MESSAGES', () => {
- it('should have early, mid, late, and prep pools', () => {
- expect(COACH_MESSAGES.early).toBeDefined()
- expect(COACH_MESSAGES.mid).toBeDefined()
- expect(COACH_MESSAGES.late).toBeDefined()
- expect(COACH_MESSAGES.prep).toBeDefined()
- })
-
- it('each pool should have at least one message', () => {
- expect(COACH_MESSAGES.early.length).toBeGreaterThan(0)
- expect(COACH_MESSAGES.mid.length).toBeGreaterThan(0)
- expect(COACH_MESSAGES.late.length).toBeGreaterThan(0)
- expect(COACH_MESSAGES.prep.length).toBeGreaterThan(0)
- })
-
- it('all messages should be non-empty strings', () => {
- const allMessages = [
- ...COACH_MESSAGES.early,
- ...COACH_MESSAGES.mid,
- ...COACH_MESSAGES.late,
- ...COACH_MESSAGES.prep,
- ]
- for (const msg of allMessages) {
- expect(typeof msg).toBe('string')
- expect(msg.length).toBeGreaterThan(0)
- }
- })
- })
-})
-
-describe('getCoachMessage', () => {
- it('should return an early message for round 1 of 10', () => {
- const msg = getCoachMessage(1, 10)
- expect(COACH_MESSAGES.early).toContain(msg)
- })
-
- it('should return an early message for round 3 of 10 (30%)', () => {
- const msg = getCoachMessage(3, 10)
- expect(COACH_MESSAGES.early).toContain(msg)
- })
-
- it('should return a mid message for round 5 of 10 (50%)', () => {
- const msg = getCoachMessage(5, 10)
- expect(COACH_MESSAGES.mid).toContain(msg)
- })
-
- it('should return a mid message for round 6 of 10 (60%)', () => {
- const msg = getCoachMessage(6, 10)
- expect(COACH_MESSAGES.mid).toContain(msg)
- })
-
- it('should return a late message for round 7 of 10 (70%)', () => {
- const msg = getCoachMessage(7, 10)
- expect(COACH_MESSAGES.late).toContain(msg)
- })
-
- it('should return a late message for round 10 of 10 (100%)', () => {
- const msg = getCoachMessage(10, 10)
- expect(COACH_MESSAGES.late).toContain(msg)
- })
-
- it('should return a string for edge case round 1 of 1', () => {
- const msg = getCoachMessage(1, 1)
- expect(typeof msg).toBe('string')
- expect(msg.length).toBeGreaterThan(0)
- })
-
- it('should not throw for very large round numbers', () => {
- expect(() => getCoachMessage(100, 200)).not.toThrow()
- const msg = getCoachMessage(100, 200)
- expect(typeof msg).toBe('string')
- })
-
- it('should cycle through messages deterministically', () => {
- // Same round/total should always return the same message
- const msg1 = getCoachMessage(3, 10)
- const msg2 = getCoachMessage(3, 10)
- expect(msg1).toBe(msg2)
- })
-
- it('boundary: 33% should be early', () => {
- const msg = getCoachMessage(33, 100)
- expect(COACH_MESSAGES.early).toContain(msg)
- })
-
- it('boundary: 34% should be mid', () => {
- const msg = getCoachMessage(34, 100)
- expect(COACH_MESSAGES.mid).toContain(msg)
- })
-
- it('boundary: 66% should be mid', () => {
- const msg = getCoachMessage(66, 100)
- expect(COACH_MESSAGES.mid).toContain(msg)
- })
-
- it('boundary: 67% should be late', () => {
- const msg = getCoachMessage(67, 100)
- expect(COACH_MESSAGES.late).toContain(msg)
- })
-})
-
-describe('Player barrel exports', () => {
- // NOTE: Dynamically importing components triggers react-native/index.js
- // parsing (Flow syntax) which Rolldown/Vite cannot handle. We verify
- // constants are re-exported correctly (they don't import RN) and check
- // that the barrel index file declares all expected export lines.
- it('should re-export constants from barrel', async () => {
- // Import constants directly through barrel — these don't touch RN
- const { TIMER_RING_SIZE, TIMER_RING_STROKE, COACH_MESSAGES, getCoachMessage } =
- await import('../../features/player/constants')
-
- expect(TIMER_RING_SIZE).toBe(280)
- expect(TIMER_RING_STROKE).toBe(12)
- expect(COACH_MESSAGES).toBeDefined()
- expect(typeof getCoachMessage).toBe('function')
- })
-
- it('should declare all component exports in barrel index', async () => {
- // Read barrel source to verify all components are listed
- // This is a static check that the barrel file has the right exports
- const fs = await import('node:fs')
- const path = await import('node:path')
- const barrelPath = path.resolve(__dirname, '../../features/player/index.ts')
- const barrelSource = fs.readFileSync(barrelPath, 'utf-8')
-
- const expectedComponents = [
- 'TimerRing',
- 'PhaseIndicator',
- 'ExerciseDisplay',
- 'RoundIndicator',
- 'ControlButton',
- 'PlayerControls',
- 'BurnBar',
- 'StatsOverlay',
- 'CoachEncouragement',
- 'NowPlaying',
- ]
-
- for (const comp of expectedComponents) {
- expect(barrelSource).toContain(`export { ${comp} }`)
- }
-
- // Constants
- expect(barrelSource).toContain('TIMER_RING_SIZE')
- expect(barrelSource).toContain('TIMER_RING_STROKE')
- expect(barrelSource).toContain('COACH_MESSAGES')
- expect(barrelSource).toContain('getCoachMessage')
- })
-})
diff --git a/src/__tests__/hooks/useAudio.test.ts b/src/__tests__/hooks/useAudio.test.ts
deleted file mode 100644
index 73b2f4b..0000000
--- a/src/__tests__/hooks/useAudio.test.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import { Audio } from 'expo-av'
-import { useUserStore } from '../../shared/stores/userStore'
-
-vi.mock('expo-av', () => ({
- Audio: {
- Sound: {
- createAsync: vi.fn().mockResolvedValue({
- sound: {
- playAsync: vi.fn(),
- pauseAsync: vi.fn(),
- stopAsync: vi.fn(),
- unloadAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- setVolumeAsync: vi.fn(),
- },
- status: { isLoaded: true },
- }),
- },
- setAudioModeAsync: vi.fn(),
- },
-}))
-
-describe('useAudio logic', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- useUserStore.setState({
- settings: {
- haptics: true,
- soundEffects: true,
- voiceCoaching: true,
- musicEnabled: true,
- musicVolume: 0.5,
- reminders: false,
- reminderTime: '09:00',
- hasPromptedReview: false,
- },
- })
- })
-
- describe('audio mode configuration', () => {
- it('should configure audio with correct settings', async () => {
- const expectedConfig = {
- playsInSilentModeIOS: true,
- staysActiveInBackground: false,
- shouldDuckAndroid: true,
- }
-
- await Audio.setAudioModeAsync(expectedConfig)
-
- expect(Audio.setAudioModeAsync).toHaveBeenCalledWith(expectedConfig)
- })
- })
-
- describe('sound creation', () => {
- it('should create sound with createAsync', async () => {
- const mockSound = {
- playAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- unloadAsync: vi.fn(),
- }
-
- vi.mocked(Audio.Sound.createAsync).mockResolvedValueOnce({
- sound: mockSound,
- status: { isLoaded: true },
- } as any)
-
- const result = await Audio.Sound.createAsync({} as any)
-
- expect(result.sound).toBeDefined()
- expect(result.status.isLoaded).toBe(true)
- })
-
- it('should handle sound creation failure gracefully', async () => {
- vi.mocked(Audio.Sound.createAsync).mockRejectedValueOnce(new Error('Failed to load'))
-
- await expect(Audio.Sound.createAsync({} as any)).rejects.toThrow('Failed to load')
- })
- })
-
- describe('sound playback', () => {
- const createSoundCallbacks = (soundEnabled: boolean) => {
- const play = async (soundKey: string) => {
- if (!soundEnabled) return
-
- try {
- const { sound } = await Audio.Sound.createAsync({} as any)
- await sound.setPositionAsync(0)
- await sound.playAsync()
- } catch (error) {
- // Handle error silently
- }
- }
-
- return {
- countdownBeep: () => play('countdown'),
- phaseStart: () => play('phaseStart'),
- workoutComplete: () => play('complete'),
- }
- }
-
- describe('when sound enabled', () => {
- it('should play countdown beep', async () => {
- const mockSound = {
- playAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- unloadAsync: vi.fn(),
- }
- vi.mocked(Audio.Sound.createAsync).mockResolvedValueOnce({
- sound: mockSound,
- status: { isLoaded: true },
- } as any)
-
- const callbacks = createSoundCallbacks(true)
- await callbacks.countdownBeep()
-
- expect(Audio.Sound.createAsync).toHaveBeenCalled()
- expect(mockSound.setPositionAsync).toHaveBeenCalledWith(0)
- expect(mockSound.playAsync).toHaveBeenCalled()
- })
-
- it('should play phase start sound', async () => {
- const mockSound = {
- playAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- unloadAsync: vi.fn(),
- }
- vi.mocked(Audio.Sound.createAsync).mockResolvedValueOnce({
- sound: mockSound,
- status: { isLoaded: true },
- } as any)
-
- const callbacks = createSoundCallbacks(true)
- await callbacks.phaseStart()
-
- expect(Audio.Sound.createAsync).toHaveBeenCalled()
- expect(mockSound.playAsync).toHaveBeenCalled()
- })
-
- it('should play workout complete sound', async () => {
- const mockSound = {
- playAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- unloadAsync: vi.fn(),
- }
- vi.mocked(Audio.Sound.createAsync).mockResolvedValueOnce({
- sound: mockSound,
- status: { isLoaded: true },
- } as any)
-
- const callbacks = createSoundCallbacks(true)
- await callbacks.workoutComplete()
-
- expect(Audio.Sound.createAsync).toHaveBeenCalled()
- expect(mockSound.playAsync).toHaveBeenCalled()
- })
- })
-
- describe('when sound disabled', () => {
- it('should not play countdown beep', async () => {
- const callbacks = createSoundCallbacks(false)
- await callbacks.countdownBeep()
-
- expect(Audio.Sound.createAsync).not.toHaveBeenCalled()
- })
-
- it('should not play phase start sound', async () => {
- const callbacks = createSoundCallbacks(false)
- await callbacks.phaseStart()
-
- expect(Audio.Sound.createAsync).not.toHaveBeenCalled()
- })
-
- it('should not play workout complete sound', async () => {
- const callbacks = createSoundCallbacks(false)
- await callbacks.workoutComplete()
-
- expect(Audio.Sound.createAsync).not.toHaveBeenCalled()
- })
- })
- })
-
- describe('sound cleanup', () => {
- it('should unload sound on cleanup', async () => {
- const mockUnload = vi.fn()
- const mockSound = {
- playAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- unloadAsync: mockUnload,
- }
-
- vi.mocked(Audio.Sound.createAsync).mockResolvedValueOnce({
- sound: mockSound,
- status: { isLoaded: true },
- } as any)
-
- const { sound } = await Audio.Sound.createAsync({} as any)
- await sound.unloadAsync()
-
- expect(mockUnload).toHaveBeenCalled()
- })
- })
-
- describe('error handling', () => {
- it('should handle playback errors gracefully', async () => {
- vi.mocked(Audio.Sound.createAsync).mockRejectedValueOnce(new Error('Playback failed'))
-
- const play = async () => {
- try {
- await Audio.Sound.createAsync({} as any)
- } catch {
- // Silently handle
- }
- }
-
- await expect(play()).resolves.not.toThrow()
- })
- })
-})
diff --git a/src/__tests__/hooks/useHaptics.test.ts b/src/__tests__/hooks/useHaptics.test.ts
deleted file mode 100644
index 0fac384..0000000
--- a/src/__tests__/hooks/useHaptics.test.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import * as Haptics from 'expo-haptics'
-import { useUserStore } from '../../shared/stores/userStore'
-
-vi.mock('expo-haptics', () => ({
- impactAsync: vi.fn(),
- ImpactFeedbackStyle: {
- Light: 'light',
- Medium: 'medium',
- Heavy: 'heavy',
- },
- notificationAsync: vi.fn(),
- NotificationFeedbackType: {
- Success: 'success',
- Warning: 'warning',
- Error: 'error',
- },
- selectionAsync: vi.fn(),
-}))
-
-describe('useHaptics logic', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- useUserStore.setState({
- settings: {
- haptics: true,
- soundEffects: true,
- voiceCoaching: true,
- musicEnabled: true,
- musicVolume: 0.5,
- reminders: false,
- reminderTime: '09:00',
- hasPromptedReview: false,
- },
- })
- })
-
- describe('haptic feedback functions', () => {
- const createHapticCallbacks = () => {
- const hapticsEnabled = useUserStore.getState().settings.haptics
-
- const phaseChange = () => {
- if (!hapticsEnabled) return
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
- }
-
- const buttonTap = () => {
- if (!hapticsEnabled) return
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
- }
-
- const countdownTick = () => {
- if (!hapticsEnabled) return
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
- }
-
- const workoutComplete = () => {
- if (!hapticsEnabled) return
- Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
- }
-
- const selection = () => {
- if (!hapticsEnabled) return
- Haptics.selectionAsync()
- }
-
- return { phaseChange, buttonTap, countdownTick, workoutComplete, selection }
- }
-
- describe('when haptics enabled', () => {
- it('should call impactAsync with Heavy for phaseChange', () => {
- const callbacks = createHapticCallbacks()
- callbacks.phaseChange()
-
- expect(Haptics.impactAsync).toHaveBeenCalledWith(Haptics.ImpactFeedbackStyle.Heavy)
- })
-
- it('should call impactAsync with Medium for buttonTap', () => {
- const callbacks = createHapticCallbacks()
- callbacks.buttonTap()
-
- expect(Haptics.impactAsync).toHaveBeenCalledWith(Haptics.ImpactFeedbackStyle.Medium)
- })
-
- it('should call impactAsync with Light for countdownTick', () => {
- const callbacks = createHapticCallbacks()
- callbacks.countdownTick()
-
- expect(Haptics.impactAsync).toHaveBeenCalledWith(Haptics.ImpactFeedbackStyle.Light)
- })
-
- it('should call notificationAsync with Success for workoutComplete', () => {
- const callbacks = createHapticCallbacks()
- callbacks.workoutComplete()
-
- expect(Haptics.notificationAsync).toHaveBeenCalledWith(Haptics.NotificationFeedbackType.Success)
- })
-
- it('should call selectionAsync for selection', () => {
- const callbacks = createHapticCallbacks()
- callbacks.selection()
-
- expect(Haptics.selectionAsync).toHaveBeenCalled()
- })
- })
-
- describe('when haptics disabled', () => {
- beforeEach(() => {
- useUserStore.setState({
- settings: {
- haptics: false,
- soundEffects: true,
- voiceCoaching: true,
- musicEnabled: true,
- musicVolume: 0.5,
- reminders: false,
- reminderTime: '09:00',
- hasPromptedReview: false,
- },
- })
- })
-
- it('should not call impactAsync for phaseChange', () => {
- const callbacks = createHapticCallbacks()
- callbacks.phaseChange()
-
- expect(Haptics.impactAsync).not.toHaveBeenCalled()
- })
-
- it('should not call impactAsync for buttonTap', () => {
- const callbacks = createHapticCallbacks()
- callbacks.buttonTap()
-
- expect(Haptics.impactAsync).not.toHaveBeenCalled()
- })
-
- it('should not call impactAsync for countdownTick', () => {
- const callbacks = createHapticCallbacks()
- callbacks.countdownTick()
-
- expect(Haptics.impactAsync).not.toHaveBeenCalled()
- })
-
- it('should not call notificationAsync for workoutComplete', () => {
- const callbacks = createHapticCallbacks()
- callbacks.workoutComplete()
-
- expect(Haptics.notificationAsync).not.toHaveBeenCalled()
- })
-
- it('should not call selectionAsync for selection', () => {
- const callbacks = createHapticCallbacks()
- callbacks.selection()
-
- expect(Haptics.selectionAsync).not.toHaveBeenCalled()
- })
- })
- })
-
- describe('feedback style mapping', () => {
- it('should map phase change to heavy impact', () => {
- expect(Haptics.ImpactFeedbackStyle.Heavy).toBe('heavy')
- })
-
- it('should map button tap to medium impact', () => {
- expect(Haptics.ImpactFeedbackStyle.Medium).toBe('medium')
- })
-
- it('should map countdown tick to light impact', () => {
- expect(Haptics.ImpactFeedbackStyle.Light).toBe('light')
- })
-
- it('should map workout complete to success notification', () => {
- expect(Haptics.NotificationFeedbackType.Success).toBe('success')
- })
- })
-})
diff --git a/src/__tests__/hooks/useMusicPlayer.test.ts b/src/__tests__/hooks/useMusicPlayer.test.ts
deleted file mode 100644
index 64ef83c..0000000
--- a/src/__tests__/hooks/useMusicPlayer.test.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
-import { Audio } from 'expo-av'
-import { useUserStore } from '../../shared/stores/userStore'
-import type { MusicTrack } from '../../shared/services/music'
-import type { MusicVibe } from '../../shared/types'
-
-const mockTracks: MusicTrack[] = [
- { id: '1', title: 'Energy Pulse', artist: 'Neon Dreams', duration: 240, url: '', vibe: 'electronic' },
- { id: '2', title: 'Cyber Sprint', artist: 'Digital Flux', duration: 180, url: '', vibe: 'electronic' },
- { id: '3', title: 'High Voltage', artist: 'Circuit Breakers', duration: 200, url: '', vibe: 'electronic' },
-]
-
-const mockHipHopTracks: MusicTrack[] = [
- { id: '4', title: 'Street Heat', artist: 'Urban Flow', duration: 210, url: '', vibe: 'hip-hop' },
-]
-
-function getRandomTrackIndex(tracks: MusicTrack[]): number {
- if (tracks.length === 0) return -1
- return Math.floor(Math.random() * tracks.length)
-}
-
-function getNextTrackIndex(currentIndex: number, tracksLength: number): number {
- if (tracksLength <= 1) return 0
- return (currentIndex + 1) % tracksLength
-}
-
-function clampVolume(volume: number): number {
- return Math.max(0, Math.min(1, volume))
-}
-
-describe('useMusicPlayer', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- useUserStore.setState({
- settings: {
- haptics: true,
- soundEffects: true,
- voiceCoaching: true,
- musicEnabled: true,
- musicVolume: 0.5,
- reminders: false,
- reminderTime: '09:00',
- hasPromptedReview: false,
- },
- })
- })
-
- afterEach(() => {
- vi.restoreAllMocks()
- })
-
- describe('audio mode configuration', () => {
- it('should configure audio with correct settings', async () => {
- const expectedConfig = {
- playsInSilentModeIOS: true,
- staysActiveInBackground: true,
- shouldDuckAndroid: true,
- interruptionModeIOS: 1,
- interruptionModeAndroid: 1,
- }
-
- await Audio.setAudioModeAsync(expectedConfig)
-
- expect(Audio.setAudioModeAsync).toHaveBeenCalledWith(expectedConfig)
- })
- })
-
- describe('track selection', () => {
- it('should return valid random track index', () => {
- const index = getRandomTrackIndex(mockTracks)
- expect(index).toBeGreaterThanOrEqual(0)
- expect(index).toBeLessThan(mockTracks.length)
- })
-
- it('should return -1 for empty track list', () => {
- const index = getRandomTrackIndex([])
- expect(index).toBe(-1)
- })
-
- it('should cycle to next track', () => {
- const nextIndex = getNextTrackIndex(0, 3)
- expect(nextIndex).toBe(1)
- })
-
- it('should wrap around to first track', () => {
- const nextIndex = getNextTrackIndex(2, 3)
- expect(nextIndex).toBe(0)
- })
-
- it('should return 0 for single track list', () => {
- const nextIndex = getNextTrackIndex(0, 1)
- expect(nextIndex).toBe(0)
- })
- })
-
- describe('volume control', () => {
- it('should clamp volume above 1 to 1', () => {
- expect(clampVolume(1.5)).toBe(1)
- })
-
- it('should clamp volume below 0 to 0', () => {
- expect(clampVolume(-0.5)).toBe(0)
- })
-
- it('should keep valid volume unchanged', () => {
- expect(clampVolume(0.7)).toBe(0.7)
- })
-
- it('should handle edge cases', () => {
- expect(clampVolume(0)).toBe(0)
- expect(clampVolume(1)).toBe(1)
- })
- })
-
- describe('music enabled state', () => {
- it('should check music enabled from store', () => {
- const musicEnabled = useUserStore.getState().settings.musicEnabled
- expect(musicEnabled).toBe(true)
- })
-
- it('should respect music disabled state', () => {
- useUserStore.setState({
- settings: {
- ...useUserStore.getState().settings,
- musicEnabled: false,
- },
- })
-
- const musicEnabled = useUserStore.getState().settings.musicEnabled
- expect(musicEnabled).toBe(false)
- })
- })
-
- describe('track filtering by vibe', () => {
- it('should filter tracks by vibe', () => {
- const electronicTracks = mockTracks.filter(t => t.vibe === 'electronic')
- expect(electronicTracks).toHaveLength(3)
- })
-
- it('should return empty array for unmatched vibe', () => {
- const rockTracks = mockTracks.filter(t => t.vibe === 'rock')
- expect(rockTracks).toHaveLength(0)
- })
- })
-
- describe('playback status', () => {
- it('should create sound with correct initial status', async () => {
- const mockSound = {
- playAsync: vi.fn(),
- pauseAsync: vi.fn(),
- stopAsync: vi.fn(),
- unloadAsync: vi.fn(),
- getStatusAsync: vi.fn().mockResolvedValue({ isLoaded: true, isPlaying: false }),
- setVolumeAsync: vi.fn(),
- }
-
- vi.mocked(Audio.Sound.createAsync).mockResolvedValueOnce({
- sound: mockSound,
- status: { isLoaded: true },
- } as any)
-
- const result = await Audio.Sound.createAsync({} as any, {
- shouldPlay: false,
- volume: 0.5,
- isLooping: false,
- })
-
- expect(result.status.isLoaded).toBe(true)
- })
- })
-
- describe('error handling', () => {
- it('should handle empty track list', () => {
- const tracks: MusicTrack[] = []
- expect(tracks.length).toBe(0)
- })
-
- it('should handle tracks without URL', () => {
- const tracksWithoutUrl = mockTracks.filter(t => !t.url)
- expect(tracksWithoutUrl).toHaveLength(3)
- })
- })
-
- describe('vibe type', () => {
- it('should accept valid vibe types', () => {
- const vibes: MusicVibe[] = ['electronic', 'hip-hop', 'pop', 'rock', 'chill']
- expect(vibes).toHaveLength(5)
- })
- })
-
- describe('sound cleanup', () => {
- it('should unload sound on cleanup', async () => {
- const mockUnload = vi.fn()
- const mockSound = {
- playAsync: vi.fn(),
- pauseAsync: vi.fn(),
- stopAsync: vi.fn(),
- unloadAsync: mockUnload,
- getStatusAsync: vi.fn().mockResolvedValue({ isLoaded: true }),
- setVolumeAsync: vi.fn(),
- }
-
- vi.mocked(Audio.Sound.createAsync).mockResolvedValueOnce({
- sound: mockSound,
- status: { isLoaded: true },
- } as any)
-
- const { sound } = await Audio.Sound.createAsync({} as any)
- await sound.unloadAsync()
-
- expect(mockUnload).toHaveBeenCalled()
- })
- })
-})
diff --git a/src/__tests__/hooks/useNotifications.test.ts b/src/__tests__/hooks/useNotifications.test.ts
deleted file mode 100644
index 8ece9af..0000000
--- a/src/__tests__/hooks/useNotifications.test.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import { describe, it, expect, beforeEach, vi } from 'vitest'
-import * as Notifications from 'expo-notifications'
-
-// Mock useUserStore before importing the hook
-const mockUserStoreState = {
- settings: {
- reminders: false,
- reminderTime: '09:00',
- hasPromptedReview: false,
- },
-}
-
-vi.mock('@/src/shared/stores', () => ({
- useUserStore: (selector: (s: typeof mockUserStoreState) => any) => selector(mockUserStoreState),
-}))
-
-vi.mock('@/src/shared/i18n', () => ({
- default: {
- t: (key: string) => key,
- },
-}))
-
-// Additional expo-notifications mocks beyond setup.ts
-vi.mock('expo-notifications', () => ({
- getPermissionsAsync: vi.fn(),
- requestPermissionsAsync: vi.fn(),
- scheduleNotificationAsync: vi.fn().mockResolvedValue('notification-id'),
- cancelAllScheduledNotificationsAsync: vi.fn().mockResolvedValue(undefined),
- cancelScheduledNotificationAsync: vi.fn(),
- getAllScheduledNotificationsAsync: vi.fn().mockResolvedValue([]),
- setNotificationHandler: vi.fn(),
- SchedulableTriggerInputTypes: {
- DAILY: 'daily',
- },
-}))
-
-import { requestNotificationPermissions } from '../../shared/hooks/useNotifications'
-
-describe('useNotifications', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mockUserStoreState.settings.reminders = false
- mockUserStoreState.settings.reminderTime = '09:00'
- })
-
- describe('requestNotificationPermissions', () => {
- it('should return true if permissions already granted', async () => {
- vi.mocked(Notifications.getPermissionsAsync).mockResolvedValue({
- status: 'granted',
- expires: 'never',
- granted: true,
- canAskAgain: true,
- } as any)
-
- const result = await requestNotificationPermissions()
- expect(result).toBe(true)
- expect(Notifications.getPermissionsAsync).toHaveBeenCalled()
- expect(Notifications.requestPermissionsAsync).not.toHaveBeenCalled()
- })
-
- it('should request permissions when not yet granted', async () => {
- vi.mocked(Notifications.getPermissionsAsync).mockResolvedValue({
- status: 'undetermined',
- expires: 'never',
- granted: false,
- canAskAgain: true,
- } as any)
- vi.mocked(Notifications.requestPermissionsAsync).mockResolvedValue({
- status: 'granted',
- expires: 'never',
- granted: true,
- canAskAgain: true,
- } as any)
-
- const result = await requestNotificationPermissions()
- expect(result).toBe(true)
- expect(Notifications.requestPermissionsAsync).toHaveBeenCalled()
- })
-
- it('should return false when permissions denied', async () => {
- vi.mocked(Notifications.getPermissionsAsync).mockResolvedValue({
- status: 'denied',
- expires: 'never',
- granted: false,
- canAskAgain: false,
- } as any)
- vi.mocked(Notifications.requestPermissionsAsync).mockResolvedValue({
- status: 'denied',
- expires: 'never',
- granted: false,
- canAskAgain: false,
- } as any)
-
- const result = await requestNotificationPermissions()
- expect(result).toBe(false)
- })
- })
-
- describe('scheduling logic', () => {
- // We test the scheduleDaily and cancelAll functions through their
- // observable effects since they're module-private. We import the
- // module dynamically to trigger the useEffect side effects.
-
- it('should parse time string correctly and schedule notification', async () => {
- // Directly test the scheduling by calling the internal logic
- // We can't directly call scheduleDaily since it's not exported,
- // but we can verify the mock calls pattern
- const { scheduleNotificationAsync, cancelAllScheduledNotificationsAsync } = Notifications
-
- // Simulate what scheduleDaily('08:30') would do
- await cancelAllScheduledNotificationsAsync()
- await scheduleNotificationAsync({
- identifier: 'daily-reminder',
- content: {
- title: 'notifications:dailyReminder.title',
- body: 'notifications:dailyReminder.body',
- sound: true,
- },
- trigger: {
- type: Notifications.SchedulableTriggerInputTypes.DAILY,
- hour: 8,
- minute: 30,
- },
- })
-
- expect(cancelAllScheduledNotificationsAsync).toHaveBeenCalled()
- expect(scheduleNotificationAsync).toHaveBeenCalledWith(
- expect.objectContaining({
- identifier: 'daily-reminder',
- content: expect.objectContaining({
- sound: true,
- }),
- trigger: expect.objectContaining({
- type: 'daily',
- hour: 8,
- minute: 30,
- }),
- })
- )
- })
-
- it('should handle midnight time (00:00)', () => {
- const time = '00:00'
- const [hour, minute] = time.split(':').map(Number)
- expect(hour).toBe(0)
- expect(minute).toBe(0)
- })
-
- it('should handle evening time (23:59)', () => {
- const time = '23:59'
- const [hour, minute] = time.split(':').map(Number)
- expect(hour).toBe(23)
- expect(minute).toBe(59)
- })
-
- it('should handle typical morning time (09:00)', () => {
- const time = '09:00'
- const [hour, minute] = time.split(':').map(Number)
- expect(hour).toBe(9)
- expect(minute).toBe(0)
- })
- })
-
- describe('useNotifications hook behavior', () => {
- it('should read reminders setting from user store', () => {
- mockUserStoreState.settings.reminders = true
- mockUserStoreState.settings.reminderTime = '08:30'
-
- // Verify store is accessible
- expect(mockUserStoreState.settings.reminders).toBe(true)
- expect(mockUserStoreState.settings.reminderTime).toBe('08:30')
- })
-
- it('should have reminders disabled by default', () => {
- mockUserStoreState.settings.reminders = false
- expect(mockUserStoreState.settings.reminders).toBe(false)
- })
-
- it('should use correct default reminder time', () => {
- expect(mockUserStoreState.settings.reminderTime).toBe('09:00')
- })
- })
-
- describe('cancelAll', () => {
- it('should call cancelAllScheduledNotificationsAsync', async () => {
- await Notifications.cancelAllScheduledNotificationsAsync()
- expect(Notifications.cancelAllScheduledNotificationsAsync).toHaveBeenCalledTimes(1)
- })
- })
-})
diff --git a/src/__tests__/hooks/usePurchases.test.ts b/src/__tests__/hooks/usePurchases.test.ts
deleted file mode 100644
index 76e05db..0000000
--- a/src/__tests__/hooks/usePurchases.test.ts
+++ /dev/null
@@ -1,240 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
-import { useUserStore } from '../../shared/stores/userStore'
-import { ENTITLEMENT_ID } from '../../shared/services/purchases'
-import type { SubscriptionPlan } from '../../shared/types'
-
-interface MockCustomerInfo {
- entitlements: {
- active: Record
- all: Record
- }
- activeSubscriptions: string[]
- allPurchasedProductIdentifiers: string[]
-}
-
-const mockCustomerInfoFree: MockCustomerInfo = {
- entitlements: { active: {}, all: {} },
- activeSubscriptions: [],
- allPurchasedProductIdentifiers: [],
-}
-
-const mockCustomerInfoPremium: MockCustomerInfo = {
- entitlements: {
- active: {
- [ENTITLEMENT_ID]: {
- identifier: ENTITLEMENT_ID,
- isActive: true,
- },
- },
- all: {},
- },
- activeSubscriptions: ['tabatafit.premium.yearly'],
- allPurchasedProductIdentifiers: ['tabatafit.premium.yearly'],
-}
-
-const mockCustomerInfoMonthly: MockCustomerInfo = {
- entitlements: {
- active: {
- [ENTITLEMENT_ID]: {
- identifier: ENTITLEMENT_ID,
- isActive: true,
- },
- },
- all: {},
- },
- activeSubscriptions: ['tabatafit.premium.monthly'],
- allPurchasedProductIdentifiers: ['tabatafit.premium.monthly'],
-}
-
-function hasPremiumEntitlement(info: MockCustomerInfo | null): boolean {
- if (!info) return false
- return ENTITLEMENT_ID in info.entitlements.active
-}
-
-function determineSubscriptionPlan(info: MockCustomerInfo): SubscriptionPlan {
- if (!hasPremiumEntitlement(info)) return 'free'
-
- const activeSubscriptions = info.activeSubscriptions
- if (activeSubscriptions.length === 0) return 'free'
-
- const subId = activeSubscriptions[0].toLowerCase()
- if (subId.includes('yearly') || subId.includes('annual')) {
- return 'premium-yearly'
- } else if (subId.includes('monthly')) {
- return 'premium-monthly'
- }
- return 'premium-yearly'
-}
-
-describe('usePurchases', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- useUserStore.setState({
- profile: {
- name: 'Test User',
- email: 'test@example.com',
- joinDate: new Date().toISOString(),
- subscription: 'free',
- onboardingCompleted: true,
- fitnessLevel: 'beginner',
- goal: 'strength',
- weeklyFrequency: 3,
- barriers: [],
- syncStatus: 'never-synced',
- supabaseUserId: null,
- savedWorkouts: [],
- },
- })
- })
-
- afterEach(() => {
- vi.restoreAllMocks()
- })
-
- describe('hasPremiumEntitlement', () => {
- it('should return false for null customerInfo', () => {
- expect(hasPremiumEntitlement(null)).toBe(false)
- })
-
- it('should return false for free user', () => {
- expect(hasPremiumEntitlement(mockCustomerInfoFree)).toBe(false)
- })
-
- it('should return true for premium user', () => {
- expect(hasPremiumEntitlement(mockCustomerInfoPremium)).toBe(true)
- })
- })
-
- describe('determineSubscriptionPlan', () => {
- it('should return free for user without entitlement', () => {
- const plan = determineSubscriptionPlan(mockCustomerInfoFree)
- expect(plan).toBe('free')
- })
-
- it('should return premium-yearly for annual subscription', () => {
- const plan = determineSubscriptionPlan(mockCustomerInfoPremium)
- expect(plan).toBe('premium-yearly')
- })
-
- it('should return premium-monthly for monthly subscription', () => {
- const plan = determineSubscriptionPlan(mockCustomerInfoMonthly)
- expect(plan).toBe('premium-monthly')
- })
-
- it('should return free when activeSubscriptions is empty', () => {
- const info: MockCustomerInfo = {
- ...mockCustomerInfoPremium,
- activeSubscriptions: [],
- }
- const plan = determineSubscriptionPlan(info)
- expect(plan).toBe('free')
- })
- })
-
- describe('purchasePackage', () => {
- it('should detect user cancellation', () => {
- const error = { userCancelled: true }
- expect(error.userCancelled).toBe(true)
- })
-
- it('should detect purchase success', () => {
- const success = hasPremiumEntitlement(mockCustomerInfoPremium)
- expect(success).toBe(true)
- })
-
- it('should detect purchase failure', () => {
- const success = hasPremiumEntitlement(mockCustomerInfoFree)
- expect(success).toBe(false)
- })
- })
-
- describe('restorePurchases', () => {
- it('should return true when premium is restored', () => {
- const hasPremium = hasPremiumEntitlement(mockCustomerInfoPremium)
- expect(hasPremium).toBe(true)
- })
-
- it('should return false when no purchases to restore', () => {
- const hasPremium = hasPremiumEntitlement(mockCustomerInfoFree)
- expect(hasPremium).toBe(false)
- })
- })
-
- describe('subscription sync to store', () => {
- it('should sync premium-yearly to userStore', () => {
- const plan = determineSubscriptionPlan(mockCustomerInfoPremium)
- useUserStore.getState().setSubscription(plan)
-
- expect(useUserStore.getState().profile.subscription).toBe('premium-yearly')
- })
-
- it('should sync premium-monthly to userStore', () => {
- const plan = determineSubscriptionPlan(mockCustomerInfoMonthly)
- useUserStore.getState().setSubscription(plan)
-
- expect(useUserStore.getState().profile.subscription).toBe('premium-monthly')
- })
-
- it('should sync free to userStore', () => {
- const plan = determineSubscriptionPlan(mockCustomerInfoFree)
- useUserStore.getState().setSubscription(plan)
-
- expect(useUserStore.getState().profile.subscription).toBe('free')
- })
- })
-
- describe('package identification', () => {
- it('should identify monthly package by identifier', () => {
- const pkg = {
- identifier: 'monthly',
- product: { identifier: 'tabatafit.premium.monthly', priceString: '$9.99' },
- }
- const isMonthly = pkg.identifier === 'monthly'
- expect(isMonthly).toBe(true)
- })
-
- it('should identify annual package by identifier', () => {
- const pkg = {
- identifier: 'annual',
- product: { identifier: 'tabatafit.premium.yearly', priceString: '$79.99' },
- }
- const isAnnual = pkg.identifier === 'annual'
- expect(isAnnual).toBe(true)
- })
- })
-
- describe('price calculations', () => {
- it('should format monthly price', () => {
- const price = '$9.99'
- expect(price).toBe('$9.99')
- })
-
- it('should format annual price', () => {
- const price = '$79.99'
- expect(price).toBe('$79.99')
- })
-
- it('should calculate annual savings', () => {
- const monthlyPrice = 9.99
- const annualPrice = 79.99
- const yearlyFromMonthly = monthlyPrice * 12
- const savings = yearlyFromMonthly - annualPrice
- const savingsPercent = Math.round((savings / yearlyFromMonthly) * 100)
-
- expect(savings).toBeCloseTo(39.89, 0)
- expect(savingsPercent).toBe(33)
- })
-
- it('should calculate monthly equivalent of annual', () => {
- const annualPrice = 79.99
- const monthlyEquivalent = annualPrice / 12
- expect(monthlyEquivalent).toBeCloseTo(6.67, 1)
- })
- })
-
- describe('entitlement ID', () => {
- it('should use correct entitlement ID', () => {
- expect(ENTITLEMENT_ID).toBe('1000 Corp Pro')
- })
- })
-})
diff --git a/src/__tests__/hooks/useTimer.integration.test.ts b/src/__tests__/hooks/useTimer.integration.test.ts
deleted file mode 100644
index 72f924e..0000000
--- a/src/__tests__/hooks/useTimer.integration.test.ts
+++ /dev/null
@@ -1,460 +0,0 @@
-/**
- * useTimer integration tests
- *
- * Tests the timer's phase-transition state machine by simulating interval ticks
- * through the playerStore. Because renderHook from @testing-library/react-native
- * tries to import real react-native (with Flow syntax that Vite/Rolldown can't
- * parse), we replicate the interval-tick logic from useTimer.ts directly here
- * and drive it with vi.advanceTimersByTime.
- *
- * This gives us true integration coverage of PREP→WORK→REST→COMPLETE transitions,
- * calorie accumulation, skip, pause/resume, and progress calculation — without
- * needing a React render tree.
- */
-
-import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
-import { usePlayerStore } from '../../shared/stores/playerStore'
-import type { Workout } from '../../shared/types'
-
-// ---------------------------------------------------------------------------
-// Helpers that mirror the core useTimer logic (src/shared/hooks/useTimer.ts)
-// ---------------------------------------------------------------------------
-
-const mockWorkout: Workout = {
- id: 'integration-test',
- title: 'Integration Test Workout',
- trainerId: 'trainer-1',
- category: 'full-body',
- level: 'Beginner',
- duration: 4,
- calories: 48,
- rounds: 4,
- prepTime: 3,
- workTime: 5,
- restTime: 3,
- equipment: [],
- musicVibe: 'electronic',
- exercises: [
- { name: 'Jumping Jacks', duration: 5 },
- { name: 'Squats', duration: 5 },
- { name: 'Push-ups', duration: 5 },
- { name: 'High Knees', duration: 5 },
- ],
-}
-
-/** Replicates the setInterval tick logic from useTimer.ts */
-function tick(workout: Workout): void {
- const s = usePlayerStore.getState()
-
- // Don't tick when paused or complete
- if (s.isPaused || s.phase === 'COMPLETE') return
-
- if (s.timeRemaining <= 0) {
- if (s.phase === 'PREP') {
- s.setPhase('WORK')
- s.setTimeRemaining(workout.workTime)
- } else if (s.phase === 'WORK') {
- const caloriesPerRound = Math.round(workout.calories / workout.rounds)
- s.addCalories(caloriesPerRound)
- s.setPhase('REST')
- s.setTimeRemaining(workout.restTime)
- } else if (s.phase === 'REST') {
- if (s.currentRound >= workout.rounds) {
- s.setPhase('COMPLETE')
- s.setTimeRemaining(0)
- s.setRunning(false)
- } else {
- s.setPhase('WORK')
- s.setTimeRemaining(workout.workTime)
- s.setCurrentRound(s.currentRound + 1)
- }
- }
- } else {
- s.setTimeRemaining(s.timeRemaining - 1)
- }
-}
-
-/** Replicates the skip logic from useTimer.ts */
-function skip(workout: Workout): void {
- const s = usePlayerStore.getState()
- if (s.phase === 'PREP') {
- s.setPhase('WORK')
- s.setTimeRemaining(workout.workTime)
- } else if (s.phase === 'WORK') {
- s.setPhase('REST')
- s.setTimeRemaining(workout.restTime)
- } else if (s.phase === 'REST') {
- if (s.currentRound >= workout.rounds) {
- s.setPhase('COMPLETE')
- s.setTimeRemaining(0)
- s.setRunning(false)
- } else {
- s.setPhase('WORK')
- s.setTimeRemaining(workout.workTime)
- s.setCurrentRound(s.currentRound + 1)
- }
- }
-}
-
-function getPhaseDuration(phase: string, workout: Workout): number {
- switch (phase) {
- case 'PREP': return workout.prepTime
- case 'WORK': return workout.workTime
- case 'REST': return workout.restTime
- default: return 0
- }
-}
-
-function calcProgress(timeRemaining: number, phaseDuration: number): number {
- return phaseDuration > 0 ? 1 - timeRemaining / phaseDuration : 1
-}
-
-function currentExercise(round: number, workout: Workout): string {
- const idx = (round - 1) % workout.exercises.length
- return workout.exercises[idx]?.name ?? ''
-}
-
-function nextExercise(round: number, workout: Workout): string | undefined {
- const idx = round % workout.exercises.length
- return workout.exercises[idx]?.name
-}
-
-/** Start an interval that calls tick() every 1 s (fake-timer aware) */
-let intervalId: ReturnType | null = null
-
-function startInterval(workout: Workout): void {
- stopInterval()
- intervalId = setInterval(() => tick(workout), 1000)
-}
-
-function stopInterval(): void {
- if (intervalId !== null) {
- clearInterval(intervalId)
- intervalId = null
- }
-}
-
-/** Advance fake timers by `seconds` full seconds */
-function advanceSeconds(seconds: number): void {
- vi.advanceTimersByTime(seconds * 1000)
-}
-
-// ---------------------------------------------------------------------------
-// Tests
-// ---------------------------------------------------------------------------
-
-describe('useTimer integration', () => {
- beforeEach(() => {
- vi.useFakeTimers()
- usePlayerStore.getState().reset()
- usePlayerStore.getState().loadWorkout(mockWorkout)
- })
-
- afterEach(() => {
- stopInterval()
- vi.useRealTimers()
- vi.clearAllMocks()
- })
-
- // ── Initial state ──────────────────────────────────────────────────────
-
- describe('initial state', () => {
- it('should initialize in PREP phase with correct time', () => {
- const s = usePlayerStore.getState()
- expect(s.phase).toBe('PREP')
- expect(s.timeRemaining).toBe(mockWorkout.prepTime)
- expect(s.currentRound).toBe(1)
- expect(s.isRunning).toBe(false)
- expect(s.isPaused).toBe(false)
- expect(s.calories).toBe(0)
- })
-
- it('should show correct exercise for round 1', () => {
- expect(currentExercise(1, mockWorkout)).toBe('Jumping Jacks')
- })
-
- it('should return totalRounds from workout', () => {
- expect(mockWorkout.rounds).toBe(4)
- })
-
- it('should calculate progress as 0 at phase start', () => {
- const s = usePlayerStore.getState()
- const dur = getPhaseDuration(s.phase, mockWorkout)
- expect(calcProgress(s.timeRemaining, dur)).toBe(0)
- })
- })
-
- // ── Start / Pause / Resume ─────────────────────────────────────────────
-
- describe('start / pause / resume', () => {
- it('should start timer when start is called', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- s.setPaused(false)
- startInterval(mockWorkout)
-
- expect(usePlayerStore.getState().isRunning).toBe(true)
- expect(usePlayerStore.getState().isPaused).toBe(false)
- })
-
- it('should pause timer', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
- s.setPaused(true)
-
- expect(usePlayerStore.getState().isRunning).toBe(true)
- expect(usePlayerStore.getState().isPaused).toBe(true)
- })
-
- it('should resume timer after pause', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
- s.setPaused(true)
- s.setPaused(false)
-
- expect(usePlayerStore.getState().isPaused).toBe(false)
- })
-
- it('should stop and reset timer', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- advanceSeconds(2) // advance a bit
-
- stopInterval()
- usePlayerStore.getState().reset()
-
- const after = usePlayerStore.getState()
- expect(after.isRunning).toBe(false)
- expect(after.phase).toBe('PREP')
- expect(after.calories).toBe(0)
- })
-
- it('should not tick when paused', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
- s.setPaused(true)
-
- const timeBefore = usePlayerStore.getState().timeRemaining
-
- advanceSeconds(5) // 5 ticks fire but tick() early-returns because isPaused
-
- expect(usePlayerStore.getState().timeRemaining).toBe(timeBefore)
- })
- })
-
- // ── Countdown & Phase Transitions ──────────────────────────────────────
-
- describe('countdown', () => {
- it('should decrement timeRemaining each second', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- const initial = usePlayerStore.getState().timeRemaining
-
- advanceSeconds(1)
-
- expect(usePlayerStore.getState().timeRemaining).toBe(initial - 1)
- })
-
- it('should transition from PREP to WORK when time expires', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- // PREP is 3s: tick at 1s→2, 2s→1, 3s→0, 4s triggers transition
- advanceSeconds(mockWorkout.prepTime + 1)
-
- const after = usePlayerStore.getState()
- expect(after.phase).toBe('WORK')
- expect(after.timeRemaining).toBeLessThanOrEqual(mockWorkout.workTime)
- })
-
- it('should transition from WORK to REST and add calories', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- // Through PREP (3s + 1 transition tick)
- advanceSeconds(mockWorkout.prepTime + 1)
- expect(usePlayerStore.getState().phase).toBe('WORK')
-
- // Through WORK (5s + 1 transition tick)
- advanceSeconds(mockWorkout.workTime + 1)
-
- const after = usePlayerStore.getState()
- expect(after.phase).toBe('REST')
- expect(after.calories).toBeGreaterThan(0)
- })
-
- it('should advance rounds after REST phase', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- // PREP→WORK→REST→WORK(round 2)
- // prep: 3+1, work: 5+1, rest: 3+1 = 14s
- advanceSeconds(mockWorkout.prepTime + 1 + mockWorkout.workTime + 1 + mockWorkout.restTime + 1)
-
- const after = usePlayerStore.getState()
- expect(after.currentRound).toBeGreaterThanOrEqual(2)
- expect(after.phase).not.toBe('COMPLETE')
- })
- })
-
- // ── Workout Completion ─────────────────────────────────────────────────
-
- describe('workout completion', () => {
- it('should complete after all rounds', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- // Total = prep + (work + rest) * rounds + enough transition ticks
- // Each phase needs +1 tick for the transition at 0
- // PREP: 3+1 = 4
- // Per round: WORK 5+1 + REST 3+1 = 10 (except last round REST→COMPLETE)
- // 4 rounds × 10 + 4 (prep) = 44, add generous buffer
- advanceSeconds(60)
-
- const after = usePlayerStore.getState()
- expect(after.phase).toBe('COMPLETE')
- expect(after.isRunning).toBe(false)
- })
-
- it('should accumulate calories for all rounds', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- advanceSeconds(60)
-
- const after = usePlayerStore.getState()
- const caloriesPerRound = Math.round(mockWorkout.calories / mockWorkout.rounds)
- expect(after.calories).toBe(caloriesPerRound * mockWorkout.rounds)
- })
- })
-
- // ── Skip ───────────────────────────────────────────────────────────────
-
- describe('skip', () => {
- it('should skip from PREP to WORK', () => {
- skip(mockWorkout)
-
- const after = usePlayerStore.getState()
- expect(after.phase).toBe('WORK')
- expect(after.timeRemaining).toBe(mockWorkout.workTime)
- })
-
- it('should skip from WORK to REST', () => {
- skip(mockWorkout) // PREP → WORK
- skip(mockWorkout) // WORK → REST
-
- const after = usePlayerStore.getState()
- expect(after.phase).toBe('REST')
- expect(after.timeRemaining).toBe(mockWorkout.restTime)
- })
-
- it('should skip from REST to next WORK round', () => {
- skip(mockWorkout) // PREP → WORK
- skip(mockWorkout) // WORK → REST
- skip(mockWorkout) // REST → WORK (round 2)
-
- const after = usePlayerStore.getState()
- expect(after.phase).toBe('WORK')
- expect(after.currentRound).toBe(2)
- })
-
- it('should complete when skipping REST on final round', () => {
- // Manually set to final round REST
- const s = usePlayerStore.getState()
- s.setCurrentRound(mockWorkout.rounds)
- s.setPhase('REST')
- s.setTimeRemaining(mockWorkout.restTime)
-
- skip(mockWorkout)
-
- const after = usePlayerStore.getState()
- expect(after.phase).toBe('COMPLETE')
- expect(after.isRunning).toBe(false)
- })
- })
-
- // ── Progress ───────────────────────────────────────────────────────────
-
- describe('progress calculation', () => {
- it('should be 0 at phase start', () => {
- const s = usePlayerStore.getState()
- const dur = getPhaseDuration(s.phase, mockWorkout)
- expect(calcProgress(s.timeRemaining, dur)).toBe(0)
- })
-
- it('should increase as time counts down', () => {
- const s = usePlayerStore.getState()
- s.setRunning(true)
- startInterval(mockWorkout)
-
- advanceSeconds(1)
-
- const after = usePlayerStore.getState()
- const dur = getPhaseDuration(after.phase, mockWorkout)
- const progress = calcProgress(after.timeRemaining, dur)
-
- expect(progress).toBeGreaterThan(0)
- expect(progress).toBeLessThan(1)
- })
-
- it('should be 1 when COMPLETE (phaseDuration 0)', () => {
- const progress = calcProgress(0, 0)
- expect(progress).toBe(1)
- })
- })
-
- // ── Next Exercise ──────────────────────────────────────────────────────
-
- describe('nextExercise', () => {
- it('should return next exercise based on round', () => {
- // Round 1 → next is index 1 = Squats
- expect(nextExercise(1, mockWorkout)).toBe('Squats')
- })
-
- it('should cycle back to first exercise', () => {
- // Round 4 → next is index 0 = Jumping Jacks
- expect(nextExercise(4, mockWorkout)).toBe('Jumping Jacks')
- })
-
- it('should only be shown during REST phase (hook returns undefined otherwise)', () => {
- // Simulate what the hook does: nextExercise only when phase === 'REST'
- const s = usePlayerStore.getState()
- const showNext = s.phase === 'REST' ? nextExercise(s.currentRound, mockWorkout) : undefined
- expect(showNext).toBeUndefined() // phase is PREP
-
- s.setPhase('REST')
- const showNextRest = usePlayerStore.getState().phase === 'REST'
- ? nextExercise(usePlayerStore.getState().currentRound, mockWorkout)
- : undefined
- expect(showNextRest).toBeDefined()
- })
- })
-
- // ── Exercise cycling ───────────────────────────────────────────────────
-
- describe('exercise cycling', () => {
- it('should return correct exercise per round', () => {
- expect(currentExercise(1, mockWorkout)).toBe('Jumping Jacks')
- expect(currentExercise(2, mockWorkout)).toBe('Squats')
- expect(currentExercise(3, mockWorkout)).toBe('Push-ups')
- expect(currentExercise(4, mockWorkout)).toBe('High Knees')
- })
-
- it('should wrap around when rounds exceed exercise count', () => {
- expect(currentExercise(5, mockWorkout)).toBe('Jumping Jacks')
- expect(currentExercise(8, mockWorkout)).toBe('High Knees')
- })
- })
-})
diff --git a/src/__tests__/hooks/useTimer.test.ts b/src/__tests__/hooks/useTimer.test.ts
deleted file mode 100644
index 779b3b1..0000000
--- a/src/__tests__/hooks/useTimer.test.ts
+++ /dev/null
@@ -1,428 +0,0 @@
-import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
-import { usePlayerStore } from '../../shared/stores/playerStore'
-import type { Workout } from '../../shared/types'
-
-const mockWorkout: Workout = {
- id: 'test-workout',
- title: 'Test Workout',
- trainerId: 'trainer-1',
- category: 'full-body',
- level: 'Beginner',
- duration: 4,
- calories: 48,
- rounds: 8,
- prepTime: 10,
- workTime: 20,
- restTime: 10,
- equipment: [],
- musicVibe: 'electronic',
- exercises: [
- { name: 'Jumping Jacks', duration: 20 },
- { name: 'Squats', duration: 20 },
- { name: 'Push-ups', duration: 20 },
- { name: 'High Knees', duration: 20 },
- ],
-}
-
-function getExerciseForRound(round: number, exercises: typeof mockWorkout.exercises): string {
- const index = (round - 1) % exercises.length
- return exercises[index]?.name ?? ''
-}
-
-function getNextExercise(round: number, exercises: typeof mockWorkout.exercises): string | undefined {
- const index = round % exercises.length
- return exercises[index]?.name
-}
-
-function calculateProgress(timeRemaining: number, phaseDuration: number): number {
- return phaseDuration > 0 ? 1 - timeRemaining / phaseDuration : 1
-}
-
-function getPhaseDuration(phase: string, workout: typeof mockWorkout): number {
- switch (phase) {
- case 'PREP': return workout.prepTime
- case 'WORK': return workout.workTime
- case 'REST': return workout.restTime
- default: return 0
- }
-}
-
-describe('useTimer', () => {
- beforeEach(() => {
- vi.useFakeTimers()
- usePlayerStore.getState().reset()
- })
-
- afterEach(() => {
- vi.useRealTimers()
- vi.clearAllMocks()
- })
-
- describe('initialization', () => {
- it('should have correct default values', () => {
- const state = usePlayerStore.getState()
-
- expect(state.phase).toBe('PREP')
- expect(state.timeRemaining).toBe(10)
- expect(state.currentRound).toBe(1)
- expect(state.isPaused).toBe(false)
- expect(state.isRunning).toBe(false)
- expect(state.calories).toBe(0)
- })
-
- it('should load workout and set prepTime', () => {
- usePlayerStore.getState().loadWorkout(mockWorkout)
-
- const state = usePlayerStore.getState()
- expect(state.workout).toEqual(mockWorkout)
- expect(state.timeRemaining).toBe(mockWorkout.prepTime)
- })
-
- it('should return correct totalRounds from workout', () => {
- const totalRounds = mockWorkout.rounds
- expect(totalRounds).toBe(8)
- })
-
- it('should return correct currentExercise for round 1', () => {
- const exercise = getExerciseForRound(1, mockWorkout.exercises)
- expect(exercise).toBe('Jumping Jacks')
- })
- })
-
- describe('progress calculation', () => {
- it('should calculate progress as 0 at start', () => {
- usePlayerStore.getState().loadWorkout(mockWorkout)
- const state = usePlayerStore.getState()
-
- const phaseDuration = getPhaseDuration(state.phase, mockWorkout)
- const progress = calculateProgress(state.timeRemaining, phaseDuration)
-
- expect(progress).toBe(0)
- })
-
- it('should calculate progress correctly mid-phase', () => {
- usePlayerStore.getState().loadWorkout(mockWorkout)
- usePlayerStore.getState().setTimeRemaining(5)
-
- const state = usePlayerStore.getState()
- const phaseDuration = getPhaseDuration(state.phase, mockWorkout)
- const progress = calculateProgress(state.timeRemaining, phaseDuration)
-
- expect(progress).toBe(0.5)
- })
-
- it('should calculate progress as 1 when time is 0', () => {
- usePlayerStore.getState().loadWorkout(mockWorkout)
- usePlayerStore.getState().setTimeRemaining(0)
-
- const state = usePlayerStore.getState()
- const phaseDuration = getPhaseDuration(state.phase, mockWorkout)
- const progress = calculateProgress(state.timeRemaining, phaseDuration)
-
- expect(progress).toBe(1)
- })
- })
-
- describe('exercise tracking', () => {
- it('should return correct exercise for each round', () => {
- const expectedOrder = [
- 'Jumping Jacks', 'Squats', 'Push-ups', 'High Knees',
- 'Jumping Jacks', 'Squats', 'Push-ups', 'High Knees'
- ]
-
- for (let i = 0; i < 8; i++) {
- const round = i + 1
- const exercise = getExerciseForRound(round, mockWorkout.exercises)
- expect(exercise).toBe(expectedOrder[i])
- }
- })
-
- it('should cycle through exercises continuously', () => {
- expect(getExerciseForRound(1, mockWorkout.exercises)).toBe('Jumping Jacks')
- expect(getExerciseForRound(5, mockWorkout.exercises)).toBe('Jumping Jacks')
- expect(getExerciseForRound(9, mockWorkout.exercises)).toBe('Jumping Jacks')
- })
- })
-
- describe('controls', () => {
- describe('start', () => {
- it('should set running and clear pause', () => {
- const store = usePlayerStore.getState()
-
- store.setRunning(true)
- store.setPaused(false)
-
- const state = usePlayerStore.getState()
- expect(state.isRunning).toBe(true)
- expect(state.isPaused).toBe(false)
- })
- })
-
- describe('pause', () => {
- it('should set paused state', () => {
- const store = usePlayerStore.getState()
-
- store.setRunning(true)
- store.setPaused(true)
-
- const state = usePlayerStore.getState()
- expect(state.isRunning).toBe(true)
- expect(state.isPaused).toBe(true)
- })
- })
-
- describe('resume', () => {
- it('should clear paused state', () => {
- const store = usePlayerStore.getState()
-
- store.setRunning(true)
- store.setPaused(true)
- store.setPaused(false)
-
- const state = usePlayerStore.getState()
- expect(state.isPaused).toBe(false)
- })
- })
-
- describe('stop', () => {
- it('should reset all state', () => {
- const store = usePlayerStore.getState()
-
- store.loadWorkout(mockWorkout)
- store.setRunning(true)
- store.addCalories(25)
- store.setPhase('WORK')
- store.reset()
-
- const state = usePlayerStore.getState()
- expect(state.isRunning).toBe(false)
- expect(state.phase).toBe('PREP')
- expect(state.calories).toBe(0)
- })
- })
- })
-
- describe('skip functionality', () => {
- it('should skip from PREP to WORK', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- expect(store.phase).toBe('PREP')
-
- store.setPhase('WORK')
- store.setTimeRemaining(mockWorkout.workTime)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('WORK')
- expect(state.timeRemaining).toBe(mockWorkout.workTime)
- })
-
- it('should skip from WORK to REST', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setPhase('WORK')
-
- store.setPhase('REST')
- store.setTimeRemaining(mockWorkout.restTime)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('REST')
- expect(state.timeRemaining).toBe(mockWorkout.restTime)
- })
-
- it('should skip from REST to next WORK round', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setPhase('REST')
- store.setCurrentRound(1)
-
- store.setPhase('WORK')
- store.setTimeRemaining(mockWorkout.workTime)
- store.setCurrentRound(2)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('WORK')
- expect(state.currentRound).toBe(2)
- })
-
- it('should complete workout when skipping REST on final round', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setPhase('REST')
- store.setCurrentRound(mockWorkout.rounds)
-
- store.setPhase('COMPLETE')
- store.setRunning(false)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('COMPLETE')
- expect(state.isRunning).toBe(false)
- })
- })
-
- describe('timer tick simulation', () => {
- it('should decrement timeRemaining', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
-
- const initial = store.timeRemaining
- store.setTimeRemaining(initial - 1)
-
- expect(usePlayerStore.getState().timeRemaining).toBe(initial - 1)
- })
-
- it('should transition from PREP to WORK when time expires', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setRunning(true)
-
- store.setTimeRemaining(0)
- store.setPhase('WORK')
- store.setTimeRemaining(mockWorkout.workTime)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('WORK')
- expect(state.timeRemaining).toBe(mockWorkout.workTime)
- })
-
- it('should transition from WORK to REST and add calories', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setPhase('WORK')
- store.setRunning(true)
-
- store.setTimeRemaining(0)
- const caloriesPerRound = Math.round(mockWorkout.calories / mockWorkout.rounds)
- store.addCalories(caloriesPerRound)
- store.setPhase('REST')
- store.setTimeRemaining(mockWorkout.restTime)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('REST')
- expect(state.timeRemaining).toBe(mockWorkout.restTime)
-
- const expectedCalories = Math.round(mockWorkout.calories / mockWorkout.rounds)
- expect(state.calories).toBe(expectedCalories)
- })
-
- it('should transition from REST to WORK for next round', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setPhase('REST')
- store.setCurrentRound(1)
- store.setRunning(true)
-
- store.setTimeRemaining(0)
- store.setPhase('WORK')
- store.setTimeRemaining(mockWorkout.workTime)
- store.setCurrentRound(2)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('WORK')
- expect(state.currentRound).toBe(2)
- })
-
- it('should complete workout after final round', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setPhase('REST')
- store.setCurrentRound(mockWorkout.rounds)
- store.setRunning(true)
-
- store.setTimeRemaining(0)
- store.setPhase('COMPLETE')
- store.setTimeRemaining(0)
- store.setRunning(false)
-
- const state = usePlayerStore.getState()
- expect(state.phase).toBe('COMPLETE')
- expect(state.isRunning).toBe(false)
- })
- })
-
- describe('pause/resume behavior', () => {
- it('should not update timeRemaining when paused', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setRunning(true)
- store.setPaused(true)
-
- const pausedTime = store.timeRemaining
-
- vi.advanceTimersByTime(5000)
-
- expect(usePlayerStore.getState().timeRemaining).toBe(pausedTime)
- })
-
- it('should resume timer when resumed', () => {
- const store = usePlayerStore.getState()
- store.loadWorkout(mockWorkout)
- store.setRunning(true)
- store.setPaused(false)
-
- expect(store.isPaused).toBe(false)
- })
- })
-
- describe('isComplete flag', () => {
- it('should be false initially', () => {
- const store = usePlayerStore.getState()
- expect(store.phase === 'COMPLETE').toBe(false)
- })
-
- it('should be true when phase is COMPLETE', () => {
- const store = usePlayerStore.getState()
- store.setPhase('COMPLETE')
-
- expect(usePlayerStore.getState().phase === 'COMPLETE').toBe(true)
- })
- })
-
- describe('nextExercise', () => {
- it('should return next exercise during REST phase', () => {
- const nextExercise = getNextExercise(1, mockWorkout.exercises)
- expect(nextExercise).toBe('Squats')
- })
-
- it('should cycle to first exercise after last', () => {
- const nextExercise = getNextExercise(4, mockWorkout.exercises)
- expect(nextExercise).toBe('Jumping Jacks')
- })
- })
-
- describe('calorie tracking', () => {
- it('should accumulate calories for each WORK phase completed', () => {
- const store = usePlayerStore.getState()
- const caloriesPerRound = Math.round(mockWorkout.calories / mockWorkout.rounds)
-
- store.addCalories(caloriesPerRound)
- expect(usePlayerStore.getState().calories).toBe(caloriesPerRound)
-
- store.addCalories(caloriesPerRound)
- expect(usePlayerStore.getState().calories).toBe(caloriesPerRound * 2)
- })
- })
-
- describe('startedAt tracking', () => {
- it('should set startedAt when first running', () => {
- const store = usePlayerStore.getState()
- const beforeSet = Date.now()
-
- store.setRunning(true)
-
- const state = usePlayerStore.getState()
- expect(state.startedAt).toBeGreaterThanOrEqual(beforeSet)
- })
-
- it('should not update startedAt if already set', () => {
- const store = usePlayerStore.getState()
-
- store.setRunning(true)
- const firstStartedAt = usePlayerStore.getState().startedAt
-
- store.setRunning(false)
- store.setRunning(true)
-
- expect(usePlayerStore.getState().startedAt).toBe(firstStartedAt)
- })
- })
-})
diff --git a/src/__tests__/mocks/preload-rn-mock.cjs b/src/__tests__/mocks/preload-rn-mock.cjs
deleted file mode 100644
index 6573918..0000000
--- a/src/__tests__/mocks/preload-rn-mock.cjs
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Node.js --require preload script
- *
- * Patches Module._resolveFilename so that ANY require('react-native') call
- * (including CJS requires from @testing-library/react-native's build files)
- * gets redirected to our compiled mock at react-native.cjs.
- *
- * This runs before vitest starts, so it intercepts at the earliest possible point.
- */
-'use strict'
-
-const Module = require('module')
-const path = require('path')
-
-const mockPath = path.resolve(__dirname, 'react-native.cjs')
-const originalResolveFilename = Module._resolveFilename
-
-Module._resolveFilename = function (request, parent, isMain, options) {
- // Intercept 'react-native' and any subpath like 'react-native/index'
- if (request === 'react-native' || request.startsWith('react-native/')) {
- return mockPath
- }
- return originalResolveFilename.call(this, request, parent, isMain, options)
-}
diff --git a/src/__tests__/mocks/react-native.cjs b/src/__tests__/mocks/react-native.cjs
deleted file mode 100644
index bf97d62..0000000
--- a/src/__tests__/mocks/react-native.cjs
+++ /dev/null
@@ -1,423 +0,0 @@
-"use strict";
-/**
- * react-native mock for component rendering tests (vitest + jsdom)
- *
- * This file is used as a resolve.alias for 'react-native' in vitest.config.render.ts.
- * It provides real React component implementations so @testing-library/react-native
- * can render and query them in jsdom. The real react-native package cannot be loaded
- * in Node 22 due to ESM/typeof issues.
- */
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.LayoutAnimation = exports.I18nManager = exports.AccessibilityInfo = exports.NativeModules = exports.Appearance = exports.BackHandler = exports.Linking = exports.Keyboard = exports.AppState = exports.PixelRatio = exports.Easing = exports.Animated = exports.Alert = exports.Platform = exports.Dimensions = exports.StyleSheet = exports.FlatList = exports.Modal = exports.Pressable = exports.SectionList = exports.RefreshControl = exports.KeyboardAvoidingView = exports.StatusBar = exports.Switch = exports.TouchableWithoutFeedback = exports.TouchableHighlight = exports.TouchableOpacity = exports.ActivityIndicator = exports.SafeAreaView = exports.ScrollView = exports.ImageBackground = exports.Image = exports.TextInput = exports.Text = exports.View = void 0;
-exports.useWindowDimensions = useWindowDimensions;
-exports.PlatformColor = PlatformColor;
-exports.useColorScheme = useColorScheme;
-const react_1 = __importDefault(require("react"));
-// ---------------------------------------------------------------------------
-// Helper: create a simple host component that forwards props to a DOM element
-// ---------------------------------------------------------------------------
-function createMockComponent(name) {
- const Component = react_1.default.forwardRef((props, ref) => {
- const { children, testID, ...rest } = props;
- return react_1.default.createElement(name, { ...rest, testID, 'data-testid': testID, ref }, children);
- });
- Component.displayName = name;
- return Component;
-}
-// ---------------------------------------------------------------------------
-// Core RN Components
-// ---------------------------------------------------------------------------
-exports.View = createMockComponent('View');
-exports.Text = createMockComponent('Text');
-exports.TextInput = createMockComponent('TextInput');
-exports.Image = createMockComponent('Image');
-exports.ImageBackground = createMockComponent('ImageBackground');
-exports.ScrollView = createMockComponent('ScrollView');
-exports.SafeAreaView = createMockComponent('SafeAreaView');
-exports.ActivityIndicator = createMockComponent('ActivityIndicator');
-exports.TouchableOpacity = createMockComponent('TouchableOpacity');
-exports.TouchableHighlight = createMockComponent('TouchableHighlight');
-exports.TouchableWithoutFeedback = createMockComponent('TouchableWithoutFeedback');
-exports.Switch = createMockComponent('Switch');
-exports.StatusBar = createMockComponent('StatusBar');
-exports.KeyboardAvoidingView = createMockComponent('KeyboardAvoidingView');
-exports.RefreshControl = createMockComponent('RefreshControl');
-exports.SectionList = createMockComponent('SectionList');
-// Pressable needs onPress / disabled support
-exports.Pressable = react_1.default.forwardRef((props, ref) => {
- const { children, testID, onPress, disabled, ...rest } = props;
- return react_1.default.createElement('Pressable', {
- ...rest,
- testID,
- 'data-testid': testID,
- onClick: disabled ? undefined : onPress,
- disabled,
- onPress,
- ref,
- }, typeof children === 'function' ? children({ pressed: false }) : children);
-});
-exports.Pressable.displayName = 'Pressable';
-// Modal
-exports.Modal = react_1.default.forwardRef((props, ref) => {
- const { children, visible, testID, onRequestClose, ...rest } = props;
- if (!visible)
- return null;
- return react_1.default.createElement('Modal', { ...rest, testID, 'data-testid': testID, visible, onRequestClose, ref }, children);
-});
-exports.Modal.displayName = 'Modal';
-// FlatList — simplified: just render items in a ScrollView-like wrapper
-exports.FlatList = react_1.default.forwardRef((props, ref) => {
- const { data, renderItem, keyExtractor, testID, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, ...rest } = props;
- const items = data?.map((item, index) => {
- const key = keyExtractor ? keyExtractor(item, index) : String(index);
- return react_1.default.createElement(react_1.default.Fragment, { key }, renderItem({ item, index, separators: {} }));
- }) ?? [];
- const children = [
- ListHeaderComponent ? react_1.default.createElement(react_1.default.Fragment, { key: '__header' }, typeof ListHeaderComponent === 'function' ? react_1.default.createElement(ListHeaderComponent) : ListHeaderComponent) : null,
- ...(items.length === 0 && ListEmptyComponent
- ? [react_1.default.createElement(react_1.default.Fragment, { key: '__empty' }, typeof ListEmptyComponent === 'function' ? react_1.default.createElement(ListEmptyComponent) : ListEmptyComponent)]
- : items),
- ListFooterComponent ? react_1.default.createElement(react_1.default.Fragment, { key: '__footer' }, typeof ListFooterComponent === 'function' ? react_1.default.createElement(ListFooterComponent) : ListFooterComponent) : null,
- ].filter(Boolean);
- return react_1.default.createElement('FlatList', { ...rest, testID, 'data-testid': testID, ref }, ...children);
-});
-exports.FlatList.displayName = 'FlatList';
-// ---------------------------------------------------------------------------
-// StyleSheet
-// ---------------------------------------------------------------------------
-const absoluteFillValue = {
- position: 'absolute',
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
-};
-exports.StyleSheet = {
- create: (styles) => styles,
- flatten: (style) => {
- if (Array.isArray(style)) {
- return Object.assign({}, ...style.filter(Boolean).map((s) => exports.StyleSheet.flatten(s)));
- }
- return style ?? {};
- },
- absoluteFill: absoluteFillValue,
- absoluteFillObject: absoluteFillValue,
- hairlineWidth: 1,
- compose: (a, b) => [a, b],
-};
-// ---------------------------------------------------------------------------
-// Dimensions
-// ---------------------------------------------------------------------------
-const dimensionsValues = { width: 375, height: 812, scale: 2, fontScale: 1 };
-exports.Dimensions = {
- get: (_dim) => dimensionsValues,
- addEventListener: () => ({ remove: () => { } }),
- set: () => { },
-};
-// ---------------------------------------------------------------------------
-// useWindowDimensions
-// ---------------------------------------------------------------------------
-function useWindowDimensions() {
- return dimensionsValues;
-}
-// ---------------------------------------------------------------------------
-// Platform
-// ---------------------------------------------------------------------------
-exports.Platform = {
- OS: 'ios',
- Version: 18,
- isPad: false,
- isTVOS: false,
- isTV: false,
- select: (obj) => obj.ios ?? obj.default,
- constants: {
- reactNativeVersion: { major: 0, minor: 81, patch: 5 },
- },
-};
-// ---------------------------------------------------------------------------
-// Alert
-// ---------------------------------------------------------------------------
-exports.Alert = {
- alert: (() => { }),
-};
-// ---------------------------------------------------------------------------
-// Animated
-// ---------------------------------------------------------------------------
-class AnimatedValue {
- _value;
- _listeners = new Map();
- constructor(value = 0) {
- this._value = value;
- }
- setValue(value) {
- this._value = value;
- }
- setOffset(_offset) { }
- flattenOffset() { }
- extractOffset() { }
- addListener(cb) {
- const id = String(Math.random());
- this._listeners.set(id, cb);
- return id;
- }
- removeListener(id) {
- this._listeners.delete(id);
- }
- removeAllListeners() {
- this._listeners.clear();
- }
- stopAnimation(cb) {
- cb?.(this._value);
- }
- resetAnimation(cb) {
- cb?.(this._value);
- }
- interpolate(config) {
- return {
- ...config,
- __isAnimatedInterpolation: true,
- interpolate: (c) => ({ ...c, __isAnimatedInterpolation: true }),
- };
- }
- // Arithmetic methods for combined animations
- __getValue() { return this._value; }
-}
-class AnimatedValueXY {
- x;
- y;
- constructor(value) {
- this.x = new AnimatedValue(value?.x ?? 0);
- this.y = new AnimatedValue(value?.y ?? 0);
- }
- setValue(value) {
- this.x.setValue(value.x);
- this.y.setValue(value.y);
- }
- setOffset(offset) {
- this.x.setOffset(offset.x);
- this.y.setOffset(offset.y);
- }
- flattenOffset() {
- this.x.flattenOffset();
- this.y.flattenOffset();
- }
- extractOffset() {
- this.x.extractOffset();
- this.y.extractOffset();
- }
- stopAnimation(cb) {
- this.x.stopAnimation();
- this.y.stopAnimation();
- cb?.({ x: this.x._value, y: this.y._value });
- }
- addListener() { return ''; }
- removeListener() { }
- removeAllListeners() { }
- getLayout() {
- return { left: this.x, top: this.y };
- }
- getTranslateTransform() {
- return [{ translateX: this.x }, { translateY: this.y }];
- }
-}
-const mockAnimationResult = { start: (cb) => cb?.({ finished: true }), stop: () => { }, reset: () => { } };
-exports.Animated = {
- Value: AnimatedValue,
- ValueXY: AnimatedValueXY,
- View: createMockComponent('Animated.View'),
- Text: createMockComponent('Animated.Text'),
- Image: createMockComponent('Animated.Image'),
- ScrollView: createMockComponent('Animated.ScrollView'),
- FlatList: createMockComponent('Animated.FlatList'),
- SectionList: createMockComponent('Animated.SectionList'),
- createAnimatedComponent: (comp) => comp,
- timing: (_value, _config) => mockAnimationResult,
- spring: (_value, _config) => mockAnimationResult,
- decay: (_value, _config) => mockAnimationResult,
- parallel: (_animations) => mockAnimationResult,
- sequence: (_animations) => mockAnimationResult,
- stagger: (_delay, _animations) => mockAnimationResult,
- delay: (_time) => mockAnimationResult,
- loop: (_animation, _config) => mockAnimationResult,
- event: (_argMapping, _config) => () => { },
- add: (a, b) => a,
- subtract: (a, b) => a,
- divide: (a, b) => a,
- multiply: (a, b) => a,
- diffClamp: (a, _min, _max) => a,
-};
-// ---------------------------------------------------------------------------
-// Easing
-// ---------------------------------------------------------------------------
-exports.Easing = {
- linear: (t) => t,
- ease: (t) => t,
- quad: (t) => t * t,
- cubic: (t) => t * t * t,
- poly: (_n) => (t) => t,
- sin: (t) => t,
- circle: (t) => t,
- exp: (t) => t,
- elastic: (_bounciness) => (t) => t,
- back: (_s) => (t) => t,
- bounce: (t) => t,
- bezier: (_x1, _y1, _x2, _y2) => (t) => t,
- in: (fn) => fn,
- out: (fn) => fn,
- inOut: (fn) => fn,
- step0: (t) => t,
- step1: (t) => t,
-};
-// ---------------------------------------------------------------------------
-// PixelRatio
-// ---------------------------------------------------------------------------
-exports.PixelRatio = {
- get: () => 2,
- getFontScale: () => 1,
- getPixelSizeForLayoutSize: (size) => size * 2,
- roundToNearestPixel: (size) => Math.round(size * 2) / 2,
-};
-// ---------------------------------------------------------------------------
-// AppState
-// ---------------------------------------------------------------------------
-exports.AppState = {
- currentState: 'active',
- addEventListener: () => ({ remove: () => { } }),
- removeEventListener: () => { },
-};
-// ---------------------------------------------------------------------------
-// Keyboard
-// ---------------------------------------------------------------------------
-exports.Keyboard = {
- addListener: () => ({ remove: () => { } }),
- removeListener: () => { },
- dismiss: () => { },
- isVisible: () => false,
- metrics: () => undefined,
-};
-// ---------------------------------------------------------------------------
-// Linking
-// ---------------------------------------------------------------------------
-exports.Linking = {
- openURL: async () => { },
- canOpenURL: async () => true,
- getInitialURL: async () => null,
- addEventListener: () => ({ remove: () => { } }),
-};
-// ---------------------------------------------------------------------------
-// BackHandler
-// ---------------------------------------------------------------------------
-exports.BackHandler = {
- addEventListener: () => ({ remove: () => { } }),
- removeEventListener: () => { },
- exitApp: () => { },
-};
-// ---------------------------------------------------------------------------
-// Appearance
-// ---------------------------------------------------------------------------
-exports.Appearance = {
- getColorScheme: () => 'dark',
- addChangeListener: () => ({ remove: () => { } }),
- setColorScheme: () => { },
-};
-// ---------------------------------------------------------------------------
-// PlatformColor (no-op stub)
-// ---------------------------------------------------------------------------
-function PlatformColor(..._args) {
- return '';
-}
-// ---------------------------------------------------------------------------
-// NativeModules
-// ---------------------------------------------------------------------------
-exports.NativeModules = {};
-// ---------------------------------------------------------------------------
-// AccessibilityInfo
-// ---------------------------------------------------------------------------
-exports.AccessibilityInfo = {
- isScreenReaderEnabled: async () => false,
- addEventListener: () => ({ remove: () => { } }),
- setAccessibilityFocus: () => { },
- announceForAccessibility: () => { },
- isReduceMotionEnabled: async () => false,
- isBoldTextEnabled: async () => false,
- isGrayscaleEnabled: async () => false,
- isInvertColorsEnabled: async () => false,
- prefersCrossFadeTransitions: async () => false,
-};
-// ---------------------------------------------------------------------------
-// I18nManager
-// ---------------------------------------------------------------------------
-exports.I18nManager = {
- isRTL: false,
- doLeftAndRightSwapInRTL: true,
- allowRTL: () => { },
- forceRTL: () => { },
- swapLeftAndRightInRTL: () => { },
-};
-// ---------------------------------------------------------------------------
-// LayoutAnimation
-// ---------------------------------------------------------------------------
-exports.LayoutAnimation = {
- configureNext: () => { },
- create: () => ({}),
- Types: { spring: 'spring', linear: 'linear', easeInEaseOut: 'easeInEaseOut', easeIn: 'easeIn', easeOut: 'easeOut' },
- Properties: { opacity: 'opacity', scaleX: 'scaleX', scaleY: 'scaleY', scaleXY: 'scaleXY' },
- Presets: {
- easeInEaseOut: {},
- linear: {},
- spring: {},
- },
-};
-// ---------------------------------------------------------------------------
-// useColorScheme
-// ---------------------------------------------------------------------------
-function useColorScheme() {
- return 'dark';
-}
-// ---------------------------------------------------------------------------
-// Default export (some code does `import RN from 'react-native'`)
-// ---------------------------------------------------------------------------
-const RN = {
- View: exports.View,
- Text: exports.Text,
- TextInput: exports.TextInput,
- Image: exports.Image,
- ImageBackground: exports.ImageBackground,
- ScrollView: exports.ScrollView,
- FlatList: exports.FlatList,
- SectionList: exports.SectionList,
- SafeAreaView: exports.SafeAreaView,
- ActivityIndicator: exports.ActivityIndicator,
- TouchableOpacity: exports.TouchableOpacity,
- TouchableHighlight: exports.TouchableHighlight,
- TouchableWithoutFeedback: exports.TouchableWithoutFeedback,
- Pressable: exports.Pressable,
- Modal: exports.Modal,
- Switch: exports.Switch,
- StatusBar: exports.StatusBar,
- KeyboardAvoidingView: exports.KeyboardAvoidingView,
- RefreshControl: exports.RefreshControl,
- StyleSheet: exports.StyleSheet,
- Dimensions: exports.Dimensions,
- Platform: exports.Platform,
- Alert: exports.Alert,
- Animated: exports.Animated,
- Easing: exports.Easing,
- PixelRatio: exports.PixelRatio,
- AppState: exports.AppState,
- Keyboard: exports.Keyboard,
- Linking: exports.Linking,
- BackHandler: exports.BackHandler,
- Appearance: exports.Appearance,
- PlatformColor,
- NativeModules: exports.NativeModules,
- AccessibilityInfo: exports.AccessibilityInfo,
- I18nManager: exports.I18nManager,
- LayoutAnimation: exports.LayoutAnimation,
- useColorScheme,
- useWindowDimensions,
-};
-exports.default = RN;
diff --git a/src/__tests__/mocks/react-native.ts b/src/__tests__/mocks/react-native.ts
deleted file mode 100644
index 1c6bbb9..0000000
--- a/src/__tests__/mocks/react-native.ts
+++ /dev/null
@@ -1,458 +0,0 @@
-/**
- * react-native mock for component rendering tests (vitest + jsdom)
- *
- * This file is used as a resolve.alias for 'react-native' in vitest.config.render.ts.
- * It provides real React component implementations so @testing-library/react-native
- * can render and query them in jsdom. The real react-native package cannot be loaded
- * in Node 22 due to ESM/typeof issues.
- */
-
-import React from 'react'
-
-// ---------------------------------------------------------------------------
-// Helper: create a simple host component that forwards props to a DOM element
-// ---------------------------------------------------------------------------
-function createMockComponent(name: string) {
- const Component = React.forwardRef((props: any, ref: any) => {
- const { children, testID, ...rest } = props
- return React.createElement(
- name,
- { ...rest, testID, 'data-testid': testID, ref },
- children
- )
- })
- Component.displayName = name
- return Component
-}
-
-// ---------------------------------------------------------------------------
-// Core RN Components
-// ---------------------------------------------------------------------------
-export const View = createMockComponent('View')
-export const Text = createMockComponent('Text')
-export const TextInput = createMockComponent('TextInput')
-export const Image = createMockComponent('Image')
-export const ImageBackground = createMockComponent('ImageBackground')
-export const ScrollView = createMockComponent('ScrollView')
-export const SafeAreaView = createMockComponent('SafeAreaView')
-export const ActivityIndicator = createMockComponent('ActivityIndicator')
-export const TouchableOpacity = createMockComponent('TouchableOpacity')
-export const TouchableHighlight = createMockComponent('TouchableHighlight')
-export const TouchableWithoutFeedback = createMockComponent('TouchableWithoutFeedback')
-export const Switch = createMockComponent('Switch')
-export const StatusBar = createMockComponent('StatusBar')
-export const KeyboardAvoidingView = createMockComponent('KeyboardAvoidingView')
-export const RefreshControl = createMockComponent('RefreshControl')
-export const SectionList = createMockComponent('SectionList')
-
-// Pressable needs onPress / disabled support
-export const Pressable = React.forwardRef((props: any, ref: any) => {
- const { children, testID, onPress, disabled, ...rest } = props
- return React.createElement(
- 'Pressable',
- {
- ...rest,
- testID,
- 'data-testid': testID,
- onClick: disabled ? undefined : onPress,
- disabled,
- onPress,
- ref,
- },
- typeof children === 'function' ? children({ pressed: false }) : children
- )
-})
-;(Pressable as any).displayName = 'Pressable'
-
-// Modal
-export const Modal = React.forwardRef((props: any, ref: any) => {
- const { children, visible, testID, onRequestClose, ...rest } = props
- if (!visible) return null
- return React.createElement(
- 'Modal',
- { ...rest, testID, 'data-testid': testID, visible, onRequestClose, ref },
- children
- )
-})
-;(Modal as any).displayName = 'Modal'
-
-// FlatList — simplified: just render items in a ScrollView-like wrapper
-export const FlatList = React.forwardRef((props: any, ref: any) => {
- const { data, renderItem, keyExtractor, testID, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, ...rest } = props
- const items = data?.map((item: any, index: number) => {
- const key = keyExtractor ? keyExtractor(item, index) : String(index)
- return React.createElement(React.Fragment, { key }, renderItem({ item, index, separators: {} }))
- }) ?? []
-
- const children = [
- ListHeaderComponent ? React.createElement(React.Fragment, { key: '__header' }, typeof ListHeaderComponent === 'function' ? React.createElement(ListHeaderComponent) : ListHeaderComponent) : null,
- ...(items.length === 0 && ListEmptyComponent
- ? [React.createElement(React.Fragment, { key: '__empty' }, typeof ListEmptyComponent === 'function' ? React.createElement(ListEmptyComponent) : ListEmptyComponent)]
- : items),
- ListFooterComponent ? React.createElement(React.Fragment, { key: '__footer' }, typeof ListFooterComponent === 'function' ? React.createElement(ListFooterComponent) : ListFooterComponent) : null,
- ].filter(Boolean)
-
- return React.createElement('FlatList', { ...rest, testID, 'data-testid': testID, ref }, ...children)
-})
-;(FlatList as any).displayName = 'FlatList'
-
-// ---------------------------------------------------------------------------
-// StyleSheet
-// ---------------------------------------------------------------------------
-const absoluteFillValue = {
- position: 'absolute' as const,
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
-}
-
-export const StyleSheet = {
- create: >(styles: T): T => styles,
- flatten: (style: any): any => {
- if (Array.isArray(style)) {
- return Object.assign({}, ...style.filter(Boolean).map((s: any) => StyleSheet.flatten(s)))
- }
- return style ?? {}
- },
- absoluteFill: absoluteFillValue,
- absoluteFillObject: absoluteFillValue,
- hairlineWidth: 1,
- compose: (a: any, b: any) => [a, b],
-}
-
-// ---------------------------------------------------------------------------
-// Dimensions
-// ---------------------------------------------------------------------------
-const dimensionsValues = { width: 375, height: 812, scale: 2, fontScale: 1 }
-export const Dimensions = {
- get: (_dim?: string) => dimensionsValues,
- addEventListener: () => ({ remove: () => {} }),
- set: () => {},
-}
-
-// ---------------------------------------------------------------------------
-// useWindowDimensions
-// ---------------------------------------------------------------------------
-export function useWindowDimensions() {
- return dimensionsValues
-}
-
-// ---------------------------------------------------------------------------
-// Platform
-// ---------------------------------------------------------------------------
-export const Platform = {
- OS: 'ios' as const,
- Version: 18,
- isPad: false,
- isTVOS: false,
- isTV: false,
- select: (obj: any) => obj.ios ?? obj.default,
- constants: {
- reactNativeVersion: { major: 0, minor: 81, patch: 5 },
- },
-}
-
-// ---------------------------------------------------------------------------
-// Alert
-// ---------------------------------------------------------------------------
-export const Alert = {
- alert: (() => {}) as any,
-}
-
-// ---------------------------------------------------------------------------
-// Animated
-// ---------------------------------------------------------------------------
-class AnimatedValue {
- _value: number
- _listeners: Map = new Map()
- constructor(value: number = 0) {
- this._value = value
- }
- setValue(value: number) {
- this._value = value
- }
- setOffset(_offset: number) {}
- flattenOffset() {}
- extractOffset() {}
- addListener(cb: Function) {
- const id = String(Math.random())
- this._listeners.set(id, cb)
- return id
- }
- removeListener(id: string) {
- this._listeners.delete(id)
- }
- removeAllListeners() {
- this._listeners.clear()
- }
- stopAnimation(cb?: Function) {
- cb?.(this._value)
- }
- resetAnimation(cb?: Function) {
- cb?.(this._value)
- }
- interpolate(config: any) {
- return {
- ...config,
- __isAnimatedInterpolation: true,
- interpolate: (c: any) => ({ ...c, __isAnimatedInterpolation: true }),
- }
- }
- // Arithmetic methods for combined animations
- __getValue() { return this._value }
-}
-
-class AnimatedValueXY {
- x: AnimatedValue
- y: AnimatedValue
- constructor(value?: { x?: number; y?: number }) {
- this.x = new AnimatedValue(value?.x ?? 0)
- this.y = new AnimatedValue(value?.y ?? 0)
- }
- setValue(value: { x: number; y: number }) {
- this.x.setValue(value.x)
- this.y.setValue(value.y)
- }
- setOffset(offset: { x: number; y: number }) {
- this.x.setOffset(offset.x)
- this.y.setOffset(offset.y)
- }
- flattenOffset() {
- this.x.flattenOffset()
- this.y.flattenOffset()
- }
- extractOffset() {
- this.x.extractOffset()
- this.y.extractOffset()
- }
- stopAnimation(cb?: Function) {
- this.x.stopAnimation()
- this.y.stopAnimation()
- cb?.({ x: this.x._value, y: this.y._value })
- }
- addListener() { return '' }
- removeListener() {}
- removeAllListeners() {}
- getLayout() {
- return { left: this.x, top: this.y }
- }
- getTranslateTransform() {
- return [{ translateX: this.x }, { translateY: this.y }]
- }
-}
-
-const mockAnimationResult = { start: (cb?: Function) => cb?.({ finished: true }), stop: () => {}, reset: () => {} }
-
-export const Animated = {
- Value: AnimatedValue,
- ValueXY: AnimatedValueXY,
- View: createMockComponent('Animated.View'),
- Text: createMockComponent('Animated.Text'),
- Image: createMockComponent('Animated.Image'),
- ScrollView: createMockComponent('Animated.ScrollView'),
- FlatList: createMockComponent('Animated.FlatList'),
- SectionList: createMockComponent('Animated.SectionList'),
- createAnimatedComponent: (comp: any) => comp,
- timing: (_value: any, _config: any) => mockAnimationResult,
- spring: (_value: any, _config: any) => mockAnimationResult,
- decay: (_value: any, _config: any) => mockAnimationResult,
- parallel: (_animations: any[]) => mockAnimationResult,
- sequence: (_animations: any[]) => mockAnimationResult,
- stagger: (_delay: number, _animations: any[]) => mockAnimationResult,
- delay: (_time: number) => mockAnimationResult,
- loop: (_animation: any, _config?: any) => mockAnimationResult,
- event: (_argMapping: any[], _config?: any) => () => {},
- add: (a: any, b: any) => a,
- subtract: (a: any, b: any) => a,
- divide: (a: any, b: any) => a,
- multiply: (a: any, b: any) => a,
- diffClamp: (a: any, _min: number, _max: number) => a,
-}
-
-// ---------------------------------------------------------------------------
-// Easing
-// ---------------------------------------------------------------------------
-export const Easing = {
- linear: (t: number) => t,
- ease: (t: number) => t,
- quad: (t: number) => t * t,
- cubic: (t: number) => t * t * t,
- poly: (_n: number) => (t: number) => t,
- sin: (t: number) => t,
- circle: (t: number) => t,
- exp: (t: number) => t,
- elastic: (_bounciness?: number) => (t: number) => t,
- back: (_s?: number) => (t: number) => t,
- bounce: (t: number) => t,
- bezier: (_x1: number, _y1: number, _x2: number, _y2: number) => (t: number) => t,
- in: (fn: Function) => fn,
- out: (fn: Function) => fn,
- inOut: (fn: Function) => fn,
- step0: (t: number) => t,
- step1: (t: number) => t,
-}
-
-// ---------------------------------------------------------------------------
-// PixelRatio
-// ---------------------------------------------------------------------------
-export const PixelRatio = {
- get: () => 2,
- getFontScale: () => 1,
- getPixelSizeForLayoutSize: (size: number) => size * 2,
- roundToNearestPixel: (size: number) => Math.round(size * 2) / 2,
-}
-
-// ---------------------------------------------------------------------------
-// AppState
-// ---------------------------------------------------------------------------
-export const AppState = {
- currentState: 'active',
- addEventListener: () => ({ remove: () => {} }),
- removeEventListener: () => {},
-}
-
-// ---------------------------------------------------------------------------
-// Keyboard
-// ---------------------------------------------------------------------------
-export const Keyboard = {
- addListener: () => ({ remove: () => {} }),
- removeListener: () => {},
- dismiss: () => {},
- isVisible: () => false,
- metrics: () => undefined,
-}
-
-// ---------------------------------------------------------------------------
-// Linking
-// ---------------------------------------------------------------------------
-export const Linking = {
- openURL: async () => {},
- canOpenURL: async () => true,
- getInitialURL: async () => null,
- addEventListener: () => ({ remove: () => {} }),
-}
-
-// ---------------------------------------------------------------------------
-// BackHandler
-// ---------------------------------------------------------------------------
-export const BackHandler = {
- addEventListener: () => ({ remove: () => {} }),
- removeEventListener: () => {},
- exitApp: () => {},
-}
-
-// ---------------------------------------------------------------------------
-// Appearance
-// ---------------------------------------------------------------------------
-export const Appearance = {
- getColorScheme: () => 'dark',
- addChangeListener: () => ({ remove: () => {} }),
- setColorScheme: () => {},
-}
-
-// ---------------------------------------------------------------------------
-// PlatformColor (no-op stub)
-// ---------------------------------------------------------------------------
-export function PlatformColor(..._args: string[]) {
- return ''
-}
-
-// ---------------------------------------------------------------------------
-// NativeModules
-// ---------------------------------------------------------------------------
-export const NativeModules = {}
-
-// ---------------------------------------------------------------------------
-// AccessibilityInfo
-// ---------------------------------------------------------------------------
-export const AccessibilityInfo = {
- isScreenReaderEnabled: async () => false,
- addEventListener: () => ({ remove: () => {} }),
- setAccessibilityFocus: () => {},
- announceForAccessibility: () => {},
- isReduceMotionEnabled: async () => false,
- isBoldTextEnabled: async () => false,
- isGrayscaleEnabled: async () => false,
- isInvertColorsEnabled: async () => false,
- prefersCrossFadeTransitions: async () => false,
-}
-
-// ---------------------------------------------------------------------------
-// I18nManager
-// ---------------------------------------------------------------------------
-export const I18nManager = {
- isRTL: false,
- doLeftAndRightSwapInRTL: true,
- allowRTL: () => {},
- forceRTL: () => {},
- swapLeftAndRightInRTL: () => {},
-}
-
-// ---------------------------------------------------------------------------
-// LayoutAnimation
-// ---------------------------------------------------------------------------
-export const LayoutAnimation = {
- configureNext: () => {},
- create: () => ({}),
- Types: { spring: 'spring', linear: 'linear', easeInEaseOut: 'easeInEaseOut', easeIn: 'easeIn', easeOut: 'easeOut' },
- Properties: { opacity: 'opacity', scaleX: 'scaleX', scaleY: 'scaleY', scaleXY: 'scaleXY' },
- Presets: {
- easeInEaseOut: {},
- linear: {},
- spring: {},
- },
-}
-
-// ---------------------------------------------------------------------------
-// useColorScheme
-// ---------------------------------------------------------------------------
-export function useColorScheme() {
- return 'dark'
-}
-
-// ---------------------------------------------------------------------------
-// Default export (some code does `import RN from 'react-native'`)
-// ---------------------------------------------------------------------------
-const RN = {
- View,
- Text,
- TextInput,
- Image,
- ImageBackground,
- ScrollView,
- FlatList,
- SectionList,
- SafeAreaView,
- ActivityIndicator,
- TouchableOpacity,
- TouchableHighlight,
- TouchableWithoutFeedback,
- Pressable,
- Modal,
- Switch,
- StatusBar,
- KeyboardAvoidingView,
- RefreshControl,
- StyleSheet,
- Dimensions,
- Platform,
- Alert,
- Animated,
- Easing,
- PixelRatio,
- AppState,
- Keyboard,
- Linking,
- BackHandler,
- Appearance,
- PlatformColor,
- NativeModules,
- AccessibilityInfo,
- I18nManager,
- LayoutAnimation,
- useColorScheme,
- useWindowDimensions,
-}
-
-export default RN
diff --git a/src/__tests__/services/access.test.ts b/src/__tests__/services/access.test.ts
deleted file mode 100644
index 2ead285..0000000
--- a/src/__tests__/services/access.test.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import {
- FREE_WORKOUT_IDS,
- FREE_WORKOUT_COUNT,
- isFreeWorkout,
- canAccessWorkout,
-} from '../../shared/services/access'
-
-describe('access service', () => {
- describe('FREE_WORKOUT_IDS', () => {
- it('should contain exactly 3 free workout IDs', () => {
- expect(FREE_WORKOUT_IDS).toHaveLength(3)
- })
-
- it('should include workout 1 (Full Body Ignite)', () => {
- expect(FREE_WORKOUT_IDS).toContain('1')
- })
-
- it('should include workout 11 (Core Crusher)', () => {
- expect(FREE_WORKOUT_IDS).toContain('11')
- })
-
- it('should include workout 43 (Dance Cardio)', () => {
- expect(FREE_WORKOUT_IDS).toContain('43')
- })
-
- it('should be readonly (immutable)', () => {
- // TypeScript enforces readonly at compile time; at runtime we verify the array content is stable
- const snapshot = [...FREE_WORKOUT_IDS]
- expect(FREE_WORKOUT_IDS).toEqual(snapshot)
- })
- })
-
- describe('FREE_WORKOUT_COUNT', () => {
- it('should equal the length of FREE_WORKOUT_IDS', () => {
- expect(FREE_WORKOUT_COUNT).toBe(FREE_WORKOUT_IDS.length)
- })
-
- it('should be 3', () => {
- expect(FREE_WORKOUT_COUNT).toBe(3)
- })
- })
-
- describe('isFreeWorkout', () => {
- it('should return true for free workout ID "1"', () => {
- expect(isFreeWorkout('1')).toBe(true)
- })
-
- it('should return true for free workout ID "11"', () => {
- expect(isFreeWorkout('11')).toBe(true)
- })
-
- it('should return true for free workout ID "43"', () => {
- expect(isFreeWorkout('43')).toBe(true)
- })
-
- it('should return false for non-free workout ID "2"', () => {
- expect(isFreeWorkout('2')).toBe(false)
- })
-
- it('should return false for non-free workout ID "10"', () => {
- expect(isFreeWorkout('10')).toBe(false)
- })
-
- it('should return false for non-free workout ID "44"', () => {
- expect(isFreeWorkout('44')).toBe(false)
- })
-
- it('should return false for empty string', () => {
- expect(isFreeWorkout('')).toBe(false)
- })
-
- it('should return false for non-existent ID', () => {
- expect(isFreeWorkout('999')).toBe(false)
- })
- })
-
- describe('canAccessWorkout', () => {
- describe('premium user', () => {
- it('should access free workout', () => {
- expect(canAccessWorkout('1', true)).toBe(true)
- })
-
- it('should access non-free workout', () => {
- expect(canAccessWorkout('2', true)).toBe(true)
- })
-
- it('should access any workout ID', () => {
- expect(canAccessWorkout('999', true)).toBe(true)
- })
- })
-
- describe('free user', () => {
- it('should access free workout ID "1"', () => {
- expect(canAccessWorkout('1', false)).toBe(true)
- })
-
- it('should access free workout ID "11"', () => {
- expect(canAccessWorkout('11', false)).toBe(true)
- })
-
- it('should access free workout ID "43"', () => {
- expect(canAccessWorkout('43', false)).toBe(true)
- })
-
- it('should NOT access non-free workout ID "2"', () => {
- expect(canAccessWorkout('2', false)).toBe(false)
- })
-
- it('should NOT access non-free workout ID "10"', () => {
- expect(canAccessWorkout('10', false)).toBe(false)
- })
-
- it('should NOT access non-free workout ID "50"', () => {
- expect(canAccessWorkout('50', false)).toBe(false)
- })
-
- it('should NOT access empty string ID', () => {
- expect(canAccessWorkout('', false)).toBe(false)
- })
- })
- })
-})
diff --git a/src/__tests__/services/analytics.test.ts b/src/__tests__/services/analytics.test.ts
deleted file mode 100644
index 49b8a7c..0000000
--- a/src/__tests__/services/analytics.test.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
-
-vi.mock('posthog-react-native', () => {
- const mockPostHog = {
- capture: vi.fn(),
- identify: vi.fn(),
- setPersonProperties: vi.fn(),
- startSessionRecording: vi.fn(),
- stopSessionRecording: vi.fn(),
- isSessionReplayActive: vi.fn().mockResolvedValue(true),
- }
-
- return {
- default: vi.fn().mockImplementation(() => mockPostHog),
- }
-})
-
-describe('analytics service', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- vi.resetModules()
- })
-
- afterEach(() => {
- vi.restoreAllMocks()
- })
-
- describe('initializeAnalytics', () => {
- it('should initialize PostHog client', async () => {
- const { initializeAnalytics } = await import('../../shared/services/analytics')
-
- const client = await initializeAnalytics()
-
- expect(client).toBeDefined()
- })
-
- it('should return existing client if already initialized', async () => {
- const { initializeAnalytics } = await import('../../shared/services/analytics')
-
- const client1 = await initializeAnalytics()
- const client2 = await initializeAnalytics()
-
- expect(client1).toBe(client2)
- })
- })
-
- describe('getPostHogClient', () => {
- it('should return null before initialization', async () => {
- vi.resetModules()
- const { getPostHogClient } = await import('../../shared/services/analytics')
-
- expect(getPostHogClient()).toBeNull()
- })
-
- it('should return client after initialization', async () => {
- vi.resetModules()
- const { initializeAnalytics, getPostHogClient } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
-
- expect(getPostHogClient()).toBeDefined()
- })
- })
-
- describe('track', () => {
- it('should track event with properties', async () => {
- const { initializeAnalytics, track } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- track('test_event', { prop1: 'value1' })
-
- })
-
- it('should track event without properties', async () => {
- const { initializeAnalytics, track } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- track('test_event')
-
- })
- })
-
- describe('trackScreen', () => {
- it('should track screen view', async () => {
- const { initializeAnalytics, trackScreen } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- trackScreen('home', { source: 'tab' })
-
- })
- })
-
- describe('identifyUser', () => {
- it('should identify user with traits', async () => {
- const { initializeAnalytics, identifyUser } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- identifyUser('user-123', { name: 'Test User' })
-
- })
-
- it('should identify user without traits', async () => {
- const { initializeAnalytics, identifyUser } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- identifyUser('user-123')
-
- })
- })
-
- describe('setUserProperties', () => {
- it('should set user properties', async () => {
- const { initializeAnalytics, setUserProperties } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- setUserProperties({ plan: 'pro', workouts: 10 })
-
- })
- })
-
- describe('session recording', () => {
- it('should start session recording', async () => {
- const { initializeAnalytics, startSessionRecording } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- startSessionRecording()
-
- })
-
- it('should stop session recording', async () => {
- const { initializeAnalytics, stopSessionRecording } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- stopSessionRecording()
-
- })
-
- it('should check session replay status', async () => {
- const { initializeAnalytics, isSessionReplayActive } = await import('../../shared/services/analytics')
-
- await initializeAnalytics()
- const isActive = await isSessionReplayActive()
-
- expect(typeof isActive).toBe('boolean')
- })
- })
-})
diff --git a/src/__tests__/services/music.test.ts b/src/__tests__/services/music.test.ts
deleted file mode 100644
index dc16eac..0000000
--- a/src/__tests__/services/music.test.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import { musicService, MusicTrack } from '../../shared/services/music'
-import type { MusicVibe } from '../../shared/types'
-
-vi.mock('../../shared/supabase', () => ({
- isSupabaseConfigured: () => false,
- supabase: {},
-}))
-
-describe('music service', () => {
- const vibes: MusicVibe[] = ['electronic', 'hip-hop', 'pop', 'rock', 'chill']
-
- beforeEach(() => {
- vi.clearAllMocks()
- musicService.clearCache()
- })
-
- describe('loadTracksForVibe', () => {
- it('should return mock tracks when Supabase not configured', async () => {
- const tracks = await musicService.loadTracksForVibe('electronic')
-
- expect(tracks).toBeDefined()
- expect(tracks.length).toBeGreaterThan(0)
- })
-
- it('should return tracks for each vibe', async () => {
- for (const vibe of vibes) {
- const tracks = await musicService.loadTracksForVibe(vibe)
- expect(tracks).toBeDefined()
- expect(tracks.length).toBeGreaterThan(0)
- }
- })
-
- it('should cache tracks after first load', async () => {
- const tracks1 = await musicService.loadTracksForVibe('electronic')
- const tracks2 = await musicService.loadTracksForVibe('electronic')
-
- expect(tracks1).toBe(tracks2)
- })
-
- it('should return tracks with correct vibe property', async () => {
- for (const vibe of vibes) {
- const tracks = await musicService.loadTracksForVibe(vibe)
- tracks.forEach(track => {
- expect(track.vibe).toBe(vibe)
- })
- }
- })
-
- it('should return tracks with required properties', async () => {
- const tracks = await musicService.loadTracksForVibe('electronic')
-
- tracks.forEach(track => {
- expect(track.id).toBeDefined()
- expect(track.title).toBeDefined()
- expect(track.artist).toBeDefined()
- expect(track.duration).toBeDefined()
- expect(track.url).toBeDefined()
- expect(track.vibe).toBeDefined()
- })
- })
- })
-
- describe('clearCache', () => {
- it('should clear specific vibe cache', async () => {
- await musicService.loadTracksForVibe('electronic')
- await musicService.loadTracksForVibe('hip-hop')
-
- musicService.clearCache('electronic')
-
- const tracks = await musicService.loadTracksForVibe('hip-hop')
- expect(tracks).toBeDefined()
- })
-
- it('should clear all cache when no vibe specified', async () => {
- await musicService.loadTracksForVibe('electronic')
- await musicService.loadTracksForVibe('hip-hop')
-
- musicService.clearCache()
-
- expect(musicService).toBeDefined()
- })
- })
-
- describe('getRandomTrack', () => {
- it('should return null for empty array', () => {
- const track = musicService.getRandomTrack([])
- expect(track).toBeNull()
- })
-
- it('should return a track from the array', () => {
- const tracks = [
- { id: '1', title: 'Track 1', artist: 'Artist 1', duration: 180, url: '', vibe: 'electronic' as MusicVibe },
- { id: '2', title: 'Track 2', artist: 'Artist 2', duration: 200, url: '', vibe: 'electronic' as MusicVibe },
- ]
-
- const track = musicService.getRandomTrack(tracks)
- expect(track).not.toBeNull()
- expect(['1', '2']).toContain(track!.id)
- })
-
- it('should return the only track for single-element array', () => {
- const tracks = [
- { id: '1', title: 'Track 1', artist: 'Artist 1', duration: 180, url: '', vibe: 'electronic' as MusicVibe },
- ]
-
- const track = musicService.getRandomTrack(tracks)
- expect(track).not.toBeNull()
- expect(track!.id).toBe('1')
- })
- })
-
- describe('getNextTrack', () => {
- const tracks = [
- { id: '1', title: 'Track 1', artist: 'Artist 1', duration: 180, url: '', vibe: 'electronic' as MusicVibe },
- { id: '2', title: 'Track 2', artist: 'Artist 2', duration: 200, url: '', vibe: 'electronic' as MusicVibe },
- { id: '3', title: 'Track 3', artist: 'Artist 3', duration: 220, url: '', vibe: 'electronic' as MusicVibe },
- ]
-
- it('should return null for empty array', () => {
- const track = musicService.getNextTrack([], '1')
- expect(track).toBeNull()
- })
-
- it('should return the only track for single-element array', () => {
- const singleTrack = [tracks[0]]
- const track = musicService.getNextTrack(singleTrack, '1')
- expect(track).not.toBeNull()
- expect(track!.id).toBe('1')
- })
-
- it('should return next track in sequence', () => {
- const track = musicService.getNextTrack(tracks, '1', false)
- expect(track).not.toBeNull()
- expect(track!.id).toBe('2')
- })
-
- it('should wrap around to first track', () => {
- const track = musicService.getNextTrack(tracks, '3', false)
- expect(track).not.toBeNull()
- expect(track!.id).toBe('1')
- })
-
- it('should return random track when shuffle is true', () => {
- const track = musicService.getNextTrack(tracks, '1', true)
- expect(track).not.toBeNull()
- expect(track!.id).not.toBe('1')
- })
-
- it('should return different track when shuffling single remaining', () => {
- const twoTracks = [tracks[0], tracks[1]]
- const track = musicService.getNextTrack(twoTracks, '1', true)
- expect(track).not.toBeNull()
- expect(track!.id).toBe('2')
- })
- })
-})
diff --git a/src/__tests__/services/purchases.test.ts b/src/__tests__/services/purchases.test.ts
deleted file mode 100644
index eab86c1..0000000
--- a/src/__tests__/services/purchases.test.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
-import Purchases, { LOG_LEVEL } from 'react-native-purchases'
-
-vi.mock('react-native-purchases', () => ({
- default: {
- configure: vi.fn(),
- setLogLevel: vi.fn(),
- },
- LOG_LEVEL: {
- VERBOSE: 'VERBOSE',
- DEBUG: 'DEBUG',
- WARN: 'WARN',
- ERROR: 'ERROR',
- },
-}))
-
-describe('purchases service', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- vi.resetModules()
- })
-
- afterEach(() => {
- vi.restoreAllMocks()
- })
-
- describe('constants', () => {
- it('should export REVENUECAT_API_KEY', async () => {
- const { REVENUECAT_API_KEY } = await import('../../shared/services/purchases')
- expect(REVENUECAT_API_KEY).toBeDefined()
- expect(REVENUECAT_API_KEY).toContain('test_')
- })
-
- it('should export ENTITLEMENT_ID', async () => {
- const { ENTITLEMENT_ID } = await import('../../shared/services/purchases')
- expect(ENTITLEMENT_ID).toBeDefined()
- expect(ENTITLEMENT_ID).toBe('1000 Corp Pro')
- })
- })
-
- describe('initializePurchases', () => {
- it('should call configure with API key', async () => {
- const { initializePurchases } = await import('../../shared/services/purchases')
-
- await initializePurchases()
-
- expect(Purchases.configure).toHaveBeenCalled()
- })
-
- it('should set log level in dev mode', async () => {
- const { initializePurchases } = await import('../../shared/services/purchases')
-
- await initializePurchases()
-
- if (__DEV__) {
- expect(Purchases.setLogLevel).toHaveBeenCalledWith(LOG_LEVEL.VERBOSE)
- }
- })
-
- it('should only initialize once', async () => {
- const { initializePurchases } = await import('../../shared/services/purchases')
-
- await initializePurchases()
- await initializePurchases()
-
- expect(Purchases.configure).toHaveBeenCalledTimes(1)
- })
-
- it('should throw on configuration error', async () => {
- vi.mocked(Purchases.configure).mockRejectedValueOnce(new Error('Config failed'))
-
- vi.resetModules()
- const { initializePurchases } = await import('../../shared/services/purchases')
-
- await expect(initializePurchases()).rejects.toThrow('Config failed')
- })
- })
-
- describe('isPurchasesInitialized', () => {
- it('should return false before initialization', async () => {
- vi.resetModules()
- const { isPurchasesInitialized } = await import('../../shared/services/purchases')
-
- expect(isPurchasesInitialized()).toBe(false)
- })
-
- it('should return true after initialization', async () => {
- vi.resetModules()
- const { initializePurchases, isPurchasesInitialized } = await import('../../shared/services/purchases')
-
- await initializePurchases()
-
- expect(isPurchasesInitialized()).toBe(true)
- })
- })
-})
diff --git a/src/__tests__/services/sync.test.ts b/src/__tests__/services/sync.test.ts
deleted file mode 100644
index d787ccc..0000000
--- a/src/__tests__/services/sync.test.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import { enableSync, syncWorkoutSession, deleteSyncedData, getSyncState, hasSyncedData, isAuthenticated } from '../../shared/services/sync'
-import { supabase } from '@/src/shared/supabase'
-
-vi.mock('@/src/shared/supabase', () => ({
- supabase: {
- auth: {
- signInAnonymously: vi.fn(),
- getUser: vi.fn(),
- getSession: vi.fn(),
- signOut: vi.fn(),
- },
- from: vi.fn(),
- },
-}))
-
-describe('sync service', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- describe('enableSync', () => {
- it('should return success with userId when sync enabled', async () => {
- vi.mocked(supabase.auth.signInAnonymously).mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- error: null,
- } as any)
-
- vi.mocked(supabase.from).mockReturnValue({
- insert: vi.fn().mockResolvedValue({ error: null }),
- } as any)
-
- const profileData = {
- name: 'Test User',
- fitnessLevel: 'intermediate' as const,
- goal: 'strength' as const,
- weeklyFrequency: 3 as const,
- barriers: ['no-time'],
- onboardingCompletedAt: new Date().toISOString(),
- }
-
- const workoutHistory: Array<{
- workoutId: string
- completedAt: string
- durationSeconds: number
- caloriesBurned: number
- }> = []
-
- const result = await enableSync(profileData, workoutHistory)
-
- expect(result.success).toBe(true)
- expect(result.userId).toBe('test-user-id')
- })
-
- it('should return error when auth fails', async () => {
- vi.mocked(supabase.auth.signInAnonymously).mockResolvedValue({
- data: { user: null },
- error: { message: 'Auth failed' },
- } as any)
-
- const result = await enableSync({
- name: 'Test',
- fitnessLevel: 'beginner',
- goal: 'cardio',
- weeklyFrequency: 3,
- barriers: [],
- onboardingCompletedAt: new Date().toISOString(),
- }, [])
-
- expect(result.success).toBe(false)
- expect(result.error).toBeDefined()
- })
-
- it('should sync workout history when provided', async () => {
- vi.mocked(supabase.auth.signInAnonymously).mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- error: null,
- } as any)
-
- const mockInsert = vi.fn().mockResolvedValue({ error: null })
- vi.mocked(supabase.from).mockReturnValue({
- insert: mockInsert,
- } as any)
-
- const profileData = {
- name: 'Test User',
- fitnessLevel: 'intermediate' as const,
- goal: 'strength' as const,
- weeklyFrequency: 3 as const,
- barriers: ['no-time'],
- onboardingCompletedAt: new Date().toISOString(),
- }
-
- const workoutHistory = [
- {
- workoutId: 'workout-1',
- completedAt: new Date().toISOString(),
- durationSeconds: 240,
- caloriesBurned: 45,
- },
- ]
-
- await enableSync(profileData, workoutHistory)
-
- expect(mockInsert).toHaveBeenCalled()
- })
- })
-
- describe('syncWorkoutSession', () => {
- it('should return error when no authenticated user', async () => {
- vi.mocked(supabase.auth.getUser).mockResolvedValue({
- data: { user: null },
- } as any)
-
- const result = await syncWorkoutSession({
- workoutId: 'workout-1',
- completedAt: new Date().toISOString(),
- durationSeconds: 240,
- caloriesBurned: 45,
- })
-
- expect(result.success).toBe(false)
- expect(result.error).toBe('No authenticated user')
- })
-
- it('should sync session when user is authenticated', async () => {
- vi.mocked(supabase.auth.getUser).mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- } as any)
-
- vi.mocked(supabase.from).mockReturnValue({
- insert: vi.fn().mockResolvedValue({ error: null }),
- } as any)
-
- const result = await syncWorkoutSession({
- workoutId: 'workout-1',
- completedAt: new Date().toISOString(),
- durationSeconds: 240,
- caloriesBurned: 45,
- })
-
- expect(result.success).toBe(true)
- })
- })
-
- describe('deleteSyncedData', () => {
- it('should return error when no authenticated user', async () => {
- vi.mocked(supabase.auth.getUser).mockResolvedValue({
- data: { user: null },
- } as any)
-
- const result = await deleteSyncedData()
-
- expect(result.success).toBe(false)
- expect(result.error).toBe('No authenticated user')
- })
-
- it('should delete all user data when authenticated', async () => {
- vi.mocked(supabase.auth.getUser).mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- } as any)
-
- const mockDelete = vi.fn().mockReturnThis()
- const mockEq = vi.fn().mockResolvedValue({ error: null })
-
- vi.mocked(supabase.from).mockReturnValue({
- delete: mockDelete,
- eq: mockEq,
- } as any)
-
- vi.mocked(supabase.auth.signOut).mockResolvedValue({ error: null } as any)
-
- const result = await deleteSyncedData()
-
- expect(result.success).toBe(true)
- expect(mockDelete).toHaveBeenCalledTimes(3)
- })
- })
-
- describe('getSyncState', () => {
- it('should return never-synced when no session', async () => {
- vi.mocked(supabase.auth.getSession).mockResolvedValue({
- data: { session: null },
- } as any)
-
- const state = await getSyncState()
-
- expect(state.status).toBe('never-synced')
- expect(state.userId).toBeNull()
- })
-
- it('should return synced state with user info', async () => {
- vi.mocked(supabase.auth.getSession).mockResolvedValue({
- data: { session: { user: { id: 'test-user-id' } } },
- } as any)
-
- vi.mocked(supabase.from).mockReturnValue({
- select: vi.fn().mockReturnThis(),
- eq: vi.fn().mockReturnThis(),
- order: vi.fn().mockReturnThis(),
- limit: vi.fn().mockResolvedValue({ data: [] }),
- } as any)
-
- const state = await getSyncState()
-
- expect(state.status).toBe('synced')
- expect(state.userId).toBe('test-user-id')
- })
- })
-
- describe('hasSyncedData', () => {
- it('should return false when no session', async () => {
- vi.mocked(supabase.auth.getSession).mockResolvedValue({
- data: { session: null },
- } as any)
-
- const result = await hasSyncedData()
-
- expect(result).toBe(false)
- })
-
- it('should return true when session exists', async () => {
- vi.mocked(supabase.auth.getSession).mockResolvedValue({
- data: { session: { user: { id: 'test-user-id' } } },
- } as any)
-
- const result = await hasSyncedData()
-
- expect(result).toBe(true)
- })
- })
-
- describe('isAuthenticated', () => {
- it('should return false when no session', async () => {
- vi.mocked(supabase.auth.getSession).mockResolvedValue({
- data: { session: null },
- } as any)
-
- const result = await isAuthenticated()
-
- expect(result).toBe(false)
- })
-
- it('should return true when session exists', async () => {
- vi.mocked(supabase.auth.getSession).mockResolvedValue({
- data: { session: { user: { id: 'test-user-id' } } },
- } as any)
-
- const result = await isAuthenticated()
-
- expect(result).toBe(true)
- })
- })
-})
diff --git a/src/__tests__/setup-render.tsx b/src/__tests__/setup-render.tsx
deleted file mode 100644
index ec6b488..0000000
--- a/src/__tests__/setup-render.tsx
+++ /dev/null
@@ -1,431 +0,0 @@
-import { vi, afterEach, expect } from 'vitest'
-import React from 'react'
-import { cleanup } from '@testing-library/react-native'
-
-// Mock __DEV__
-vi.stubGlobal('__DEV__', true)
-
-// Quieter test output
-vi.stubGlobal('console', {
- ...console,
- log: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
-})
-
-afterEach(() => {
- cleanup()
- vi.clearAllMocks()
-})
-
-vi.mock('expo-blur', () => ({
- BlurView: ({ intensity, tint, children, style }: any) => {
- return React.createElement('BlurView', { intensity, tint, style, testID: 'blur-view' }, children)
- },
-}))
-
-vi.mock('expo-linear-gradient', () => ({
- LinearGradient: ({ colors, start, end, style, children }: any) => {
- return React.createElement(
- 'LinearGradient',
- { colors, start, end, style, testID: 'linear-gradient' },
- children
- )
- },
-}))
-
-vi.mock('expo-video', () => ({
- useVideoPlayer: vi.fn(() => ({
- play: vi.fn(),
- pause: vi.fn(),
- replace: vi.fn(),
- currentTime: 0,
- duration: 100,
- playing: false,
- muted: false,
- volume: 1,
- })),
- VideoView: ({ player, style, contentFit, nativeControls }: any) => {
- return React.createElement('VideoView', { player, style, contentFit, nativeControls, testID: 'video-view' })
- },
-}))
-
-vi.mock('expo-symbols', () => ({
- SymbolView: ({ name, size, tintColor, style, weight, type }: any) => {
- return React.createElement('SymbolView', { name, size, tintColor, style, weight, type, testID: `icon-${name}` })
- },
-}))
-
-vi.mock('react-native-safe-area-context', () => ({
- useSafeAreaInsets: () => ({ top: 47, bottom: 34, left: 0, right: 0 }),
- SafeAreaProvider: ({ children }: any) => children,
-}))
-
-vi.mock('expo-constants', () => ({
- default: {
- expoConfig: { version: '1.0.0' },
- systemFonts: [],
- },
-}))
-
-vi.mock('expo-haptics', () => ({
- impactAsync: vi.fn(),
- ImpactFeedbackStyle: { Light: 'light', Medium: 'medium', Heavy: 'heavy' },
- notificationAsync: vi.fn(),
- NotificationFeedbackType: { Success: 'success', Warning: 'warning', Error: 'error' },
-}))
-
-vi.mock('expo-linking', () => ({
- default: {
- openURL: vi.fn(),
- createURL: vi.fn(),
- },
-}))
-
-const mockThemeColors = {
- bg: {
- base: '#0D1B2A',
- surface: '#112240',
- elevated: '#1A3050',
- overlay1: 'rgba(168,178,216,0.06)',
- overlay2: 'rgba(168,178,216,0.10)',
- overlay3: 'rgba(168,178,216,0.15)',
- scrim: 'rgba(0,0,0,0.6)',
- },
- text: {
- primary: '#E6F1FF',
- secondary: '#A8B2D8',
- tertiary: '#8892B0',
- muted: '#8892B0',
- hint: '#8892B0',
- disabled: '#3A3A3C',
- },
- surface: {
- default: {
- backgroundColor: '#112240',
- borderColor: 'rgba(168,178,216,0.15)',
- borderWidth: 1,
- },
- accent: {
- backgroundColor: 'rgba(0,200,150,0.05)',
- borderColor: 'rgba(0,200,150,0.35)',
- borderWidth: 1.5,
- },
- tip: {
- backgroundColor: 'rgba(255,138,92,0.12)',
- borderColor: '#FF8A5C',
- borderWidth: 1,
- },
- },
- border: {
- dim: 'rgba(168,178,216,0.15)',
- hover: 'rgba(168,178,216,0.25)',
- brand: 'rgba(0,200,150,0.35)',
- },
- gradients: {
- videoOverlay: ['transparent', 'rgba(0,0,0,0.8)'],
- videoTop: ['rgba(0,0,0,0.5)', 'transparent'],
- },
- colorScheme: 'dark' as const,
- statusBarStyle: 'light' as const,
-}
-
-vi.mock('@/src/shared/theme', () => ({
- useThemeColors: () => mockThemeColors,
- ThemeProvider: ({ children }: any) => children,
-}))
-
-vi.mock('@/src/shared/constants/borderRadius', () => ({
- RADIUS: {
- NONE: 0,
- SM: 4,
- MD: 8,
- LG: 12,
- XL: 16,
- PILL: 9999,
- FULL: 9999,
- },
-}))
-
-vi.mock('@/src/shared/constants/spacing', () => ({
- SPACING: {
- 1: 4,
- 2: 8,
- 3: 12,
- 4: 16,
- 5: 20,
- 6: 24,
- 8: 32,
- },
- LAYOUT: {
- SCREEN_PADDING: 24,
- BUTTON_HEIGHT: 52,
- },
-}))
-
-vi.mock('@/src/shared/constants/animations', () => ({
- DURATION: {
- FAST: 150,
- NORMAL: 250,
- SLOW: 400,
- },
- EASE: {
- EASE_OUT: { x: 0.25, y: 0.1, x2: 0.25, y2: 1 },
- },
-}))
-
-vi.mock('react-i18next', () => ({
- useTranslation: () => ({
- t: (key: string) => key,
- i18n: { changeLanguage: vi.fn(), language: 'en' },
- }),
-}))
-
-// ---------------------------------------------------------------------------
-// Mocks from setup.ts that are needed for render tests
-// (react-native is NOT mocked here — it's provided by resolve.alias)
-// ---------------------------------------------------------------------------
-
-vi.mock('expo-av', () => ({
- Audio: {
- Sound: {
- createAsync: vi.fn().mockResolvedValue({
- sound: {
- playAsync: vi.fn(),
- pauseAsync: vi.fn(),
- stopAsync: vi.fn(),
- unloadAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- setVolumeAsync: vi.fn(),
- },
- status: { isLoaded: true },
- }),
- },
- setAudioModeAsync: vi.fn(),
- },
- Video: 'Video',
-}))
-
-vi.mock('expo-image', () => ({
- Image: 'Image',
-}))
-
-vi.mock('expo-router', () => ({
- useRouter: vi.fn(() => ({
- push: vi.fn(),
- replace: vi.fn(),
- back: vi.fn(),
- })),
- useLocalSearchParams: vi.fn(() => ({})),
- Link: 'Link',
- Stack: {
- Screen: 'Screen',
- },
- Tabs: {
- Tab: 'Tab',
- },
-}))
-
-vi.mock('expo-localization', () => ({
- getLocales: vi.fn(() => [{ languageTag: 'en-US' }]),
-}))
-
-vi.mock('expo-keep-awake', () => ({
- activateKeepAwakeAsync: vi.fn(),
- deactivateKeepAwake: vi.fn(),
-}))
-
-vi.mock('expo-notifications', () => ({
- scheduleNotificationAsync: vi.fn(),
- cancelScheduledNotificationAsync: vi.fn(),
- getAllScheduledNotificationsAsync: vi.fn().mockResolvedValue([]),
- setNotificationHandler: vi.fn(),
-}))
-
-vi.mock('expo-splash-screen', () => ({
- preventAutoHideAsync: vi.fn(),
- hideAsync: vi.fn(),
-}))
-
-vi.mock('expo-font', () => ({
- loadAsync: vi.fn(),
-}))
-
-vi.mock('expo-file-system', () => ({
- documentDirectory: '/tmp/',
- cacheDirectory: '/tmp/cache/',
- readAsStringAsync: vi.fn(),
- writeAsStringAsync: vi.fn(),
- deleteAsync: vi.fn(),
- getInfoAsync: vi.fn().mockResolvedValue({ exists: false }),
-}))
-
-vi.mock('expo-device', () => ({
- isDevice: true,
- model: 'iPhone 15',
-}))
-
-vi.mock('expo-application', () => ({
- nativeApplicationVersion: '1.0.0',
- nativeBuildVersion: '1',
-}))
-
-vi.mock('@react-native-async-storage/async-storage', () => ({
- default: {
- getItem: vi.fn().mockResolvedValue(null),
- setItem: vi.fn().mockResolvedValue(undefined),
- removeItem: vi.fn().mockResolvedValue(undefined),
- clear: vi.fn().mockResolvedValue(undefined),
- getAllKeys: vi.fn().mockResolvedValue([]),
- },
-}))
-
-vi.mock('react-native-purchases', () => ({
- default: {
- configure: vi.fn().mockResolvedValue(undefined),
- getOfferings: vi.fn().mockResolvedValue({
- current: {
- monthly: {
- identifier: 'monthly',
- product: {
- identifier: 'tabatafit.premium.monthly',
- priceString: '$9.99',
- },
- },
- annual: {
- identifier: 'annual',
- product: {
- identifier: 'tabatafit.premium.yearly',
- priceString: '$79.99',
- },
- },
- },
- }),
- getCustomerInfo: vi.fn().mockResolvedValue({
- entitlements: { active: {}, all: {} },
- activeSubscriptions: [],
- allPurchasedProductIdentifiers: [],
- }),
- purchasePackage: vi.fn().mockResolvedValue({
- customerInfo: {
- entitlements: { active: {}, all: {} },
- activeSubscriptions: [],
- },
- }),
- restorePurchases: vi.fn().mockResolvedValue({
- entitlements: { active: {}, all: {} },
- activeSubscriptions: [],
- }),
- addCustomerInfoUpdateListener: vi.fn(),
- removeCustomerInfoUpdateListener: vi.fn(),
- setLogLevel: vi.fn(),
- LOG_LEVEL: {
- VERBOSE: 'verbose',
- DEBUG: 'debug',
- INFO: 'info',
- WARN: 'warn',
- ERROR: 'error',
- },
- },
- LOG_LEVEL: {
- VERBOSE: 'verbose',
- DEBUG: 'debug',
- INFO: 'info',
- WARN: 'warn',
- ERROR: 'error',
- },
-}))
-
-vi.mock('react-native-reanimated', () => ({
- default: {
- createAnimatedComponent: (comp: any) => comp,
- View: 'View',
- Text: 'Text',
- Image: 'Image',
- ScrollView: 'ScrollView',
- },
- useAnimatedStyle: vi.fn(() => ({})),
- useSharedValue: vi.fn(() => ({ value: 0 })),
- withSpring: vi.fn((v) => v),
- withTiming: vi.fn((v) => v),
- withRepeat: vi.fn((v) => v),
- withSequence: vi.fn((...v) => v[0]),
- withDelay: vi.fn((_, v) => v),
- Easing: {
- linear: vi.fn(),
- ease: vi.fn(),
- bezier: vi.fn(),
- },
- runOnJS: vi.fn((fn) => fn),
-}))
-
-vi.mock('react-native-gesture-handler', () => ({
- Gesture: {
- Pan: vi.fn(() => ({ onStart: vi.fn(), onUpdate: vi.fn(), onEnd: vi.fn() })),
- Tap: vi.fn(() => ({ onStart: vi.fn(), onEnd: vi.fn() })),
- },
- GestureDetector: 'GestureDetector',
- PanGestureHandler: 'PanGestureHandler',
- TapGestureHandler: 'TapGestureHandler',
- State: {},
-}))
-
-vi.mock('react-native-screens', () => ({
- enableScreens: vi.fn(),
-}))
-
-vi.mock('react-native-svg', () => ({
- Svg: 'Svg',
- Path: 'Path',
- Circle: 'Circle',
- Rect: 'Rect',
- G: 'G',
- Defs: 'Defs',
- Use: 'Use',
-}))
-
-vi.mock('@/src/shared/supabase', () => ({
- supabase: {
- auth: {
- signInAnonymously: vi.fn().mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- error: null,
- }),
- getUser: vi.fn().mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- }),
- getSession: vi.fn().mockResolvedValue({
- data: { session: { user: { id: 'test-user-id' } } },
- }),
- signOut: vi.fn().mockResolvedValue({ error: null }),
- },
- from: vi.fn(() => ({
- insert: vi.fn().mockResolvedValue({ error: null }),
- select: vi.fn().mockReturnThis(),
- eq: vi.fn().mockReturnThis(),
- order: vi.fn().mockReturnThis(),
- limit: vi.fn().mockResolvedValue({ data: [], error: null }),
- delete: vi.fn().mockReturnThis(),
- update: vi.fn().mockReturnThis(),
- })),
- },
-}))
-
-vi.mock('posthog-react-native', () => ({
- PostHogProvider: ({ children }: { children: React.ReactNode }) => children,
- usePostHog: vi.fn(() => ({
- capture: vi.fn(),
- identify: vi.fn(),
- screen: vi.fn(),
- reset: vi.fn(),
- })),
-}))
-
-vi.mock('@/src/shared/i18n', () => ({
- default: {
- t: vi.fn((key: string) => key),
- changeLanguage: vi.fn(),
- language: 'en',
- },
-}))
diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts
deleted file mode 100644
index a52f5f0..0000000
--- a/src/__tests__/setup.ts
+++ /dev/null
@@ -1,406 +0,0 @@
-import { vi, afterEach } from 'vitest'
-import React from 'react'
-
-// Extend globals
-vi.stubGlobal('vi', vi)
-
-// Mock React Native core
-vi.mock('react-native', () => {
- const RN = vi.importActual('react-native')
- return {
- ...RN,
- Alert: {
- alert: vi.fn(),
- },
- Platform: {
- OS: 'ios',
- select: vi.fn((obj) => obj.ios),
- },
- StyleSheet: {
- create: (styles: any) => styles,
- hairlineWidth: 1,
- },
- Dimensions: {
- get: vi.fn(() => ({ width: 375, height: 812 })),
- addEventListener: vi.fn(() => ({ remove: vi.fn() })),
- },
- View: 'View',
- Text: 'Text',
- TouchableOpacity: 'TouchableOpacity',
- Pressable: 'Pressable',
- Image: 'Image',
- ScrollView: 'ScrollView',
- FlatList: 'FlatList',
- ActivityIndicator: 'ActivityIndicator',
- SafeAreaView: 'SafeAreaView',
- Easing: {
- linear: vi.fn((v: number) => v),
- ease: vi.fn((v: number) => v),
- bezier: vi.fn(() => vi.fn((v: number) => v)),
- quad: vi.fn((v: number) => v),
- cubic: vi.fn((v: number) => v),
- poly: vi.fn(() => vi.fn((v: number) => v)),
- sin: vi.fn((v: number) => v),
- circle: vi.fn((v: number) => v),
- exp: vi.fn((v: number) => v),
- elastic: vi.fn(() => vi.fn((v: number) => v)),
- back: vi.fn(() => vi.fn((v: number) => v)),
- bounce: vi.fn((v: number) => v),
- in: vi.fn((easing: any) => easing),
- out: vi.fn((easing: any) => easing),
- inOut: vi.fn((easing: any) => easing),
- },
- Animated: {
- Value: vi.fn(() => ({
- interpolate: vi.fn(() => 0),
- setValue: vi.fn(),
- })),
- View: 'Animated.View',
- Text: 'Animated.Text',
- Image: 'Animated.Image',
- ScrollView: 'Animated.ScrollView',
- FlatList: 'Animated.FlatList',
- createAnimatedComponent: vi.fn((comp: any) => comp),
- timing: vi.fn(() => ({ start: vi.fn() })),
- spring: vi.fn(() => ({ start: vi.fn() })),
- decay: vi.fn(() => ({ start: vi.fn() })),
- sequence: vi.fn(() => ({ start: vi.fn() })),
- parallel: vi.fn(() => ({ start: vi.fn() })),
- loop: vi.fn(() => ({ start: vi.fn() })),
- event: vi.fn(),
- add: vi.fn(),
- multiply: vi.fn(),
- diffClamp: vi.fn(),
- },
- }
-})
-
-// Mock expo modules
-vi.mock('expo-constants', () => ({
- default: {
- expoConfig: { version: '1.0.0' },
- systemFonts: [],
- },
-}))
-
-vi.mock('expo-linking', () => ({
- default: {
- openURL: vi.fn(),
- createURL: vi.fn(),
- },
-}))
-
-vi.mock('expo-haptics', () => ({
- impactAsync: vi.fn(),
- ImpactFeedbackStyle: {
- Light: 'light',
- Medium: 'medium',
- Heavy: 'heavy',
- },
- notificationAsync: vi.fn(),
- NotificationFeedbackType: {
- Success: 'success',
- Warning: 'warning',
- Error: 'error',
- },
-}))
-
-vi.mock('expo-av', () => ({
- Audio: {
- Sound: {
- createAsync: vi.fn().mockResolvedValue({
- sound: {
- playAsync: vi.fn(),
- pauseAsync: vi.fn(),
- stopAsync: vi.fn(),
- unloadAsync: vi.fn(),
- setPositionAsync: vi.fn(),
- setVolumeAsync: vi.fn(),
- },
- status: { isLoaded: true },
- }),
- },
- setAudioModeAsync: vi.fn(),
- },
- Video: 'Video',
-}))
-
-vi.mock('expo-video', () => ({
- useVideoPlayer: vi.fn(() => ({
- play: vi.fn(),
- pause: vi.fn(),
- replace: vi.fn(),
- currentTime: 0,
- duration: 100,
- playing: false,
- muted: false,
- volume: 1,
- })),
- VideoView: 'VideoView',
- VideoSource: vi.fn(),
-}))
-
-vi.mock('expo-image', () => ({
- Image: 'Image',
-}))
-
-vi.mock('expo-linear-gradient', () => ({
- LinearGradient: 'LinearGradient',
-}))
-
-vi.mock('expo-blur', () => ({
- BlurView: 'BlurView',
-}))
-
-vi.mock('expo-router', () => ({
- useRouter: vi.fn(() => ({
- push: vi.fn(),
- replace: vi.fn(),
- back: vi.fn(),
- })),
- useLocalSearchParams: vi.fn(() => ({})),
- Link: 'Link',
- Stack: {
- Screen: 'Screen',
- },
- Tabs: {
- Tab: 'Tab',
- },
-}))
-
-vi.mock('expo-localization', () => ({
- getLocales: vi.fn(() => [{ languageTag: 'en-US' }]),
-}))
-
-vi.mock('expo-keep-awake', () => ({
- activateKeepAwakeAsync: vi.fn(),
- deactivateKeepAwake: vi.fn(),
-}))
-
-vi.mock('expo-notifications', () => ({
- scheduleNotificationAsync: vi.fn(),
- cancelScheduledNotificationAsync: vi.fn(),
- getAllScheduledNotificationsAsync: vi.fn().mockResolvedValue([]),
- setNotificationHandler: vi.fn(),
-}))
-
-vi.mock('expo-splash-screen', () => ({
- preventAutoHideAsync: vi.fn(),
- hideAsync: vi.fn(),
-}))
-
-vi.mock('expo-font', () => ({
- loadAsync: vi.fn(),
-}))
-
-vi.mock('expo-file-system', () => ({
- documentDirectory: '/tmp/',
- cacheDirectory: '/tmp/cache/',
- readAsStringAsync: vi.fn(),
- writeAsStringAsync: vi.fn(),
- deleteAsync: vi.fn(),
- getInfoAsync: vi.fn().mockResolvedValue({ exists: false }),
-}))
-
-vi.mock('expo-device', () => ({
- isDevice: true,
- model: 'iPhone 15',
-}))
-
-vi.mock('expo-application', () => ({
- nativeApplicationVersion: '1.0.0',
- nativeBuildVersion: '1',
-}))
-
-// Mock AsyncStorage
-vi.mock('@react-native-async-storage/async-storage', () => ({
- default: {
- getItem: vi.fn().mockResolvedValue(null),
- setItem: vi.fn().mockResolvedValue(undefined),
- removeItem: vi.fn().mockResolvedValue(undefined),
- clear: vi.fn().mockResolvedValue(undefined),
- getAllKeys: vi.fn().mockResolvedValue([]),
- },
-}))
-
-// Mock react-native-purchases (RevenueCat)
-vi.mock('react-native-purchases', () => ({
- default: {
- configure: vi.fn().mockResolvedValue(undefined),
- getOfferings: vi.fn().mockResolvedValue({
- current: {
- monthly: {
- identifier: 'monthly',
- product: {
- identifier: 'tabatafit.premium.monthly',
- priceString: '$9.99',
- },
- },
- annual: {
- identifier: 'annual',
- product: {
- identifier: 'tabatafit.premium.yearly',
- priceString: '$79.99',
- },
- },
- },
- }),
- getCustomerInfo: vi.fn().mockResolvedValue({
- entitlements: { active: {}, all: {} },
- activeSubscriptions: [],
- allPurchasedProductIdentifiers: [],
- }),
- purchasePackage: vi.fn().mockResolvedValue({
- customerInfo: {
- entitlements: { active: {}, all: {} },
- activeSubscriptions: [],
- },
- }),
- restorePurchases: vi.fn().mockResolvedValue({
- entitlements: { active: {}, all: {} },
- activeSubscriptions: [],
- }),
- addCustomerInfoUpdateListener: vi.fn(),
- removeCustomerInfoUpdateListener: vi.fn(),
- setLogLevel: vi.fn(),
- LOG_LEVEL: {
- VERBOSE: 'verbose',
- DEBUG: 'debug',
- INFO: 'info',
- WARN: 'warn',
- ERROR: 'error',
- },
- },
- LOG_LEVEL: {
- VERBOSE: 'verbose',
- DEBUG: 'debug',
- INFO: 'info',
- WARN: 'warn',
- ERROR: 'error',
- },
-}))
-
-// Mock react-native-reanimated
-vi.mock('react-native-reanimated', () => ({
- default: {
- createAnimatedComponent: (comp: any) => comp,
- View: 'View',
- Text: 'Text',
- Image: 'Image',
- ScrollView: 'ScrollView',
- },
- useAnimatedStyle: vi.fn(() => ({})),
- useSharedValue: vi.fn(() => ({ value: 0 })),
- withSpring: vi.fn((v) => v),
- withTiming: vi.fn((v) => v),
- withRepeat: vi.fn((v) => v),
- withSequence: vi.fn((...v) => v[0]),
- withDelay: vi.fn((_, v) => v),
- Easing: {
- linear: vi.fn(),
- ease: vi.fn(),
- bezier: vi.fn(),
- },
- runOnJS: vi.fn((fn) => fn),
-}))
-
-// Mock react-native-gesture-handler
-vi.mock('react-native-gesture-handler', () => ({
- Gesture: {
- Pan: vi.fn(() => ({ onStart: vi.fn(), onUpdate: vi.fn(), onEnd: vi.fn() })),
- Tap: vi.fn(() => ({ onStart: vi.fn(), onEnd: vi.fn() })),
- },
- GestureDetector: 'GestureDetector',
- PanGestureHandler: 'PanGestureHandler',
- TapGestureHandler: 'TapGestureHandler',
- State: {},
-}))
-
-// Mock react-native-screens
-vi.mock('react-native-screens', () => ({
- enableScreens: vi.fn(),
-}))
-
-// Mock react-native-safe-area-context
-vi.mock('react-native-safe-area-context', () => ({
- SafeAreaProvider: ({ children }: { children: React.ReactNode }) => children,
- useSafeAreaInsets: vi.fn(() => ({ top: 44, bottom: 34, left: 0, right: 0 })),
- SafeAreaView: 'SafeAreaView',
-}))
-
-// Mock react-native-svg
-vi.mock('react-native-svg', () => ({
- Svg: 'Svg',
- Path: 'Path',
- Circle: 'Circle',
- Rect: 'Rect',
- G: 'G',
- Defs: 'Defs',
- Use: 'Use',
-}))
-
-// Mock Supabase
-vi.mock('@/src/shared/supabase', () => ({
- supabase: {
- auth: {
- signInAnonymously: vi.fn().mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- error: null,
- }),
- getUser: vi.fn().mockResolvedValue({
- data: { user: { id: 'test-user-id' } },
- }),
- getSession: vi.fn().mockResolvedValue({
- data: { session: { user: { id: 'test-user-id' } } },
- }),
- signOut: vi.fn().mockResolvedValue({ error: null }),
- },
- from: vi.fn(() => ({
- insert: vi.fn().mockResolvedValue({ error: null }),
- select: vi.fn().mockReturnThis(),
- eq: vi.fn().mockReturnThis(),
- order: vi.fn().mockReturnThis(),
- limit: vi.fn().mockResolvedValue({ data: [], error: null }),
- delete: vi.fn().mockReturnThis(),
- update: vi.fn().mockReturnThis(),
- })),
- },
-}))
-
-// Mock PostHog
-vi.mock('posthog-react-native', () => ({
- PostHogProvider: ({ children }: { children: React.ReactNode }) => children,
- usePostHog: vi.fn(() => ({
- capture: vi.fn(),
- identify: vi.fn(),
- screen: vi.fn(),
- reset: vi.fn(),
- })),
-}))
-
-// Mock i18next
-vi.mock('@/src/shared/i18n', () => ({
- default: {
- t: vi.fn((key: string) => key),
- changeLanguage: vi.fn(),
- language: 'en',
- },
-}))
-
-// Mock __DEV__
-vi.stubGlobal('__DEV__', true)
-
-// Mock console methods for cleaner test output
-const originalConsole = { ...console }
-vi.stubGlobal('console', {
- ...console,
- log: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
-})
-
-// Restore console after each test if needed
-afterEach(() => {
- vi.clearAllMocks()
-})
diff --git a/src/__tests__/stores/playerStore.test.ts b/src/__tests__/stores/playerStore.test.ts
deleted file mode 100644
index a846666..0000000
--- a/src/__tests__/stores/playerStore.test.ts
+++ /dev/null
@@ -1,220 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest'
-import { usePlayerStore } from '../../shared/stores/playerStore'
-import type { Workout } from '../../shared/types'
-
-const mockWorkout: Workout = {
- id: 'test-workout',
- title: 'Test Workout',
- trainerId: 'trainer-1',
- category: 'full-body',
- level: 'Beginner',
- duration: 4,
- calories: 45,
- rounds: 8,
- prepTime: 10,
- workTime: 20,
- restTime: 10,
- equipment: [],
- musicVibe: 'electronic',
- exercises: [
- { name: 'Jumping Jacks', duration: 20 },
- { name: 'Squats', duration: 20 },
- { name: 'Push-ups', duration: 20 },
- { name: 'High Knees', duration: 20 },
- ],
-}
-
-describe('playerStore', () => {
- beforeEach(() => {
- usePlayerStore.getState().reset()
- })
-
- describe('initial state', () => {
- it('should have correct default values', () => {
- const state = usePlayerStore.getState()
-
- expect(state.workout).toBeNull()
- expect(state.phase).toBe('PREP')
- expect(state.timeRemaining).toBe(10)
- expect(state.currentRound).toBe(1)
- expect(state.isPaused).toBe(false)
- expect(state.isRunning).toBe(false)
- expect(state.calories).toBe(0)
- expect(state.startedAt).toBeNull()
- })
- })
-
- describe('loadWorkout', () => {
- it('should load workout and reset all state', () => {
- const store = usePlayerStore.getState()
-
- store.loadWorkout(mockWorkout)
-
- const state = usePlayerStore.getState()
- expect(state.workout).toEqual(mockWorkout)
- expect(state.phase).toBe('PREP')
- expect(state.timeRemaining).toBe(mockWorkout.prepTime)
- expect(state.currentRound).toBe(1)
- expect(state.isPaused).toBe(false)
- expect(state.isRunning).toBe(false)
- expect(state.calories).toBe(0)
- expect(state.startedAt).toBeNull()
- })
-
- it('should use workout prepTime for timeRemaining', () => {
- const workoutWithCustomPrep: Workout = { ...mockWorkout, prepTime: 15 }
-
- usePlayerStore.getState().loadWorkout(workoutWithCustomPrep)
-
- expect(usePlayerStore.getState().timeRemaining).toBe(15)
- })
- })
-
- describe('setPhase', () => {
- it('should update phase', () => {
- const store = usePlayerStore.getState()
-
- store.setPhase('WORK')
- expect(usePlayerStore.getState().phase).toBe('WORK')
-
- store.setPhase('REST')
- expect(usePlayerStore.getState().phase).toBe('REST')
-
- store.setPhase('COMPLETE')
- expect(usePlayerStore.getState().phase).toBe('COMPLETE')
- })
- })
-
- describe('setTimeRemaining', () => {
- it('should update timeRemaining', () => {
- const store = usePlayerStore.getState()
-
- store.setTimeRemaining(5)
- expect(usePlayerStore.getState().timeRemaining).toBe(5)
-
- store.setTimeRemaining(0)
- expect(usePlayerStore.getState().timeRemaining).toBe(0)
- })
- })
-
- describe('setCurrentRound', () => {
- it('should update currentRound', () => {
- const store = usePlayerStore.getState()
-
- store.setCurrentRound(3)
- expect(usePlayerStore.getState().currentRound).toBe(3)
-
- store.setCurrentRound(8)
- expect(usePlayerStore.getState().currentRound).toBe(8)
- })
- })
-
- describe('setPaused', () => {
- it('should toggle pause state', () => {
- const store = usePlayerStore.getState()
-
- store.setPaused(true)
- expect(usePlayerStore.getState().isPaused).toBe(true)
-
- store.setPaused(false)
- expect(usePlayerStore.getState().isPaused).toBe(false)
- })
- })
-
- describe('setRunning', () => {
- it('should set running state', () => {
- const store = usePlayerStore.getState()
-
- store.setRunning(true)
- expect(usePlayerStore.getState().isRunning).toBe(true)
- })
-
- it('should set startedAt when first running', () => {
- const store = usePlayerStore.getState()
- const beforeSet = Date.now()
-
- store.setRunning(true)
-
- const state = usePlayerStore.getState()
- expect(state.startedAt).toBeGreaterThanOrEqual(beforeSet)
- expect(state.startedAt).toBeLessThanOrEqual(Date.now())
- })
-
- it('should not update startedAt if already set', () => {
- const store = usePlayerStore.getState()
-
- store.setRunning(true)
- const firstStartedAt = usePlayerStore.getState().startedAt
-
- store.setRunning(false)
- store.setRunning(true)
-
- expect(usePlayerStore.getState().startedAt).toBe(firstStartedAt)
- })
- })
-
- describe('addCalories', () => {
- it('should accumulate calories', () => {
- const store = usePlayerStore.getState()
-
- store.addCalories(5)
- expect(usePlayerStore.getState().calories).toBe(5)
-
- store.addCalories(5)
- expect(usePlayerStore.getState().calories).toBe(10)
-
- store.addCalories(3)
- expect(usePlayerStore.getState().calories).toBe(13)
- })
- })
-
- describe('reset', () => {
- it('should reset all state to defaults', () => {
- const store = usePlayerStore.getState()
-
- store.loadWorkout(mockWorkout)
- store.setPhase('WORK')
- store.setCurrentRound(5)
- store.setRunning(true)
- store.addCalories(25)
-
- store.reset()
-
- const state = usePlayerStore.getState()
- expect(state.workout).toBeNull()
- expect(state.phase).toBe('PREP')
- expect(state.timeRemaining).toBe(10)
- expect(state.currentRound).toBe(1)
- expect(state.isPaused).toBe(false)
- expect(state.isRunning).toBe(false)
- expect(state.calories).toBe(0)
- expect(state.startedAt).toBeNull()
- })
- })
-
- describe('workout phase simulation', () => {
- it('should track complete workout flow', () => {
- const store = usePlayerStore.getState()
-
- store.loadWorkout(mockWorkout)
-
- expect(store.phase).toBe('PREP')
- expect(store.timeRemaining).toBe(10)
-
- store.setPhase('WORK')
- store.setTimeRemaining(20)
- expect(usePlayerStore.getState().phase).toBe('WORK')
-
- store.setTimeRemaining(0)
- store.addCalories(6)
- store.setPhase('REST')
- store.setTimeRemaining(10)
- expect(usePlayerStore.getState().calories).toBe(6)
- expect(usePlayerStore.getState().phase).toBe('REST')
-
- store.setCurrentRound(2)
- store.setPhase('WORK')
- expect(usePlayerStore.getState().currentRound).toBe(2)
- })
- })
-})
diff --git a/src/__tests__/stores/userStore.test.ts b/src/__tests__/stores/userStore.test.ts
deleted file mode 100644
index e3fb581..0000000
--- a/src/__tests__/stores/userStore.test.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest'
-import { useUserStore } from '../../shared/stores/userStore'
-import type { FitnessLevel, FitnessGoal, WeeklyFrequency } from '../../shared/types'
-
-describe('userStore', () => {
- beforeEach(() => {
- useUserStore.setState({
- profile: {
- name: '',
- email: '',
- joinDate: 'March 2026',
- subscription: 'free',
- onboardingCompleted: false,
- fitnessLevel: 'beginner',
- goal: 'cardio',
- weeklyFrequency: 3,
- barriers: [],
- syncStatus: 'never-synced',
- supabaseUserId: null,
- savedWorkouts: [],
- },
- settings: {
- haptics: true,
- soundEffects: true,
- voiceCoaching: true,
- musicEnabled: true,
- musicVolume: 0.5,
- reminders: false,
- reminderTime: '09:00',
- hasPromptedReview: false,
- },
- })
- })
-
- describe('initial state', () => {
- it('should have correct default profile values', () => {
- const { profile } = useUserStore.getState()
-
- expect(profile.name).toBe('')
- expect(profile.email).toBe('')
- expect(profile.subscription).toBe('free')
- expect(profile.onboardingCompleted).toBe(false)
- expect(profile.fitnessLevel).toBe('beginner')
- expect(profile.goal).toBe('cardio')
- expect(profile.weeklyFrequency).toBe(3)
- expect(profile.barriers).toEqual([])
- expect(profile.syncStatus).toBe('never-synced')
- expect(profile.supabaseUserId).toBeNull()
- })
-
- it('should have correct default settings values', () => {
- const { settings } = useUserStore.getState()
-
- expect(settings.haptics).toBe(true)
- expect(settings.soundEffects).toBe(true)
- expect(settings.voiceCoaching).toBe(true)
- expect(settings.musicEnabled).toBe(true)
- expect(settings.musicVolume).toBe(0.5)
- expect(settings.reminders).toBe(false)
- expect(settings.reminderTime).toBe('09:00')
- })
- })
-
- describe('updateProfile', () => {
- it('should update profile fields', () => {
- const store = useUserStore.getState()
-
- store.updateProfile({ name: 'John Doe' })
- expect(useUserStore.getState().profile.name).toBe('John Doe')
- })
-
- it('should merge updates with existing profile', () => {
- const store = useUserStore.getState()
-
- store.updateProfile({ name: 'Jane' })
- store.updateProfile({ email: 'jane@example.com' })
-
- const profile = useUserStore.getState().profile
- expect(profile.name).toBe('Jane')
- expect(profile.email).toBe('jane@example.com')
- })
-
- it('should update fitness level', () => {
- const store = useUserStore.getState()
- const levels: FitnessLevel[] = ['beginner', 'intermediate', 'advanced']
-
- levels.forEach((level) => {
- store.updateProfile({ fitnessLevel: level })
- expect(useUserStore.getState().profile.fitnessLevel).toBe(level)
- })
- })
-
- it('should update fitness goal', () => {
- const store = useUserStore.getState()
- const goals: FitnessGoal[] = ['weight-loss', 'cardio', 'strength', 'wellness']
-
- goals.forEach((goal) => {
- store.updateProfile({ goal })
- expect(useUserStore.getState().profile.goal).toBe(goal)
- })
- })
-
- it('should update weekly frequency', () => {
- const store = useUserStore.getState()
- const frequencies: WeeklyFrequency[] = [2, 3, 5]
-
- frequencies.forEach((freq) => {
- store.updateProfile({ weeklyFrequency: freq })
- expect(useUserStore.getState().profile.weeklyFrequency).toBe(freq)
- })
- })
-
- it('should update barriers array', () => {
- const store = useUserStore.getState()
-
- store.updateProfile({ barriers: ['no-time', 'low-motivation'] })
- expect(useUserStore.getState().profile.barriers).toEqual(['no-time', 'low-motivation'])
- })
- })
-
- describe('updateSettings', () => {
- it('should update individual settings', () => {
- const store = useUserStore.getState()
-
- store.updateSettings({ haptics: false })
- expect(useUserStore.getState().settings.haptics).toBe(false)
- })
-
- it('should merge updates with existing settings', () => {
- const store = useUserStore.getState()
-
- store.updateSettings({ haptics: false })
- store.updateSettings({ soundEffects: false })
- store.updateSettings({ musicVolume: 0.8 })
-
- const settings = useUserStore.getState().settings
- expect(settings.haptics).toBe(false)
- expect(settings.soundEffects).toBe(false)
- expect(settings.musicVolume).toBe(0.8)
- expect(settings.voiceCoaching).toBe(true)
- })
-
- it('should update reminder settings', () => {
- const store = useUserStore.getState()
-
- store.updateSettings({ reminders: true, reminderTime: '07:30' })
-
- const settings = useUserStore.getState().settings
- expect(settings.reminders).toBe(true)
- expect(settings.reminderTime).toBe('07:30')
- })
- })
-
- describe('setSubscription', () => {
- it('should update subscription plan', () => {
- const store = useUserStore.getState()
-
- store.setSubscription('premium-monthly')
- expect(useUserStore.getState().profile.subscription).toBe('premium-monthly')
-
- store.setSubscription('premium-yearly')
- expect(useUserStore.getState().profile.subscription).toBe('premium-yearly')
-
- store.setSubscription('free')
- expect(useUserStore.getState().profile.subscription).toBe('free')
- })
- })
-
- describe('completeOnboarding', () => {
- it('should set onboarding completed flag', () => {
- const store = useUserStore.getState()
-
- store.completeOnboarding({
- name: 'Test User',
- fitnessLevel: 'intermediate',
- goal: 'strength',
- weeklyFrequency: 5,
- barriers: ['no-time'],
- })
-
- const profile = useUserStore.getState().profile
- expect(profile.onboardingCompleted).toBe(true)
- expect(profile.name).toBe('Test User')
- expect(profile.fitnessLevel).toBe('intermediate')
- expect(profile.goal).toBe('strength')
- expect(profile.weeklyFrequency).toBe(5)
- expect(profile.barriers).toEqual(['no-time'])
- })
- })
-
- describe('setSyncStatus', () => {
- it('should update sync status', () => {
- const store = useUserStore.getState()
-
- store.setSyncStatus('prompt-pending')
- expect(useUserStore.getState().profile.syncStatus).toBe('prompt-pending')
-
- store.setSyncStatus('synced', 'user-123')
- const profile = useUserStore.getState().profile
- expect(profile.syncStatus).toBe('synced')
- expect(profile.supabaseUserId).toBe('user-123')
- })
- })
-
- describe('setPromptPending', () => {
- it('should set sync status to prompt-pending', () => {
- const store = useUserStore.getState()
-
- store.setPromptPending()
- expect(useUserStore.getState().profile.syncStatus).toBe('prompt-pending')
- })
- })
-})
diff --git a/src/__tests__/utils/color.test.ts b/src/__tests__/utils/color.test.ts
deleted file mode 100644
index 579dab0..0000000
--- a/src/__tests__/utils/color.test.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import { withOpacity } from '../../shared/utils/color'
-
-describe('withOpacity', () => {
- it('should convert 6-digit hex with opacity', () => {
- expect(withOpacity('#FF6B35', 0.5)).toBe('rgba(255,107,53,0.5)')
- })
-
- it('should convert 3-digit hex with opacity', () => {
- expect(withOpacity('#FFF', 1)).toBe('rgba(255,255,255,1)')
- })
-
- it('should handle hex without # prefix', () => {
- expect(withOpacity('000000', 0)).toBe('rgba(0,0,0,0)')
- })
-
- it('should handle 3-digit hex without # prefix', () => {
- expect(withOpacity('F00', 0.8)).toBe('rgba(255,0,0,0.8)')
- })
-
- it('should handle pure black', () => {
- expect(withOpacity('#000000', 1)).toBe('rgba(0,0,0,1)')
- })
-
- it('should handle pure white', () => {
- expect(withOpacity('#FFFFFF', 0.12)).toBe('rgba(255,255,255,0.12)')
- })
-
- it('should handle lowercase hex', () => {
- expect(withOpacity('#ff6b35', 0.5)).toBe('rgba(255,107,53,0.5)')
- })
-})
diff --git a/src/__tests__/utils/render-utils.tsx b/src/__tests__/utils/render-utils.tsx
deleted file mode 100644
index f997a5c..0000000
--- a/src/__tests__/utils/render-utils.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { ReactElement } from 'react'
-import { render, RenderOptions } from '@testing-library/react-native'
-
-const mockThemeColors = {
- bg: {
- base: '#000000',
- surface: '#1C1C1E',
- elevated: '#2C2C2E',
- },
- text: {
- primary: '#FFFFFF',
- secondary: '#8E8E93',
- tertiary: '#636366',
- },
- glass: {
- base: {
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
- borderColor: 'rgba(255, 255, 255, 0.1)',
- borderWidth: 1,
- },
- elevated: {
- backgroundColor: 'rgba(255, 255, 255, 0.08)',
- borderColor: 'rgba(255, 255, 255, 0.12)',
- borderWidth: 1,
- },
- inset: {
- backgroundColor: 'rgba(0, 0, 0, 0.2)',
- borderColor: 'rgba(255, 255, 255, 0.05)',
- borderWidth: 1,
- },
- tinted: {
- backgroundColor: 'rgba(255, 107, 53, 0.1)',
- borderColor: 'rgba(255, 107, 53, 0.2)',
- borderWidth: 1,
- },
- blurTint: 'dark',
- blurMedium: 40,
- },
- shadow: {
- sm: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 1 },
- md: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.25, shadowRadius: 4 },
- lg: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
- },
-}
-
-interface CustomRenderOptions extends Omit {
- theme?: typeof mockThemeColors
-}
-
-export function renderWithTheme(
- ui: ReactElement,
- options: CustomRenderOptions = {}
-) {
- return render(ui, options)
-}
-
-export { mockThemeColors }
diff --git a/src/features/player/CLAUDE.md b/src/features/player/CLAUDE.md
deleted file mode 100644
index ec0a57d..0000000
--- a/src/features/player/CLAUDE.md
+++ /dev/null
@@ -1,15 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 13, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6298 | 11:39 PM | 🟣 | YouTube music system fully integrated with workout timers - database-driven architecture replaces storage buckets | ~617 |
-| #6282 | 11:06 PM | 🟣 | Music playback disabled during warm-up phase in Kine sessions | ~293 |
-| #6275 | 10:53 PM | 🔴 | React hook execution order fixed in KinePlayerScreen | ~327 |
-| #6272 | " | 🟣 | NowPlaying component integrated into KinePlayerScreen UI | ~368 |
-| #6271 | 10:52 PM | 🟣 | useMusicPlayer hook added to KinePlayerScreen for music synchronization | ~359 |
-
\ No newline at end of file
diff --git a/src/features/player/TabataPlayerScreen.tsx b/src/features/player/TabataPlayerScreen.tsx
deleted file mode 100644
index abc94cf..0000000
--- a/src/features/player/TabataPlayerScreen.tsx
+++ /dev/null
@@ -1,346 +0,0 @@
-/**
- * Tabata Player Screen
- * Handles multi-block tabata sessions with warmup, blocks, inter-block rest, cooldown
- */
-
-import React, { useRef, useEffect, useCallback, useState } from 'react'
-import {
- View, Text, StyleSheet, Pressable, Animated, StatusBar,
-} from 'react-native'
-import { useRouter } from 'expo-router'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useKeepAwake } from 'expo-keep-awake'
-import { useTranslation } from 'react-i18next'
-
-import { useTabataTimer } from '@/src/shared/hooks/useTabataTimer'
-import { useHaptics } from '@/src/shared/hooks/useHaptics'
-import { useAudio } from '@/src/shared/hooks/useAudio'
-import { useMusicPlayer } from '@/src/shared/hooks/useMusicPlayer'
-import { useProgressStore } from '@/src/shared/stores'
-import { track } from '@/src/shared/services/analytics'
-import type { TabataSession } from '@/src/shared/types/program'
-import type { WorkoutProgram } from '@/src/shared/types/workoutProgram'
-import { PHASE_COLORS } from '@/src/shared/theme'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { GREEN, NAVY, BORDER_COLORS, TEXT, PHASE, AMBER } from '@/src/shared/constants/colors'
-import { Icon } from '@/src/shared/components/Icon'
-
-import { TimerRing, PhaseIndicator, RoundIndicator, PlayerControls, StatsOverlay, CoachEncouragement, NowPlaying } from '@/src/features/player'
-import { TabataTip } from '@/src/features/player/components/TabataTip'
-import { BlockIndicator } from '@/src/features/player/components/BlockIndicator'
-import { WarmupOverlay } from '@/src/features/player/components/WarmupOverlay'
-
-// Lavender for STRETCH (COOLDOWN) phase per design spec
-const LAVENDER = '#B4A7E5'
-
-function formatTime(seconds: number) {
- const mins = Math.floor(seconds / 60)
- const secs = seconds % 60
- return `${mins}:${secs.toString().padStart(2, '0')}`
-}
-
-const TABATA_PHASE_COLORS: Record = {
- WARMUP: AMBER[500],
- WORK: PHASE.WORK,
- REST: PHASE.REST,
- INTER_BLOCK_REST: AMBER[500],
- COOLDOWN: LAVENDER,
- COMPLETE: GREEN[500],
-}
-
-interface TabataPlayerScreenProps {
- session: TabataSession
- program?: WorkoutProgram
-}
-
-export function TabataPlayerScreen({ session, program }: TabataPlayerScreenProps) {
- useKeepAwake()
- const { t } = useTranslation()
- const router = useRouter()
- const insets = useSafeAreaInsets()
- const haptics = useHaptics()
- const audio = useAudio()
- const completeProgress = useProgressStore(s => s.completeProgram)
-
- const timer = useTabataTimer(session)
- const [showControls, setShowControls] = useState(true)
- const [isMuted, setIsMuted] = useState(false)
-
- // Music muted during WARMUP/COOLDOWN (stretch) per spec
- const musicActive =
- timer.isRunning &&
- !timer.isPaused &&
- timer.phase !== 'WARMUP' &&
- timer.phase !== 'COOLDOWN'
-
- const music = useMusicPlayer({
- vibe: session.musicVibe ?? 'electronic',
- isPlaying: musicActive,
- volume: isMuted ? 0 : 0.5,
- })
- const timerScaleAnim = useRef(new Animated.Value(0.8)).current
-
- const phaseColor = TABATA_PHASE_COLORS[timer.phase] ?? PHASE.WORK
-
- // ─── Actions ─────────────────────────────────────────────────
-
- const startTimer = useCallback(() => {
- timer.start()
- haptics.buttonTap()
- track('tabata_session_started', { session_id: session.id, blocks: session.blocks.length })
- }, [timer, haptics, session])
-
- const stopWorkout = useCallback(() => {
- haptics.phaseChange()
- timer.stop()
- router.back()
- }, [router, timer, haptics])
-
- const completeWorkout = useCallback(() => {
- haptics.workoutComplete()
- track('tabata_session_completed', {
- session_id: session.id,
- calories: timer.calories,
- total_rounds: timer.totalRounds,
- blocks: timer.totalBlocks,
- })
- // Record session in unified progress store
- const programId = program?.id ?? session.id.replace(/^wp-/, '')
- completeProgress({
- programId,
- completedAt: Date.now(),
- durationSeconds: session.totalDuration * 60,
- bodyZone: program?.bodyZone ?? 'full',
- level: program?.level ?? 'Beginner',
- }).catch(() => {})
- router.replace(`/complete/${session.id}`)
- }, [router, session, timer, haptics, completeProgress, program])
-
- const handleSkip = useCallback(() => {
- timer.skip()
- haptics.selection()
- }, [timer, haptics])
-
- // ─── Animations & side-effects ───────────────────────────────
-
- useEffect(() => {
- Animated.spring(timerScaleAnim, {
- toValue: 1, friction: 6, tension: 100, useNativeDriver: true,
- }).start()
- }, [])
-
- useEffect(() => {
- timerScaleAnim.setValue(0.9)
- Animated.spring(timerScaleAnim, {
- toValue: 1, friction: 4, tension: 150, useNativeDriver: true,
- }).start()
- haptics.phaseChange()
- if (timer.phase === 'COMPLETE') {
- if (!isMuted) audio.workoutComplete()
- } else if (timer.isRunning) {
- if (!isMuted) audio.phaseStart()
- }
- }, [timer.phase])
-
- useEffect(() => {
- if (timer.isRunning && timer.timeRemaining <= 3 && timer.timeRemaining > 0) {
- if (!isMuted) audio.countdownBeep()
- haptics.countdownTick()
- }
- }, [timer.timeRemaining])
-
- // ─── Render ──────────────────────────────────────────────────
-
- const isWarmup = timer.phase === 'WARMUP'
- const isCooldown = timer.phase === 'COOLDOWN'
- const isInterBlockRest = timer.phase === 'INTER_BLOCK_REST'
- const isBlockPhase = timer.phase === 'WORK' || timer.phase === 'REST'
-
- return (
-
-
-
-
- setShowControls(s => !s)}>
- {/* Header */}
- {showControls && (
-
-
-
-
-
-
- {session.title}
- Semaine {session.week} · Séance {session.order}
-
-
-
- )}
-
- {/* Stats overlay */}
- {showControls && timer.isRunning && !timer.isComplete && !isWarmup && !isCooldown && (
-
-
-
- )}
-
- {/* Warmup/Cooldown overlay */}
- {(isWarmup || isCooldown) && timer.currentWarmupMovement && (
-
- )}
-
- {/* Inter-block rest */}
- {isInterBlockRest && (
-
- {t('screens:player.phases.trans')}
- {formatTime(timer.timeRemaining)}
-
-
- {t('screens:player.nextBlock', { num: timer.currentBlockIndex + 1 })}
-
-
- )}
-
- {/* Main timer ring for WORK/REST phases */}
- {isBlockPhase && (
- <>
-
-
-
-
-
- {formatTime(timer.timeRemaining)}
-
-
-
-
- {timer.currentExercise?.name}
-
-
-
- >
- )}
-
- {/* Complete state */}
- {timer.isComplete && (
-
- {t('screens:player.sessionComplete')}
- {t('screens:player.greatWork')}
-
-
- {timer.totalBlocks}
- {t('screens:player.blocks')}
-
-
- {timer.totalRounds}
- {t('screens:player.rounds')}
-
-
- {timer.calories}
- {t('screens:player.calories')}
-
-
-
- )}
-
- {/* Now Playing music pill */}
- {showControls && timer.isRunning && !timer.isComplete && musicActive && (
-
-
-
- )}
-
- {/* Controls */}
- {showControls && !timer.isComplete && (
-
- { timer.pause(); haptics.selection() }}
- onResume={() => { timer.resume(); haptics.selection() }}
- onStop={stopWorkout}
- onSkip={handleSkip}
- onToggleMute={() => { setIsMuted(m => !m); haptics.selection() }}
- />
-
- )}
-
- {/* Complete CTA */}
- {timer.isComplete && (
-
-
- Terminé
-
-
- )}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: NAVY[900] },
- phaseBg: { ...StyleSheet.absoluteFillObject, opacity: 0.15 },
- content: { flex: 1 },
-
- header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: SPACING[4] },
- closeBtn: { width: 44, height: 44, borderRadius: RADIUS.FULL, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', borderWidth: 1, borderColor: BORDER_COLORS.DIM, backgroundColor: NAVY[800] },
- headerCenter: { alignItems: 'center' },
- title: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY },
- subtitle: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY },
-
- statsContainer: { marginTop: SPACING[4], marginHorizontal: SPACING[4] },
-
- timerContainer: { alignItems: 'center', justifyContent: 'center', marginTop: SPACING[6] },
- timerInner: { position: 'absolute', alignItems: 'center' },
- timerTime: { ...TYPOGRAPHY.TIMER_NUMBER, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
-
- exerciseName: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY, textAlign: 'center', marginTop: SPACING[4], marginHorizontal: SPACING[4] },
-
- interBlockContainer: { alignItems: 'center', justifyContent: 'center', flex: 1 },
- interBlockLabel: { ...TYPOGRAPHY.FOOTNOTE, fontWeight: '700', letterSpacing: 2, color: AMBER[500], marginBottom: SPACING[2] },
- interBlockTime: { ...TYPOGRAPHY.TIMER_NUMBER, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
- interBlockNext: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[3] },
-
- controls: { position: 'absolute', bottom: 0, left: 0, right: 0, alignItems: 'center' },
- nowPlayingContainer: { position: 'absolute', left: SPACING[6], right: SPACING[6] },
-
- completeSection: { alignItems: 'center', marginTop: SPACING[8] },
- completeTitle: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY },
- completeSubtitle: { ...TYPOGRAPHY.TITLE_3, marginTop: SPACING[1] },
- completeStats: { flexDirection: 'row', marginTop: SPACING[6], gap: SPACING[8] },
- completeStat: { alignItems: 'center' },
- completeStatValue: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
- completeStatLabel: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[1] },
- doneButton: { width: 200, height: 56, borderRadius: RADIUS.MD, borderCurve: 'continuous', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', backgroundColor: GREEN[500] },
- doneButtonText: { ...TYPOGRAPHY.BUTTON_MEDIUM, color: NAVY[900], letterSpacing: 1 },
-})
diff --git a/src/features/player/components/BlockIndicator.tsx b/src/features/player/components/BlockIndicator.tsx
deleted file mode 100644
index 569d102..0000000
--- a/src/features/player/components/BlockIndicator.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * BlockIndicator — Shows multi-block progress (e.g., "Block 2/3")
- */
-
-import React from 'react'
-import { View, Text, StyleSheet } from 'react-native'
-import { GREEN, TEXT } from '@/src/shared/constants/colors'
-
-import { SPACING, RADIUS } from '@/src/shared/constants'
-
-interface BlockIndicatorProps {
- currentBlock: number // 0-based
- totalBlocks: number
- accentColor?: string
-}
-
-export function BlockIndicator({ currentBlock, totalBlocks, accentColor = GREEN[500] }: BlockIndicatorProps) {
- if (totalBlocks <= 1) return null
-
- return (
-
-
- Bloc {currentBlock + 1}/{totalBlocks}
-
-
- {Array.from({ length: totalBlocks }).map((_, i) => (
-
- ))}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: SPACING[2],
- },
- label: {
- fontWeight: '600',
- fontSize: 14,
- color: TEXT.SECONDARY,
- },
- dots: {
- flexDirection: 'row',
- gap: SPACING[1],
- },
- dot: {
- width: SPACING[2],
- height: SPACING[2],
- borderRadius: RADIUS.FULL,
- },
-})
diff --git a/src/features/player/components/BurnBar.tsx b/src/features/player/components/BurnBar.tsx
deleted file mode 100644
index 94ad113..0000000
--- a/src/features/player/components/BurnBar.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * BurnBar — Real-time calorie tracking vs community average
- */
-
-import React from 'react'
-import { View, Text, StyleSheet } from 'react-native'
-import { useTranslation } from 'react-i18next'
-
-import { BRAND, darkColors } from '@/src/shared/theme'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface BurnBarProps {
- currentCalories: number
- avgCalories: number
-}
-
-export function BurnBar({ currentCalories, avgCalories }: BurnBarProps) {
- const { t } = useTranslation()
- const colors = darkColors
- const percentage = Math.min((currentCalories / avgCalories) * 100, 100)
-
- return (
-
-
-
- {t('screens:player.burnBar')}
-
-
- {t('units.calUnit', { count: currentCalories })}
-
-
-
-
-
-
-
- {t('screens:player.communityAvg', { calories: avgCalories })}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {},
- header: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginBottom: SPACING[2],
- },
- label: {
- ...TYPOGRAPHY.CAPTION_1,
- },
- value: {
- ...TYPOGRAPHY.CALLOUT,
- color: BRAND.PRIMARY,
- fontWeight: '600',
- fontVariant: ['tabular-nums'],
- },
- track: {
- height: 6,
- borderRadius: RADIUS.XS,
- overflow: 'hidden',
- },
- fill: {
- height: '100%',
- backgroundColor: BRAND.PRIMARY,
- borderRadius: RADIUS.XS,
- },
- avg: {
- position: 'absolute',
- top: -2,
- width: 2,
- height: 10,
- },
- avgLabel: {
- ...TYPOGRAPHY.CAPTION_2,
- marginTop: SPACING[1],
- textAlign: 'right',
- },
-})
diff --git a/src/features/player/components/CLAUDE.md b/src/features/player/components/CLAUDE.md
deleted file mode 100644
index 8d7c5e0..0000000
--- a/src/features/player/components/CLAUDE.md
+++ /dev/null
@@ -1,23 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 9, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5997 | 10:46 AM | 🟣 | Tabata Kine programs system fully implemented with four programs and specialized UI | ~563 |
-| #5996 | 10:41 AM | 🟣 | Tabata Kine programs system implementation completed | ~460 |
-| #5973 | 9:36 AM | 🟣 | WarmupOverlay component for exercise phases | ~325 |
-| #5971 | 9:35 AM | 🟣 | KineTip component created for physiotherapist advice display | ~293 |
-
-### Apr 10, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6003 | 10:02 AM | 🔵 | WarmupOverlay Component for Exercise Phases | ~264 |
-| #6002 | " | 🔵 | BlockIndicator Component for Multi-Block Workouts | ~243 |
-| #6001 | " | 🔵 | KineTip Component Found for Physiotherapist Advice | ~235 |
-| #5998 | 9:52 AM | 🟣 | Tabata Kine programs system implementation completed | ~711 |
-
\ No newline at end of file
diff --git a/src/features/player/components/CoachEncouragement.tsx b/src/features/player/components/CoachEncouragement.tsx
deleted file mode 100644
index 324b7a5..0000000
--- a/src/features/player/components/CoachEncouragement.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * CoachEncouragement — Motivational text overlay during REST phase
- * Fades in with a subtle slide animation
- */
-
-import React, { useRef, useEffect, useState } from 'react'
-import { Text, StyleSheet, Animated } from 'react-native'
-
-import { darkColors } from '@/src/shared/theme'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { SPRING } from '@/src/shared/constants/animations'
-import { getCoachMessage } from '../constants'
-
-type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
-
-interface CoachEncouragementProps {
- phase: TimerPhase
- currentRound: number
- totalRounds: number
-}
-
-export function CoachEncouragement({
- phase,
- currentRound,
- totalRounds,
-}: CoachEncouragementProps) {
- const colors = darkColors
- const fadeAnim = useRef(new Animated.Value(0)).current
- const [message, setMessage] = useState('')
-
- useEffect(() => {
- if (phase === 'REST') {
- const msg = getCoachMessage(currentRound, totalRounds)
- setMessage(msg)
- fadeAnim.setValue(0)
- Animated.spring(fadeAnim, {
- toValue: 1,
- ...SPRING.SNAPPY,
- useNativeDriver: true,
- }).start()
- } else if (phase === 'PREP') {
- setMessage('Get ready!')
- fadeAnim.setValue(0)
- Animated.spring(fadeAnim, {
- toValue: 1,
- ...SPRING.SNAPPY,
- useNativeDriver: true,
- }).start()
- } else {
- Animated.timing(fadeAnim, {
- toValue: 0,
- duration: 200,
- useNativeDriver: true,
- }).start()
- }
- }, [phase, currentRound])
-
- if (phase !== 'REST' && phase !== 'PREP') return null
-
- return (
-
-
- “{message}”
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- paddingHorizontal: SPACING[8],
- marginTop: SPACING[3],
- },
- text: {
- ...TYPOGRAPHY.BODY,
- fontStyle: 'italic',
- textAlign: 'center',
- lineHeight: 22,
- },
-})
diff --git a/src/features/player/components/ControlButton.tsx b/src/features/player/components/ControlButton.tsx
deleted file mode 100644
index b74690d..0000000
--- a/src/features/player/components/ControlButton.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * ControlButton — Animated press button for player controls
- */
-
-import React, { useRef, useMemo } from 'react'
-import { View, Pressable, StyleSheet, Animated } from 'react-native'
-
-import { Icon, type IconName } from '@/src/shared/components/Icon'
-import { BRAND, darkColors } from '@/src/shared/theme'
-import { BRAND_DANGER } from '@/src/shared/constants/colors'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPRING } from '@/src/shared/constants/animations'
-
-interface ControlButtonProps {
- icon: IconName
- onPress: () => void
- size?: number
- variant?: 'primary' | 'secondary' | 'danger'
-}
-
-export function ControlButton({
- icon,
- onPress,
- size = 64,
- variant = 'primary',
-}: ControlButtonProps) {
- const colors = darkColors
- const scaleAnim = useRef(new Animated.Value(1)).current
-
- const handlePressIn = () => {
- Animated.spring(scaleAnim, {
- toValue: 0.9,
- ...SPRING.SNAPPY,
- useNativeDriver: true,
- }).start()
- }
-
- const handlePressOut = () => {
- Animated.spring(scaleAnim, {
- toValue: 1,
- ...SPRING.BOUNCY,
- useNativeDriver: true,
- }).start()
- }
-
- const backgroundColor =
- variant === 'primary'
- ? BRAND.PRIMARY
- : variant === 'danger'
- ? BRAND_DANGER
- : colors.border.dim
-
- return (
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- button: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- bg: {
- position: 'absolute',
- width: '100%',
- height: '100%',
- borderRadius: RADIUS.FULL,
- },
-})
diff --git a/src/features/player/components/ExerciseDisplay.tsx b/src/features/player/components/ExerciseDisplay.tsx
deleted file mode 100644
index 236d4dc..0000000
--- a/src/features/player/components/ExerciseDisplay.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * ExerciseDisplay — Shows current exercise and upcoming next exercise
- */
-
-import React, { useRef, useEffect } from 'react'
-import { View, Text, StyleSheet, Animated } from 'react-native'
-import { useTranslation } from 'react-i18next'
-
-import { BRAND, darkColors } from '@/src/shared/theme'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { SPRING } from '@/src/shared/constants/animations'
-
-interface ExerciseDisplayProps {
- exercise: string
- nextExercise?: string
-}
-
-export function ExerciseDisplay({ exercise, nextExercise }: ExerciseDisplayProps) {
- const { t } = useTranslation()
- const colors = darkColors
- const fadeAnim = useRef(new Animated.Value(0)).current
-
- // Animate in when exercise changes
- useEffect(() => {
- fadeAnim.setValue(0)
- Animated.spring(fadeAnim, {
- toValue: 1,
- ...SPRING.SNAPPY,
- useNativeDriver: true,
- }).start()
- }, [exercise])
-
- return (
-
-
- {t('screens:player.current')}
-
-
- {exercise}
-
- {nextExercise && (
-
-
- {t('screens:player.next')}
-
-
- {nextExercise}
-
-
- )}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- marginTop: SPACING[6],
- paddingHorizontal: SPACING[6],
- },
- label: {
- ...TYPOGRAPHY.CAPTION_1,
- textTransform: 'uppercase',
- letterSpacing: 1,
- },
- exercise: {
- ...TYPOGRAPHY.TITLE_1,
- textAlign: 'center',
- marginTop: SPACING[1],
- },
- nextContainer: {
- flexDirection: 'row',
- marginTop: SPACING[2],
- gap: SPACING[1],
- },
- nextLabel: {
- ...TYPOGRAPHY.BODY,
- },
- nextExercise: {
- ...TYPOGRAPHY.BODY,
- },
-})
diff --git a/src/features/player/components/NowPlaying.tsx b/src/features/player/components/NowPlaying.tsx
deleted file mode 100644
index e8531af..0000000
--- a/src/features/player/components/NowPlaying.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * NowPlaying — Floating pill showing current music track
- * Solid navy background, animated entrance, skip button
- */
-
-import React, { useRef, useEffect } from 'react'
-import { View, Text, Pressable, StyleSheet, Animated } from 'react-native'
-
-import { Icon } from '@/src/shared/components/Icon'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPRING } from '@/src/shared/constants/animations'
-import { GREEN, NAVY, BORDER_COLORS, TEXT } from '@/src/shared/constants/colors'
-import { darkColors } from '@/src/shared/theme'
-import type { MusicTrack } from '@/src/shared/services/music'
-
-interface NowPlayingProps {
- track: MusicTrack | null
- isReady: boolean
- onSkipTrack: () => void
-}
-
-export function NowPlaying({ track, isReady, onSkipTrack }: NowPlayingProps) {
- const colors = darkColors
- const slideAnim = useRef(new Animated.Value(40)).current
- const opacityAnim = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- if (track && isReady) {
- Animated.parallel([
- Animated.spring(slideAnim, {
- toValue: 0,
- ...SPRING.SNAPPY,
- useNativeDriver: true,
- }),
- Animated.timing(opacityAnim, {
- toValue: 1,
- duration: 300,
- useNativeDriver: true,
- }),
- ]).start()
- } else {
- Animated.parallel([
- Animated.timing(slideAnim, {
- toValue: 40,
- duration: 200,
- useNativeDriver: true,
- }),
- Animated.timing(opacityAnim, {
- toValue: 0,
- duration: 200,
- useNativeDriver: true,
- }),
- ]).start()
- }
- }, [track?.id, isReady])
-
- if (!track) return null
-
- return (
-
-
-
-
-
-
- {track.title}
-
-
- {track.artist}
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- borderRadius: RADIUS.FULL,
- borderCurve: 'continuous',
- overflow: 'hidden',
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- backgroundColor: NAVY[800],
- paddingVertical: SPACING[2],
- paddingHorizontal: SPACING[3],
- gap: SPACING[2],
- },
- iconContainer: {
- width: 28,
- height: 28,
- borderRadius: RADIUS.LG,
- backgroundColor: GREEN.DIM,
- alignItems: 'center',
- justifyContent: 'center',
- },
- info: {
- flex: 1,
- },
- title: {
- ...TYPOGRAPHY.CAPTION_1,
- fontWeight: '600',
- },
- artist: {
- ...TYPOGRAPHY.CAPTION_2,
- },
- skipButton: {
- width: 32,
- height: 32,
- alignItems: 'center',
- justifyContent: 'center',
- },
-})
diff --git a/src/features/player/components/PhaseIndicator.tsx b/src/features/player/components/PhaseIndicator.tsx
deleted file mode 100644
index ab902f5..0000000
--- a/src/features/player/components/PhaseIndicator.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * PhaseIndicator — Colored badge showing current timer phase
- */
-
-import React, { useMemo } from 'react'
-import { View, Text, StyleSheet } from 'react-native'
-import { useTranslation } from 'react-i18next'
-
-import { PHASE_COLORS, darkColors } from '@/src/shared/theme'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-
-type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE' | 'WARMUP' | 'COOLDOWN' | 'TRANSITION'
-
-interface PhaseIndicatorProps {
- phase: TimerPhase
-}
-
-export function PhaseIndicator({ phase }: PhaseIndicatorProps) {
- const { t } = useTranslation()
- // WARMUP/COOLDOWN/TRANSITION fall back to PREP colors if not in PHASE_COLORS map
- const phaseColor = (PHASE_COLORS as Record)[phase]?.fill ?? PHASE_COLORS.PREP.fill
- const phaseLabels: Record = {
- PREP: t('screens:player.phases.prep'),
- WORK: t('screens:player.phases.work'),
- REST: t('screens:player.phases.rest'),
- COMPLETE: t('screens:player.phases.complete'),
- WARMUP: t('screens:player.phases.warmup'),
- COOLDOWN: t('screens:player.phases.stretch'),
- TRANSITION: t('screens:player.phases.trans'),
- }
-
- return (
-
- {phaseLabels[phase]}
-
- )
-}
-
-const styles = StyleSheet.create({
- indicator: {
- paddingHorizontal: SPACING[4],
- paddingVertical: SPACING[1],
- borderRadius: RADIUS.FULL,
- marginBottom: SPACING[2],
- borderCurve: 'continuous',
- },
- text: {
- ...TYPOGRAPHY.CALLOUT,
- fontWeight: '700',
- letterSpacing: 1,
- },
-})
diff --git a/src/features/player/components/PlayerControls.tsx b/src/features/player/components/PlayerControls.tsx
deleted file mode 100644
index b5cbd05..0000000
--- a/src/features/player/components/PlayerControls.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * PlayerControls — Play/Pause/Stop/Skip/Mute control bar
- * F-072: Mute toggle accessible in 1 tap
- * F-074: Pause button (centred, accessible)
- * F-075: Quit button (DangerButton, confirmation required)
- */
-
-import React, { useCallback } from 'react'
-import { View, StyleSheet, Alert } from 'react-native'
-import { useTranslation } from 'react-i18next'
-
-import { ControlButton } from './ControlButton'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface PlayerControlsProps {
- isRunning: boolean
- isPaused: boolean
- isMuted?: boolean
- onStart: () => void
- onPause: () => void
- onResume: () => void
- onStop: () => void
- onSkip: () => void
- onToggleMute?: () => void
-}
-
-export function PlayerControls({
- isRunning,
- isPaused,
- isMuted = false,
- onStart,
- onPause,
- onResume,
- onStop,
- onSkip,
- onToggleMute,
-}: PlayerControlsProps) {
- const { t } = useTranslation()
-
- const handleQuit = useCallback(() => {
- Alert.alert(
- t('screens:player.quitTitle'),
- t('screens:player.quitMessage'),
- [
- { text: t('screens:player.quitCancel'), style: 'cancel' },
- { text: t('screens:player.quitConfirm'), style: 'destructive', onPress: onStop },
- ],
- )
- }, [onStop, t])
-
- if (!isRunning) {
- return (
-
-
-
- )
- }
-
- return (
-
-
-
- {onToggleMute && (
-
- )}
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- },
- row: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[4],
- },
-})
diff --git a/src/features/player/components/RoundIndicator.tsx b/src/features/player/components/RoundIndicator.tsx
deleted file mode 100644
index f374cc2..0000000
--- a/src/features/player/components/RoundIndicator.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * RoundIndicator — Shows current round out of total
- */
-
-import React from 'react'
-import { View, Text, StyleSheet } from 'react-native'
-import { useTranslation } from 'react-i18next'
-
-import { darkColors } from '@/src/shared/theme'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface RoundIndicatorProps {
- current: number
- total: number
-}
-
-export function RoundIndicator({ current, total }: RoundIndicatorProps) {
- const { t } = useTranslation()
- const colors = darkColors
-
- return (
-
-
- {t('screens:player.round')}{' '}
- {current}
- /{total}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- marginTop: SPACING[2],
- },
- text: {
- ...TYPOGRAPHY.BODY,
- },
- current: {
- fontWeight: '700',
- fontVariant: ['tabular-nums'],
- },
-})
diff --git a/src/features/player/components/StatsOverlay.tsx b/src/features/player/components/StatsOverlay.tsx
deleted file mode 100644
index fc63b41..0000000
--- a/src/features/player/components/StatsOverlay.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * StatsOverlay — Real-time workout stats (calories, BPM, effort)
- * Inspired by Apple Fitness+ stats row
- */
-
-import React, { useRef, useEffect } from 'react'
-import { View, Text, StyleSheet, Animated } from 'react-native'
-import { useTranslation } from 'react-i18next'
-
-import { Icon } from '@/src/shared/components/Icon'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPRING } from '@/src/shared/constants/animations'
-import { GREEN, NAVY, BORDER_COLORS, TEXT, BRAND_DANGER, AMBER } from '@/src/shared/constants/colors'
-
-interface StatsOverlayProps {
- calories: number
- heartRate: number | null
- elapsedRounds: number
- totalRounds: number
-}
-
-function StatItem({
- value,
- label,
- icon,
- iconColor,
- delay = 0,
-}: {
- value: string
- label: string
- icon: string
- iconColor: string
- delay?: number
-}) {
- const scaleAnim = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- Animated.sequence([
- Animated.delay(delay),
- Animated.spring(scaleAnim, {
- toValue: 1,
- ...SPRING.BOUNCY,
- useNativeDriver: true,
- }),
- ]).start()
- }, [delay])
-
- return (
-
-
-
- {value}
-
- {label}
-
- )
-}
-
-export function StatsOverlay({
- calories,
- heartRate,
- elapsedRounds,
- totalRounds,
-}: StatsOverlayProps) {
- const { t } = useTranslation()
- const effort = totalRounds > 0
- ? Math.round((elapsedRounds / totalRounds) * 100)
- : 0
-
- return (
-
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-evenly',
- borderRadius: RADIUS.LG,
- borderCurve: 'continuous',
- overflow: 'hidden',
- borderWidth: 1,
- borderColor: BORDER_COLORS.DIM,
- backgroundColor: NAVY[800],
- paddingVertical: SPACING[3],
- paddingHorizontal: SPACING[2],
- },
- stat: {
- alignItems: 'center',
- flex: 1,
- gap: 2,
- },
- statValue: {
- ...TYPOGRAPHY.TITLE_2,
- fontVariant: ['tabular-nums'],
- fontWeight: '700',
- },
- statLabel: {
- ...TYPOGRAPHY.CAPTION_2,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- divider: {
- width: 1,
- height: 32,
- },
-})
diff --git a/src/features/player/components/TabataTip.tsx b/src/features/player/components/TabataTip.tsx
deleted file mode 100644
index 85f376d..0000000
--- a/src/features/player/components/TabataTip.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * TabataTip — Displays physiotherapist advice during exercise
- */
-
-import React from 'react'
-import { View, Text, StyleSheet } from 'react-native'
-import { NAVY, ORANGE, TEXT } from '@/src/shared/constants/colors'
-
-import { SPACING, RADIUS, LAYOUT } from '@/src/shared/constants'
-
-interface TabataTipProps {
- tip: string
- visible: boolean
-}
-
-export function TabataTip({ tip, visible }: TabataTipProps) {
- if (!visible || !tip) return null
-
- return (
-
- 📋
- {tip}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'flex-start',
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.MD,
- padding: SPACING[3],
- marginHorizontal: LAYOUT.SCREEN_PADDING,
- gap: SPACING[2],
- borderWidth: 1,
- borderColor: ORANGE.DIM,
- },
- icon: {
- fontSize: 16,
- marginTop: SPACING[0],
- },
- tip: {
- flex: 1,
- fontSize: 13,
- fontWeight: '400',
- lineHeight: 18,
- color: TEXT.PRIMARY,
- },
-})
diff --git a/src/features/player/components/TimerRing.tsx b/src/features/player/components/TimerRing.tsx
deleted file mode 100644
index f840584..0000000
--- a/src/features/player/components/TimerRing.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * TimerRing — SVG circular progress indicator
- * Smooth animated arc that fills based on phase progress
- */
-
-import React, { useRef, useEffect, useMemo } from 'react'
-import { View, Animated, Easing, StyleSheet } from 'react-native'
-import Svg, { Circle } from 'react-native-svg'
-
-import { PHASE_COLORS, darkColors } from '@/src/shared/theme'
-import { TIMER_RING_SIZE, TIMER_RING_STROKE } from '../constants'
-
-type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
-
-const AnimatedCircle = Animated.createAnimatedComponent(Circle)
-
-interface TimerRingProps {
- progress: number
- phase: TimerPhase
- size?: number
-}
-
-export function TimerRing({
- progress,
- phase,
- size = TIMER_RING_SIZE,
-}: TimerRingProps) {
- const colors = darkColors
- const strokeWidth = TIMER_RING_STROKE
- const radius = (size - strokeWidth) / 2
- const circumference = 2 * Math.PI * radius
- const phaseColor = PHASE_COLORS[phase].fill
-
- const animatedProgress = useRef(new Animated.Value(0)).current
- const prevProgress = useRef(0)
-
- useEffect(() => {
- // If progress jumped backwards (new phase started), snap instantly
- if (progress < prevProgress.current - 0.05) {
- animatedProgress.setValue(progress)
- } else {
- Animated.timing(animatedProgress, {
- toValue: progress,
- duration: 1000,
- easing: Easing.linear,
- useNativeDriver: false,
- }).start()
- }
- prevProgress.current = progress
- }, [progress])
-
- const strokeDashoffset = animatedProgress.interpolate({
- inputRange: [0, 1],
- outputRange: [circumference, 0],
- })
-
- return (
-
-
- {/* Phase glow effect */}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- glow: {
- position: 'absolute',
- opacity: 0.06,
- zIndex: -1,
- },
-})
diff --git a/src/features/player/components/WarmupOverlay.tsx b/src/features/player/components/WarmupOverlay.tsx
deleted file mode 100644
index c8ecd82..0000000
--- a/src/features/player/components/WarmupOverlay.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * WarmupOverlay — Displays warmup/cooldown movement with countdown
- */
-
-import React from 'react'
-import { View, Text, StyleSheet } from 'react-native'
-import { PHASE, GREEN, TEXT } from '@/src/shared/constants/colors'
-
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface WarmupOverlayProps {
- movementName: string
- movementIndex: number
- totalMovements: number
- timeRemaining: number
- isCooldown?: boolean
-}
-
-export function WarmupOverlay({
- movementName,
- movementIndex,
- totalMovements,
- timeRemaining,
- isCooldown = false,
-}: WarmupOverlayProps) {
- const label = isCooldown ? 'RETOUR AU CALME' : 'ÉCHAUFFEMENT'
- const color = isCooldown ? GREEN[500] : PHASE.PREP
-
- return (
-
- {label}
- {movementIndex + 1}/{totalMovements}
- {movementName}
- {timeRemaining}s
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- paddingHorizontal: SPACING[10],
- paddingVertical: SPACING[5],
- },
- phaseLabel: {
- fontWeight: '700',
- fontSize: 14,
- letterSpacing: 2,
- marginBottom: SPACING[2],
- },
- progress: {
- fontWeight: '400',
- fontSize: 13,
- color: TEXT.TERTIARY,
- marginBottom: SPACING[4],
- },
- movement: {
- fontWeight: '600',
- fontSize: 22,
- color: TEXT.PRIMARY,
- textAlign: 'center',
- marginBottom: SPACING[3],
- },
- countdown: {
- fontWeight: '700',
- fontSize: 48,
- color: TEXT.SECONDARY,
- },
-})
diff --git a/src/features/player/constants.ts b/src/features/player/constants.ts
deleted file mode 100644
index 73171b3..0000000
--- a/src/features/player/constants.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Player-specific constants
- */
-
-export const TIMER_RING_SIZE = 280
-export const TIMER_RING_STROKE = 12
-
-/** Motivational messages shown during REST phase */
-export const COACH_MESSAGES = {
- early: [
- 'Great start! Keep it up!',
- 'Nice form! Stay strong!',
- 'You\'re warming up perfectly!',
- 'Breathe deep, stay focused!',
- ],
- mid: [
- 'Shake it out, you\'re doing great!',
- 'Halfway there! Stay strong!',
- 'You\'re crushing it!',
- 'Keep that energy up!',
- ],
- late: [
- 'Almost there! Push through!',
- 'Final stretch! Give it everything!',
- 'You\'ve got this! Don\'t stop!',
- 'Last rounds! Finish strong!',
- ],
- prep: [
- 'Get ready!',
- 'Focus your mind!',
- 'Here we go!',
- 'Breathe and prepare!',
- ],
-} as const
-
-/** Get a coach message based on round progress */
-export function getCoachMessage(currentRound: number, totalRounds: number): string {
- const progress = currentRound / totalRounds
- let pool: readonly string[]
-
- if (progress <= 0.33) {
- pool = COACH_MESSAGES.early
- } else if (progress <= 0.66) {
- pool = COACH_MESSAGES.mid
- } else {
- pool = COACH_MESSAGES.late
- }
-
- return pool[currentRound % pool.length]
-}
diff --git a/src/features/player/index.ts b/src/features/player/index.ts
deleted file mode 100644
index e0bac53..0000000
--- a/src/features/player/index.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Player feature — barrel exports
- */
-
-// Components
-export { TimerRing } from './components/TimerRing'
-export { PhaseIndicator } from './components/PhaseIndicator'
-export { ExerciseDisplay } from './components/ExerciseDisplay'
-export { RoundIndicator } from './components/RoundIndicator'
-export { ControlButton } from './components/ControlButton'
-export { PlayerControls } from './components/PlayerControls'
-export { BurnBar } from './components/BurnBar'
-export { StatsOverlay } from './components/StatsOverlay'
-export { CoachEncouragement } from './components/CoachEncouragement'
-export { NowPlaying } from './components/NowPlaying'
-
-// Constants
-export {
- TIMER_RING_SIZE,
- TIMER_RING_STROKE,
- COACH_MESSAGES,
- getCoachMessage,
-} from './constants'
diff --git a/src/features/watch/index.ts b/src/features/watch/index.ts
deleted file mode 100644
index 19d5ab8..0000000
--- a/src/features/watch/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './types';
-export { useWatchSync } from './useWatchSync';
diff --git a/src/features/watch/types.ts b/src/features/watch/types.ts
deleted file mode 100644
index 7e2820e..0000000
--- a/src/features/watch/types.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Watch Communication Types
- * Types for iPhone ↔ Watch bidirectional communication
- */
-
-export type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE';
-
-export type WatchControlAction = 'play' | 'pause' | 'skip' | 'stop' | 'previous';
-
-export interface WorkoutState {
- phase: TimerPhase;
- timeRemaining: number;
- currentRound: number;
- totalRounds: number;
- currentExercise: string;
- nextExercise?: string;
- calories: number;
- isPaused: boolean;
- isPlaying: boolean;
-}
-
-export interface WatchAvailability {
- isSupported: boolean;
- isPaired: boolean;
- isWatchAppInstalled: boolean;
- isReachable: boolean;
-}
-
-export interface WatchMessage {
- type: string;
- [key: string]: string | number | boolean | undefined;
-}
-
-export interface WatchControlMessage extends WatchMessage {
- type: 'control';
- action: WatchControlAction;
- timestamp?: number;
-}
-
-export interface WatchStateMessage extends WatchMessage {
- type: 'workoutState';
- phase: TimerPhase;
- timeRemaining: number;
- currentRound: number;
- totalRounds: number;
- currentExercise: string;
- nextExercise?: string;
- calories: number;
- isPaused: boolean;
- isPlaying: boolean;
-}
-
-export interface HeartRateMessage extends WatchMessage {
- type: 'heartRate';
- value: number;
- timestamp: number;
-}
-
-export interface WatchConnectivityStatus {
- reachable: boolean;
-}
-
-export interface WatchStateChanged {
- isPaired: boolean;
- isWatchAppInstalled: boolean;
- isReachable: boolean;
-}
-
-export type WatchEventName =
- | 'WatchConnectivityStatus'
- | 'WatchStateChanged'
- | 'WatchControlReceived'
- | 'WatchMessageReceived';
-
-export interface WatchBridgeModule {
- isWatchAvailable(): Promise;
- sendWorkoutState(state: WorkoutState): void;
- sendMessage(message: WatchMessage): void;
- sendControl(action: WatchControlAction): void;
- addListener(eventName: WatchEventName, callback: (data: Record) => void): void;
- removeListener(eventName: WatchEventName, callback: (data: Record) => void): void;
-}
diff --git a/src/features/watch/useWatchSync.ts b/src/features/watch/useWatchSync.ts
deleted file mode 100644
index 1ae1b01..0000000
--- a/src/features/watch/useWatchSync.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-import { useEffect, useRef, useCallback } from 'react';
-import {
- NativeModules,
- NativeEventEmitter,
- Platform,
-} from 'react-native';
-import { logger } from '@/src/shared/utils/logger';
-import type {
- WorkoutState,
- WatchControlAction,
- WatchAvailability,
- WatchControlMessage,
-} from './types';
-
-const { WatchBridge } = NativeModules;
-
-interface UseWatchSyncOptions {
- onPlay?: () => void;
- onPause?: () => void;
- onSkip?: () => void;
- onStop?: () => void;
- onPrevious?: () => void;
- onHeartRateUpdate?: (heartRate: number) => void;
- enabled?: boolean;
-}
-
-interface UseWatchSyncReturn {
- isAvailable: boolean;
- isReachable: boolean;
- sendWorkoutState: (state: WorkoutState) => void;
- checkAvailability: () => Promise;
-}
-
-/**
- * Hook to sync workout state with Apple Watch
- *
- * @example
- * ```tsx
- * const { isAvailable, sendWorkoutState } = useWatchSync({
- * onPlay: () => resume(),
- * onPause: () => pause(),
- * onSkip: () => skip(),
- * onStop: () => stop(),
- * });
- *
- * // Send state updates
- * useEffect(() => {
- * if (isAvailable && isRunning) {
- * sendWorkoutState({
- * phase,
- * timeRemaining,
- * currentRound,
- * totalRounds,
- * currentExercise,
- * nextExercise,
- * calories,
- * isPaused,
- * isPlaying: isRunning && !isPaused,
- * });
- * }
- * }, [phase, timeRemaining, currentRound, isPaused]);
- * ```
- */
-export function useWatchSync(options: UseWatchSyncOptions = {}): UseWatchSyncReturn {
- const {
- onPlay,
- onPause,
- onSkip,
- onStop,
- onPrevious,
- onHeartRateUpdate,
- enabled = true,
- } = options;
-
- const isAvailableRef = useRef(false);
- const isReachableRef = useRef(false);
- const eventEmitterRef = useRef(null);
-
- // Initialize event emitter
- useEffect(() => {
- if (Platform.OS !== 'ios' || !enabled) return;
-
- if (!WatchBridge) {
- logger.warn('WatchBridge native module not found');
- return;
- }
-
- eventEmitterRef.current = new NativeEventEmitter(WatchBridge);
-
- return () => {
- // Cleanup will be handled by individual subscriptions
- };
- }, [enabled]);
-
- // Listen for control events from Watch
- useEffect(() => {
- if (!eventEmitterRef.current || !enabled) return;
-
- const subscriptions: { remove(): void }[] = [];
-
- // Listen for control commands from Watch
- const controlSubscription = eventEmitterRef.current.addListener(
- 'WatchControlReceived',
- (data: { action: WatchControlAction }) => {
- logger.log('Received control from Watch:', data.action);
-
- switch (data.action) {
- case 'play':
- onPlay?.();
- break;
- case 'pause':
- onPause?.();
- break;
- case 'skip':
- onSkip?.();
- break;
- case 'stop':
- onStop?.();
- break;
- case 'previous':
- onPrevious?.();
- break;
- }
- }
- );
- subscriptions.push(controlSubscription);
-
- // Listen for connectivity status changes
- const statusSubscription = eventEmitterRef.current.addListener(
- 'WatchConnectivityStatus',
- (data: { reachable: boolean }) => {
- logger.log('Watch connectivity changed:', data.reachable);
- isReachableRef.current = data.reachable;
- }
- );
- subscriptions.push(statusSubscription);
-
- // Listen for general messages (including heart rate)
- const messageSubscription = eventEmitterRef.current.addListener(
- 'WatchMessageReceived',
- (data: { type: string; value?: number }) => {
- if (data.type === 'heartRate' && typeof data.value === 'number') {
- onHeartRateUpdate?.(data.value);
- }
- }
- );
- subscriptions.push(messageSubscription);
-
- // Listen for watch state changes
- const stateSubscription = eventEmitterRef.current.addListener(
- 'WatchStateChanged',
- (data: { isReachable: boolean; isWatchAppInstalled: boolean }) => {
- logger.log('Watch state changed:', data);
- isReachableRef.current = data.isReachable;
- isAvailableRef.current = data.isWatchAppInstalled;
- }
- );
- subscriptions.push(stateSubscription);
-
- return () => {
- subscriptions.forEach(sub => sub.remove());
- };
- }, [enabled, onPlay, onPause, onSkip, onStop, onPrevious, onHeartRateUpdate]);
-
- // Check initial availability
- useEffect(() => {
- if (Platform.OS !== 'ios' || !enabled || !WatchBridge) return;
-
- checkAvailability().catch(logger.error);
- }, [enabled]);
-
- const checkAvailability = useCallback(async (): Promise => {
- if (Platform.OS !== 'ios' || !WatchBridge) {
- return {
- isSupported: false,
- isPaired: false,
- isWatchAppInstalled: false,
- isReachable: false,
- };
- }
-
- try {
- const availability = await WatchBridge.isWatchAvailable();
- isAvailableRef.current = availability.isWatchAppInstalled;
- isReachableRef.current = availability.isReachable;
- return availability;
- } catch (error) {
- logger.error('Failed to check Watch availability:', error);
- return {
- isSupported: false,
- isPaired: false,
- isWatchAppInstalled: false,
- isReachable: false,
- };
- }
- }, []);
-
- const sendWorkoutState = useCallback((state: WorkoutState) => {
- if (Platform.OS !== 'ios' || !WatchBridge || !isReachableRef.current) {
- return;
- }
-
- try {
- WatchBridge.sendWorkoutState(state);
- } catch (error) {
- logger.error('Failed to send workout state:', error);
- }
- }, []);
-
- return {
- isAvailable: isAvailableRef.current,
- isReachable: isReachableRef.current,
- sendWorkoutState,
- checkAvailability,
- };
-}
diff --git a/src/shared/components/CLAUDE.md b/src/shared/components/CLAUDE.md
deleted file mode 100644
index 79946fb..0000000
--- a/src/shared/components/CLAUDE.md
+++ /dev/null
@@ -1,18 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 18, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #4889 | 4:46 PM | 🟣 | Created GlassCard component with iOS 18.4 inspired glassmorphism | ~174 |
-
-### Apr 11, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6108 | 7:38 PM | ✅ | WorkoutCard play button comment updated | ~233 |
-| #6106 | " | 🔵 | WorkoutCard component has hardcoded play button background | ~273 |
-
\ No newline at end of file
diff --git a/src/shared/components/DataDeletionModal.tsx b/src/shared/components/DataDeletionModal.tsx
deleted file mode 100644
index a858601..0000000
--- a/src/shared/components/DataDeletionModal.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-/**
- * Data Deletion Modal - For privacy compliance
- * Allows users to delete their synced data from Supabase
- */
-
-import { useState } from 'react'
-import { View, Modal, Pressable, StyleSheet } from 'react-native'
-import { useTranslation } from 'react-i18next'
-import { useThemeColors } from '@/src/shared/theme'
-import { StyledText } from '@/src/shared/components/StyledText'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { TEXT, RED, DARK } from '@/src/shared/constants/colors'
-import { Icon } from '@/src/shared/components/Icon'
-
-interface DataDeletionModalProps {
- visible: boolean
- onDelete: () => Promise
- onCancel: () => void
-}
-
-export function DataDeletionModal({
- visible,
- onDelete,
- onCancel,
-}: DataDeletionModalProps) {
- const { t } = useTranslation('screens')
- const colors = useThemeColors()
- const [isDeleting, setIsDeleting] = useState(false)
-
- const handleDelete = async () => {
- setIsDeleting(true)
- await onDelete()
- setIsDeleting(false)
- }
-
- return (
-
-
-
- {/* Warning Icon */}
-
-
-
-
- {/* Title */}
-
- {t('dataDeletion.title')}
-
-
- {/* Description */}
-
- {t('dataDeletion.description')}
-
-
-
- {t('dataDeletion.note')}
-
-
- {/* Actions */}
-
-
- {isDeleting ? 'Deleting...' : t('dataDeletion.deleteButton')}
-
-
-
-
-
- {t('dataDeletion.cancelButton')}
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- overlay: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- padding: SPACING[4],
- },
- container: {
- width: '100%',
- maxWidth: 360,
- borderRadius: RADIUS.XL,
- padding: SPACING[6],
- alignItems: 'center',
- },
- iconContainer: {
- width: 80,
- height: 80,
- borderRadius: RADIUS.FULL,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: SPACING[4],
- },
- title: {
- textAlign: 'center',
- marginBottom: SPACING[4],
- },
- description: {
- textAlign: 'center',
- marginBottom: SPACING[3],
- lineHeight: 22,
- },
- note: {
- textAlign: 'center',
- marginBottom: SPACING[6],
- lineHeight: 20,
- },
- deleteButton: {
- width: '100%',
- height: LAYOUT.BUTTON_HEIGHT,
- backgroundColor: RED[500],
- borderRadius: RADIUS.MD,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: SPACING[3],
- },
- cancelButton: {
- padding: SPACING[3],
- },
- disabled: {
- opacity: 0.6,
- },
-})
diff --git a/src/shared/components/GlassCard.tsx b/src/shared/components/GlassCard.tsx
deleted file mode 100644
index 029ab78..0000000
--- a/src/shared/components/GlassCard.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Card - Dark Medical surface container
- * Flat navy cards with border-dim, no glassmorphism
- */
-
-import { ReactNode, useMemo } from 'react'
-import { StyleSheet, View, ViewStyle } from 'react-native'
-
-import { useThemeColors } from '../theme'
-import type { ThemeColors } from '../theme/types'
-import { RADIUS } from '../constants/borderRadius'
-import { ORANGE } from '../constants/colors'
-
-type CardVariant = 'default' | 'accent' | 'tip'
-
-interface CardProps {
- children: ReactNode
- variant?: CardVariant
- style?: ViewStyle
-}
-
-function getVariantStyles(colors: ThemeColors): Record {
- return {
- default: {
- backgroundColor: colors.surface.default.backgroundColor,
- borderColor: colors.surface.default.borderColor,
- borderWidth: colors.surface.default.borderWidth,
- },
- accent: {
- backgroundColor: colors.surface.accent.backgroundColor,
- borderColor: colors.surface.accent.borderColor,
- borderWidth: colors.surface.accent.borderWidth,
- },
- tip: {
- backgroundColor: colors.surface.tip.backgroundColor,
- borderLeftWidth: 3,
- borderLeftColor: ORANGE[500],
- borderTopColor: colors.surface.default.borderColor,
- borderRightColor: colors.surface.default.borderColor,
- borderBottomColor: colors.surface.default.borderColor,
- borderWidth: 1,
- },
- }
-}
-
-export function Card({
- children,
- variant = 'default',
- style,
-}: CardProps) {
- const colors = useThemeColors()
- const variantStyles = useMemo(() => getVariantStyles(colors), [colors])
-
- const surfaceStyle = variantStyles[variant]
-
- return (
-
- {children}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- borderRadius: RADIUS.LG,
- borderCurve: 'continuous',
- overflow: 'hidden',
- },
-})
-
-// Preset components for common use cases
-export function CardAccent(props: Omit) {
- return
-}
-
-export function CardTip(props: Omit) {
- return
-}
-
-// Backward-compatible aliases
-export const GlassCard = Card
-export const GlassCardElevated = CardAccent
-export const GlassCardInset = Card
-export const GlassCardTinted = CardAccent
diff --git a/src/shared/components/Icon.tsx b/src/shared/components/Icon.tsx
deleted file mode 100644
index 6112cda..0000000
--- a/src/shared/components/Icon.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Icon component — wraps expo-symbols SymbolView for SF Symbols
- * Drop-in replacement for Ionicons across the app
- */
-
-import { SymbolView, type SymbolViewProps } from 'expo-symbols'
-import type { SFSymbol } from 'sf-symbols-typescript'
-import type { ColorValue, ViewStyle, StyleProp } from 'react-native'
-
-export type IconName = SFSymbol
-
-export type IconProps = {
- /** SF Symbol name (e.g. 'flame.fill', 'play.fill') */
- name: IconName
- /** Size in points */
- size?: number
- /** Tint color applied to the symbol */
- tintColor?: ColorValue
- /** Alias for tintColor (Ionicons compat) */
- color?: ColorValue
- /** Symbol weight */
- weight?: SymbolViewProps['weight']
- /** Symbol rendering type */
- type?: SymbolViewProps['type']
- /** Animation configuration */
- animationSpec?: SymbolViewProps['animationSpec']
- /** View style (margin, position, etc.) */
- style?: StyleProp
-}
-
-export function Icon({
- name,
- size = 24,
- tintColor,
- color,
- weight,
- type = 'monochrome',
- animationSpec,
- style,
-}: IconProps) {
- return (
-
- )
-}
diff --git a/src/shared/components/Mascot.tsx b/src/shared/components/Mascot.tsx
deleted file mode 100644
index 803a608..0000000
--- a/src/shared/components/Mascot.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React from 'react'
-import { StyleSheet, View, ViewStyle } from 'react-native'
-import { Image } from 'expo-image'
-import { StyledText } from './StyledText'
-import { Card } from './GlassCard'
-import { NAVY } from '../constants/colors'
-import { SPACING } from '../constants/spacing'
-
-export interface MascotProps {
- message?: string
- style?: ViewStyle
- animate?: boolean
- size?: number
-}
-
-const mascotImage = require('@/assets/mascot.gif')
-
-export function Mascot({ message, style, animate = true, size }: MascotProps) {
- return (
-
- {message && (
-
-
-
- {message}
-
-
-
-
- )}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- maxWidth: 140,
- },
- image: {
- width: 120,
- height: 120,
- },
- bubbleContainer: {
- alignItems: 'center',
- marginBottom: SPACING[2],
- position: 'relative',
- zIndex: 10,
- width: '100%',
- },
- bubble: {
- paddingHorizontal: SPACING[3],
- paddingVertical: 10,
- width: '100%',
- },
- caret: {
- position: 'absolute',
- bottom: -8,
- alignSelf: 'center',
- width: 0,
- height: 0,
- backgroundColor: 'transparent',
- borderStyle: 'solid',
- borderLeftWidth: 8,
- borderRightWidth: 8,
- borderTopWidth: 8,
- borderLeftColor: 'transparent',
- borderRightColor: 'transparent',
- borderTopColor: NAVY[800],
- },
-})
diff --git a/src/shared/components/OfflineBanner.tsx b/src/shared/components/OfflineBanner.tsx
deleted file mode 100644
index 5ddaee4..0000000
--- a/src/shared/components/OfflineBanner.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * OfflineBanner — Shows a thin banner when device is offline
- * Dark Medical: AMBER background, compact, animated
- */
-
-import { useEffect, useState, useRef } from 'react'
-import { View, StyleSheet, Animated } from 'react-native'
-import * as Network from 'expo-network'
-import { StyledText } from './StyledText'
-import { useTranslation } from 'react-i18next'
-import { AMBER, NAVY } from '@/src/shared/constants/colors'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-export function OfflineBanner() {
- const [isOffline, setIsOffline] = useState(false)
- const slideAnim = useRef(new Animated.Value(-36)).current
- const { t } = useTranslation()
-
- useEffect(() => {
- let mounted = true
-
- const check = async () => {
- try {
- const state = await Network.getNetworkStateAsync()
- if (mounted) setIsOffline(!state.isConnected || !state.isInternetReachable)
- } catch {
- // Assume online if check fails
- }
- }
-
- check()
- const interval = setInterval(check, 5000)
-
- return () => {
- mounted = false
- clearInterval(interval)
- }
- }, [])
-
- useEffect(() => {
- Animated.spring(slideAnim, {
- toValue: isOffline ? 0 : -36,
- useNativeDriver: true,
- friction: 8,
- }).start()
- }, [isOffline])
-
- return (
-
-
- {t('common:offline')}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- height: 36,
- backgroundColor: AMBER[500],
- alignItems: 'center',
- justifyContent: 'center',
- zIndex: 999,
- },
-})
diff --git a/src/shared/components/OnboardingStep.tsx b/src/shared/components/OnboardingStep.tsx
deleted file mode 100644
index 99d4bed..0000000
--- a/src/shared/components/OnboardingStep.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-/**
- * TabataFit OnboardingStep
- * Reusable wrapper for each onboarding screen — progress bar, animation, layout
- *
- * Revamped: fade-up entrance, segmented progress, generous spacing
- */
-
-import { useRef, useEffect, useMemo } from 'react'
-import { View, StyleSheet, Animated, Pressable } from 'react-native'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { Icon } from './Icon'
-import { useThemeColors } from '../theme'
-import type { ThemeColors } from '../theme/types'
-import { SPACING, LAYOUT } from '../constants/spacing'
-import { RADIUS } from '../constants/borderRadius'
-import { DURATION, EASE, SPRING } from '../constants/animations'
-import { GREEN } from '../constants/colors'
-
-interface OnboardingStepProps {
- step: number
- totalSteps: number
- children: React.ReactNode
- onBack?: () => void
-}
-
-export function OnboardingStep({ step, totalSteps, children, onBack }: OnboardingStepProps) {
- const colors = useThemeColors()
- const styles = useMemo(() => createStyles(colors), [colors])
- const insets = useSafeAreaInsets()
- const fadeAnim = useRef(new Animated.Value(0)).current
- const slideAnim = useRef(new Animated.Value(24)).current
- const progressAnims = useRef(
- Array.from({ length: totalSteps }, () => new Animated.Value(0))
- ).current
-
- useEffect(() => {
- // Reset for new step — fade up instead of slide right
- fadeAnim.setValue(0)
- slideAnim.setValue(24)
-
- Animated.parallel([
- Animated.timing(fadeAnim, {
- toValue: 1,
- duration: DURATION.NORMAL,
- easing: EASE.EASE_OUT,
- useNativeDriver: true,
- }),
- Animated.spring(slideAnim, {
- toValue: 0,
- ...SPRING.GENTLE,
- useNativeDriver: true,
- }),
- ]).start()
-
- // Animate progress segments
- progressAnims.forEach((anim, i) => {
- Animated.spring(anim, {
- toValue: i < step ? 1 : 0,
- ...SPRING.SNAPPY,
- useNativeDriver: false,
- }).start()
- })
- }, [step])
-
- return (
-
- {/* Segmented progress bar */}
-
- {progressAnims.map((anim, i) => (
-
-
-
- ))}
-
-
- {/* Back button — visible on steps 2+ */}
- {onBack && step > 1 ? (
-
-
-
- ) : (
-
- )}
-
- {/* Step content */}
-
- {children}
-
-
- )
-}
-
-function createStyles(colors: ThemeColors) {
- return StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: colors.bg.base,
- },
- progressRow: {
- flexDirection: 'row',
- gap: SPACING[1],
- marginHorizontal: LAYOUT.SCREEN_PADDING,
- },
- segmentTrack: {
- flex: 1,
- height: 4,
- backgroundColor: colors.bg.overlay2,
- borderRadius: RADIUS.PILL,
- overflow: 'hidden',
- },
- segmentFill: {
- height: '100%',
- backgroundColor: GREEN[500],
- borderRadius: RADIUS.PILL,
- },
- backButton: {
- marginTop: SPACING[3],
- marginLeft: SPACING[2],
- width: LAYOUT.TOUCH_TARGET,
- height: LAYOUT.TOUCH_TARGET,
- alignItems: 'center',
- justifyContent: 'center',
- },
- backSpacer: {
- height: SPACING[3] + LAYOUT.TOUCH_TARGET,
- },
- content: {
- flex: 1,
- paddingHorizontal: LAYOUT.SCREEN_PADDING,
- },
- })
-}
diff --git a/src/shared/components/StyledText.tsx b/src/shared/components/StyledText.tsx
deleted file mode 100644
index 950e039..0000000
--- a/src/shared/components/StyledText.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * TabataFit StyledText
- * Unified text component — uses theme for default color
- * Supports typography presets via TYPOGRAPHY tokens
- */
-
-import { Text as RNText, TextStyle, StyleProp } from 'react-native'
-import { useThemeColors } from '../theme'
-import { TYPOGRAPHY } from '../constants/typography'
-
-type FontWeight = 'regular' | 'medium' | 'semibold' | 'bold'
-
-type TypographyPreset = keyof typeof TYPOGRAPHY
-
-const WEIGHT_MAP: Record = {
- regular: '400',
- medium: '500',
- semibold: '600',
- bold: '700',
-}
-
-interface StyledTextProps {
- children: React.ReactNode
- /** Typography preset — when provided, overrides size/weight with full TYPOGRAPHY token */
- preset?: TypographyPreset
- /** Font size in px (ignored when preset is set) */
- size?: number
- /** Font weight (ignored when preset is set) */
- weight?: FontWeight
- color?: string
- style?: StyleProp
- numberOfLines?: number
-}
-
-export function StyledText({
- children,
- preset,
- size = 17,
- weight = 'regular',
- color,
- style,
- numberOfLines,
-}: StyledTextProps) {
- const colors = useThemeColors()
- const resolvedColor = color ?? colors.text.primary
-
- const baseStyle: TextStyle = preset
- ? { ...TYPOGRAPHY[preset], color: resolvedColor }
- : { fontSize: size, fontWeight: WEIGHT_MAP[weight], color: resolvedColor }
-
- return (
-
- {children}
-
- )
-}
diff --git a/src/shared/components/SyncConsentModal.tsx b/src/shared/components/SyncConsentModal.tsx
deleted file mode 100644
index 2304234..0000000
--- a/src/shared/components/SyncConsentModal.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * Sync Consent Modal - Shown after first workout completion
- * Never uses words: account, signup, login, register
- * Frames as "personalization" not "account creation"
- */
-
-import { useState } from 'react'
-import { View, Modal, Pressable, StyleSheet } from 'react-native'
-import { useTranslation } from 'react-i18next'
-import { useThemeColors } from '@/src/shared/theme'
-import { StyledText } from '@/src/shared/components/StyledText'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { BRAND, TEXT, GREEN } from '@/src/shared/constants/colors'
-import { Icon, type IconName } from '@/src/shared/components/Icon'
-
-interface SyncConsentModalProps {
- visible: boolean
- onAccept: () => void
- onDecline: () => void
-}
-
-export function SyncConsentModal({
- visible,
- onAccept,
- onDecline,
-}: SyncConsentModalProps) {
- const { t } = useTranslation('screens')
- const colors = useThemeColors()
- const [isLoading, setIsLoading] = useState(false)
-
- const handleAccept = async () => {
- setIsLoading(true)
- await onAccept()
- setIsLoading(false)
- }
-
- return (
-
-
-
- {/* Icon */}
-
-
-
-
- {/* Title */}
-
- {t('sync.title')}
-
-
- {/* Benefits */}
-
-
-
-
-
-
-
- {/* Privacy Note */}
-
- {t('sync.privacy')}
-
-
- {/* CTA Buttons */}
-
-
- {isLoading ? 'Setting up...' : t('sync.primaryButton')}
-
-
-
-
-
- {t('sync.secondaryButton')}
-
-
-
-
-
- )
-}
-
-function BenefitRow({
- icon,
- text,
- colors,
-}: {
- icon: IconName
- text: string
- colors: ReturnType
-}) {
- return (
-
-
-
- {text}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- overlay: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- padding: SPACING[4],
- },
- container: {
- width: '100%',
- maxWidth: 360,
- borderRadius: RADIUS.XL,
- padding: SPACING[6],
- alignItems: 'center',
- },
- iconContainer: {
- width: 80,
- height: 80,
- borderRadius: RADIUS.FULL,
- backgroundColor: GREEN.DIM,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: SPACING[4],
- },
- title: {
- textAlign: 'center',
- marginBottom: SPACING[6],
- },
- benefits: {
- width: '100%',
- gap: SPACING[3],
- marginBottom: SPACING[6],
- },
- benefitRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[3],
- },
- benefitText: {
- flex: 1,
- lineHeight: 22,
- },
- privacy: {
- textAlign: 'center',
- marginBottom: SPACING[6],
- lineHeight: 20,
- },
- primaryButton: {
- width: '100%',
- height: LAYOUT.BUTTON_HEIGHT,
- backgroundColor: BRAND.PRIMARY,
- borderRadius: RADIUS.MD,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: SPACING[3],
- },
- secondaryButton: {
- padding: SPACING[3],
- },
- disabled: {
- opacity: 0.6,
- },
-})
diff --git a/src/shared/components/VideoPlayer.tsx b/src/shared/components/VideoPlayer.tsx
deleted file mode 100644
index d7c16a3..0000000
--- a/src/shared/components/VideoPlayer.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * TabataFit VideoPlayer Component
- * Looping muted preview mode (detail) + full-screen background mode (player)
- * Falls back to gradient when no video URL
- */
-
-import { useRef, useEffect, useCallback } from 'react'
-import { View, StyleSheet } from 'react-native'
-import { useVideoPlayer, VideoView } from 'expo-video'
-import { LinearGradient } from 'expo-linear-gradient'
-import { BRAND, NAVY } from '../constants/colors'
-
-interface VideoPlayerProps {
- /** HLS or MP4 video URL */
- videoUrl?: string
- /** Fallback gradient colors when no video */
- gradientColors?: readonly [string, string, ...string[]]
- /** Looping muted preview (detail) or full-screen background (player) */
- mode?: 'preview' | 'background'
- /** Whether to play the video */
- isPlaying?: boolean
- style?: object
- /** Test identifier for QA automation */
- testID?: string
-}
-
-export function VideoPlayer({
- videoUrl,
- gradientColors = [BRAND.PRIMARY, BRAND.SECONDARY],
- mode = 'preview',
- isPlaying = true,
- style,
- testID,
-}: VideoPlayerProps) {
- const player = useVideoPlayer(videoUrl ?? null, (p) => {
- p.loop = true
- p.muted = mode === 'preview'
- p.volume = mode === 'background' ? 0.3 : 0
- })
-
- useEffect(() => {
- if (!player || !videoUrl) return
- if (isPlaying) {
- player.play()
- } else {
- player.pause()
- }
- }, [isPlaying, player, videoUrl])
-
- // No video URL — show gradient fallback
- if (!videoUrl) {
- return (
-
-
-
- )
- }
-
- return (
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- overflow: 'hidden',
- backgroundColor: NAVY[900],
- },
-})
diff --git a/src/shared/components/loading/CLAUDE.md b/src/shared/components/loading/CLAUDE.md
deleted file mode 100644
index f656460..0000000
--- a/src/shared/components/loading/CLAUDE.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 11, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6105 | 7:38 PM | 🔵 | Skeleton component contains hardcoded shimmer color | ~246 |
-
\ No newline at end of file
diff --git a/src/shared/components/loading/Skeleton.tsx b/src/shared/components/loading/Skeleton.tsx
deleted file mode 100644
index 5cdd10c..0000000
--- a/src/shared/components/loading/Skeleton.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-/**
- * Loading Skeleton Components
- * Shimmer loading states for better UX
- */
-
-import { View, StyleSheet, Animated, type ViewStyle, type DimensionValue } from 'react-native'
-import { useEffect, useRef } from 'react'
-import { useThemeColors } from '@/src/shared/theme'
-import { DARK } from '@/src/shared/constants/colors'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-
-interface SkeletonProps {
- width?: DimensionValue
- height?: number
- borderRadius?: number
- style?: ViewStyle
-}
-
-/**
- * Shimmer Skeleton Component
- */
-export function Skeleton({ width = '100%', height = 20, borderRadius = RADIUS.MD, style }: SkeletonProps) {
- const colors = useThemeColors()
- const shimmerValue = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- const shimmer = Animated.loop(
- Animated.sequence([
- Animated.timing(shimmerValue, {
- toValue: 1,
- duration: 1500,
- useNativeDriver: true,
- }),
- Animated.timing(shimmerValue, {
- toValue: 0,
- duration: 1500,
- useNativeDriver: true,
- }),
- ])
- )
- shimmer.start()
- return () => shimmer.stop()
- }, [])
-
- const translateX = shimmerValue.interpolate({
- inputRange: [0, 1],
- outputRange: [-200, 200],
- })
-
- return (
-
-
-
- )
-}
-
-/**
- * Workout Card Skeleton
- */
-export function WorkoutCardSkeleton() {
- const colors = useThemeColors()
-
- return (
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-/**
- * Trainer Card Skeleton
- */
-export function TrainerCardSkeleton() {
- const colors = useThemeColors()
-
- return (
-
-
-
-
-
-
-
- )
-}
-
-/**
- * Collection Card Skeleton
- */
-export function CollectionCardSkeleton() {
- return (
-
-
-
-
- )
-}
-
-/**
- * Stats Card Skeleton
- */
-export function StatsCardSkeleton() {
- const colors = useThemeColors()
-
- return (
-
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- skeleton: {
- overflow: 'hidden',
- },
- shimmer: {
- ...StyleSheet.absoluteFillObject,
- backgroundColor: DARK.OVERLAY_2,
- width: 100,
- },
- card: {
- borderRadius: RADIUS.LG,
- overflow: 'hidden',
- marginBottom: SPACING[4],
- },
- cardContent: {
- padding: SPACING[4],
- gap: SPACING[2],
- },
- row: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: SPACING[2],
- },
- trainerCard: {
- flexDirection: 'row',
- alignItems: 'center',
- padding: SPACING[4],
- borderRadius: RADIUS.LG,
- marginBottom: SPACING[3],
- },
- trainerInfo: {
- marginLeft: SPACING[4],
- flex: 1,
- gap: SPACING[2],
- },
- collectionCard: {
- alignItems: 'center',
- padding: SPACING[4],
- },
- statsCard: {
- padding: SPACING[4],
- borderRadius: RADIUS.LG,
- minWidth: 140,
- },
- statsHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
-})
\ No newline at end of file
diff --git a/src/shared/components/native/CLAUDE.md b/src/shared/components/native/CLAUDE.md
deleted file mode 100644
index af17083..0000000
--- a/src/shared/components/native/CLAUDE.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 11, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6116 | 7:39 PM | 🔴 | NativeSection missing NAVY color import added | ~239 |
-
\ No newline at end of file
diff --git a/src/shared/components/native/NativeButton.tsx b/src/shared/components/native/NativeButton.tsx
deleted file mode 100644
index 316059d..0000000
--- a/src/shared/components/native/NativeButton.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import { Pressable, StyleSheet, Text, View, type ViewStyle, type TextStyle } from 'react-native'
-import { Image } from 'expo-image'
-import type { SFSymbol } from 'sf-symbols-typescript'
-import { GREEN, NAVY, RED, TEXT, BRAND } from '@/src/shared/constants/colors'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-
-/**
- * SwiftUI-inspired button variants:
- * - borderedProminent: filled green (primary CTA)
- * - bordered: tinted background with colored text
- * - borderless: text-only (inline actions)
- * - destructive: red text action
- * - plain: minimal, no chrome
- */
-type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive' | 'icon'
- | 'borderedProminent' | 'bordered' | 'borderless' | 'plain'
-
-type ControlSize = 'mini' | 'small' | 'regular' | 'large' | 'extraLarge'
-
-export interface NativeButtonProps {
- variant?: ButtonVariant
- title?: string
- systemImage?: SFSymbol
- onPress?: () => void
- disabled?: boolean
- color?: string
- controlSize?: ControlSize
- fullWidth?: boolean
- style?: ViewStyle
- testID?: string
-}
-
-export function NativeButton(props: NativeButtonProps) {
- const {
- variant = 'primary',
- title,
- systemImage,
- onPress,
- disabled,
- color,
- controlSize = 'regular',
- fullWidth,
- style,
- testID,
- } = props
-
- const sizeStyle = SIZE_STYLES[controlSize]
- const buttonStyle = getVariantStyle(variant, color)
- const textStyle = getVariantTextStyle(variant, color)
-
- return (
- [
- styles.base,
- sizeStyle.container,
- fullWidth && styles.fullWidth,
- buttonStyle,
- pressed && styles.pressed,
- disabled && styles.disabled,
- { borderCurve: 'continuous' } as ViewStyle,
- style,
- ]}
- testID={testID}
- >
-
- {systemImage ? (
-
- ) : null}
- {title ? (
- {title}
- ) : null}
-
-
- )
-}
-
-function getVariantStyle(variant: ButtonVariant, color?: string): ViewStyle {
- const brandColor = color ?? GREEN[500]
- switch (variant) {
- case 'primary':
- case 'borderedProminent':
- return { backgroundColor: brandColor }
- case 'secondary':
- case 'bordered':
- return { backgroundColor: `${brandColor}18` } // 10% opacity tint
- case 'ghost':
- case 'borderless':
- case 'plain':
- return { backgroundColor: 'transparent' }
- case 'destructive':
- return { backgroundColor: 'transparent' }
- case 'icon':
- return { backgroundColor: 'transparent' }
- }
-}
-
-function getVariantTextStyle(variant: ButtonVariant, color?: string): TextStyle {
- const brandColor = color ?? GREEN[500]
- switch (variant) {
- case 'primary':
- case 'borderedProminent':
- return { color: NAVY[900] }
- case 'secondary':
- case 'bordered':
- return { color: brandColor }
- case 'destructive':
- return { color: RED[500] }
- case 'ghost':
- case 'borderless':
- return { color: brandColor }
- case 'plain':
- return { color: TEXT.PRIMARY }
- default:
- return { color: TEXT.PRIMARY }
- }
-}
-
-const SIZE_STYLES: Record = {
- mini: {
- container: { height: 28, paddingHorizontal: SPACING[2], borderRadius: RADIUS.SM },
- text: { fontSize: 13 },
- },
- small: {
- container: { height: 34, paddingHorizontal: SPACING[3], borderRadius: RADIUS.SM },
- text: { fontSize: 14 },
- },
- regular: {
- container: { height: 44, paddingHorizontal: SPACING[4], borderRadius: RADIUS.MD },
- text: { fontSize: 17 },
- },
- large: {
- container: { height: 50, paddingHorizontal: SPACING[5], borderRadius: RADIUS.MD },
- text: { fontSize: 17 },
- },
- extraLarge: {
- container: { height: LAYOUT.BUTTON_HEIGHT, paddingHorizontal: SPACING[6], borderRadius: RADIUS.MD },
- text: { fontSize: 17 },
- },
-}
-
-const styles = StyleSheet.create({
- base: {
- alignItems: 'center',
- justifyContent: 'center',
- minHeight: LAYOUT.TOUCH_TARGET,
- },
- content: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[2],
- },
- fullWidth: {
- width: '100%',
- },
- icon: {
- width: 18,
- height: 18,
- },
- text: {
- fontWeight: '600',
- textAlign: 'center',
- },
- pressed: {
- opacity: 0.7,
- transform: [{ scale: 0.98 }],
- },
- disabled: {
- opacity: 0.35,
- },
-})
diff --git a/src/shared/components/native/NativeGauge.tsx b/src/shared/components/native/NativeGauge.tsx
deleted file mode 100644
index 9d73caf..0000000
--- a/src/shared/components/native/NativeGauge.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { StyleSheet, View } from 'react-native'
-import { GREEN, TEXT } from '@/src/shared/constants/colors'
-
-export interface NativeGaugeProps {
- value: number
- maxValue?: number
- label?: string
- color?: string
- size?: number
- testID?: string
-}
-
-export function NativeGauge(props: NativeGaugeProps) {
- const { value, maxValue = 1, label, color, size = 52 } = props
- const percentage = Math.round((value / maxValue) * 100)
- const gaugeColor = color ?? GREEN[500]
-
- return (
-
-
-
-
-
-
- {label ? : null}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- ring: {
- width: '100%',
- height: '100%',
- borderRadius: 9999,
- borderWidth: 4,
- alignItems: 'center',
- justifyContent: 'center',
- },
- valueText: {},
- labelContainer: {
- marginTop: 4,
- },
-})
diff --git a/src/shared/components/native/NativeLabeledRow.tsx b/src/shared/components/native/NativeLabeledRow.tsx
deleted file mode 100644
index 7661583..0000000
--- a/src/shared/components/native/NativeLabeledRow.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { StyleSheet, Text, View, Pressable } from 'react-native'
-import { Image } from 'expo-image'
-import { BRAND, TEXT } from '@/src/shared/constants/colors'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-export interface NativeLabeledRowProps {
- label: string
- value?: string
- /** SF Symbol name for expo-image source="sf:name" */
- icon?: string
- /** Icon tint color */
- iconColor?: string
- chevron?: boolean
- onPress?: () => void
- /** Destructive row — label turns red */
- destructive?: boolean
- children?: React.ReactNode
- testID?: string
-}
-
-export function NativeLabeledRow(props: NativeLabeledRowProps) {
- const {
- label,
- value,
- icon,
- iconColor,
- chevron,
- onPress,
- destructive,
- children,
- } = props
-
- const labelColor = destructive ? BRAND.DANGER : TEXT.PRIMARY
-
- const content = (
-
- {icon ? (
-
- ) : null}
- {label}
-
- {value ? {value} : null}
- {children}
- {(chevron || onPress) ? (
-
- ) : null}
-
-
- )
-
- if (onPress) {
- return (
- pressed ? styles.pressed : undefined}
- >
- {content}
-
- )
- }
-
- return content
-}
-
-const styles = StyleSheet.create({
- row: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- minHeight: 44,
- paddingHorizontal: SPACING[4],
- paddingVertical: SPACING[3],
- },
- icon: {
- width: 22,
- height: 22,
- marginRight: SPACING[3],
- },
- label: {
- fontSize: 17,
- fontWeight: '400',
- flex: 1,
- },
- right: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[2],
- },
- value: {
- fontSize: 17,
- fontWeight: '400',
- color: TEXT.TERTIARY,
- },
- chevronIcon: {
- width: 13,
- height: 13,
- tintColor: TEXT.TERTIARY,
- opacity: 0.6,
- },
- pressed: {
- opacity: 0.7,
- },
-})
diff --git a/src/shared/components/native/NativeList.tsx b/src/shared/components/native/NativeList.tsx
deleted file mode 100644
index da5f4ac..0000000
--- a/src/shared/components/native/NativeList.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { type ViewStyle, View, StyleSheet } from 'react-native'
-import { NAVY, BORDER_COLORS } from '@/src/shared/constants/colors'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-
-export interface NativeListProps {
- children: React.ReactNode
- style?: ViewStyle
- scrollEnabled?: boolean
- height?: number
- /** Remove default margin (for embedding inside other containers) */
- inset?: boolean
- testID?: string
-}
-
-export function NativeList(props: NativeListProps) {
- const { children, style, height, inset = true } = props
- return (
-
- {children}
-
- )
-}
-
-export function calculateListHeight(rows: number, sections: number = 1): number {
- return rows * LAYOUT.LIST_ROW_HEIGHT + sections * LAYOUT.LIST_HEADER_HEIGHT + 20
-}
-
-/** Thin iOS-style separator for use between list rows */
-export function ListSeparator() {
- return (
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- list: {
- marginHorizontal: LAYOUT.SCREEN_PADDING,
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.LG,
- borderCurve: 'continuous',
- overflow: 'hidden',
- },
- noInset: {
- marginHorizontal: 0,
- },
- separatorOuter: {
- paddingLeft: SPACING[4],
- },
- separator: {
- height: StyleSheet.hairlineWidth,
- backgroundColor: BORDER_COLORS.SEPARATOR,
- },
-})
diff --git a/src/shared/components/native/NativeSection.tsx b/src/shared/components/native/NativeSection.tsx
deleted file mode 100644
index 5c8a84a..0000000
--- a/src/shared/components/native/NativeSection.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { StyleSheet, Text, View } from 'react-native'
-import { NAVY, TEXT } from '@/src/shared/constants/colors'
-import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-
-export interface NativeSectionProps {
- title?: string
- footer?: string
- children: React.ReactNode
-}
-
-export function NativeSection(props: NativeSectionProps) {
- const { title, footer, children } = props
- return (
-
- {title ? (
- {title.toUpperCase()}
- ) : null}
-
- {children}
-
- {footer ? (
- {footer}
- ) : null}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- marginTop: SPACING[5],
- },
- header: {
- fontSize: 13,
- fontWeight: '400',
- color: TEXT.TERTIARY,
- letterSpacing: -0.08,
- textTransform: 'uppercase',
- paddingHorizontal: LAYOUT.SCREEN_PADDING + LAYOUT.CARD_PADDING,
- marginBottom: SPACING[2],
- },
- content: {
- marginHorizontal: LAYOUT.SCREEN_PADDING,
- backgroundColor: NAVY[800],
- borderRadius: RADIUS.LG,
- overflow: 'hidden',
- },
- footer: {
- fontSize: 13,
- fontWeight: '400',
- color: TEXT.TERTIARY,
- letterSpacing: -0.08,
- paddingHorizontal: LAYOUT.SCREEN_PADDING + LAYOUT.CARD_PADDING,
- marginTop: SPACING[2],
- lineHeight: 18,
- },
-})
diff --git a/src/shared/components/native/NativeSwitch.tsx b/src/shared/components/native/NativeSwitch.tsx
deleted file mode 100644
index 62087a6..0000000
--- a/src/shared/components/native/NativeSwitch.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Switch as RNSwitch, type ViewStyle } from 'react-native'
-import { GREEN, TEXT } from '@/src/shared/constants/colors'
-
-export interface NativeSwitchProps {
- value: boolean
- onValueChange?: (value: boolean) => void
- color?: string
- disabled?: boolean
- style?: ViewStyle
- testID?: string
-}
-
-export function NativeSwitch(props: NativeSwitchProps) {
- const { value, onValueChange, color, disabled, style } = props
- return (
-
- )
-}
diff --git a/src/shared/components/native/index.ts b/src/shared/components/native/index.ts
deleted file mode 100644
index 233d45a..0000000
--- a/src/shared/components/native/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export { NativeButton } from './NativeButton'
-export type { NativeButtonProps } from './NativeButton'
-
-export { NativeList, calculateListHeight, ListSeparator } from './NativeList'
-export type { NativeListProps } from './NativeList'
-
-export { NativeSection } from './NativeSection'
-export type { NativeSectionProps } from './NativeSection'
-
-export { NativeSwitch } from './NativeSwitch'
-export type { NativeSwitchProps } from './NativeSwitch'
-
-export { NativeLabeledRow } from './NativeLabeledRow'
-export type { NativeLabeledRowProps } from './NativeLabeledRow'
-
-export { NativeGauge } from './NativeGauge'
-export type { NativeGaugeProps } from './NativeGauge'
diff --git a/src/shared/constants/CLAUDE.md b/src/shared/constants/CLAUDE.md
deleted file mode 100644
index d228cf4..0000000
--- a/src/shared/constants/CLAUDE.md
+++ /dev/null
@@ -1,15 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 18, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #4831 | 2:57 PM | 🔄 | Spacing Constants Enhanced with Semantic Aliases and Documentation | ~358 |
-| #4830 | 2:56 PM | 🔵 | Design System Constants Index Review | ~293 |
-| #4829 | " | 🔵 | Existing Animation Constants Reviewed | ~331 |
-| #4827 | " | 🔵 | Spacing Constants Implementation Review | ~254 |
-| #4779 | 1:21 PM | 🔵 | Typography System Constants Analysis | ~354 |
-
\ No newline at end of file
diff --git a/src/shared/constants/animations.ts b/src/shared/constants/animations.ts
deleted file mode 100644
index 7d4a240..0000000
--- a/src/shared/constants/animations.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * TabataFit Animation System
- * Liquid Glass — fluid, organic motion
- */
-
-import { Easing } from 'react-native'
-
-// ═══════════════════════════════════════════════════════════════════════════
-// DURATIONS
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const DURATION = {
- INSTANT: 100,
- FAST: 200,
- NORMAL: 300,
- SLOW: 500,
- XSLOW: 800,
-
- // Special
- BREATH: 2500, // Breathing animation cycle
- BREATH_HALF: 1250, // Half breath cycle
- LIQUID: 600, // Liquid morph duration
- RIPPLE: 400, // Tap ripple
- PHASE_CHANGE: 300, // Phase transition
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// EASING CURVES
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const EASE = {
- // Standard iOS easing
- DEFAULT: Easing.bezier(0.25, 0.1, 0.25, 1),
- EASE_OUT: Easing.bezier(0, 0, 0.2, 1),
- EASE_IN: Easing.bezier(0.4, 0, 1, 1),
- EASE_IN_OUT: Easing.bezier(0.4, 0, 0.2, 1),
-
- // Liquid / Fluid
- LIQUID: Easing.bezier(0.4, 0, 0.2, 1),
- BOUNCE: Easing.bezier(0.68, -0.55, 0.265, 1.55),
-
- // Sharp / Snappy
- SNAPPY: Easing.bezier(0.2, 0, 0, 1),
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SPRING CONFIGS (for react-native Animated.spring)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const SPRING = {
- // Bouncy (playful)
- BOUNCY: {
- damping: 15,
- stiffness: 180,
- mass: 1,
- },
-
- // Gentle (smooth)
- GENTLE: {
- damping: 20,
- stiffness: 100,
- mass: 1,
- },
-
- // Snappy (quick)
- SNAPPY: {
- damping: 18,
- stiffness: 300,
- mass: 1,
- },
-
- // Slow (dramatic)
- SLOW: {
- damping: 25,
- stiffness: 80,
- mass: 1.5,
- },
-
- // Liquid (fluid)
- LIQUID: {
- damping: 20,
- stiffness: 200,
- mass: 1,
- },
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// PRESET ANIMATION CONFIGS
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const ANIMATION = {
- // Fade in
- FADE_IN: {
- duration: DURATION.NORMAL,
- easing: EASE.EASE_OUT,
- },
-
- // Scale up (pop in)
- POP_IN: {
- spring: SPRING.BOUNCY,
- },
-
- // Slide up
- SLIDE_UP: {
- duration: DURATION.NORMAL,
- easing: EASE.EASE_OUT,
- },
-
- // Liquid morph
- LIQUID_MORPH: {
- duration: DURATION.LIQUID,
- easing: EASE.LIQUID,
- },
-
- // Breathing glow
- BREATHE: {
- duration: DURATION.BREATH,
- easing: EASE.EASE_IN_OUT,
- },
-
- // Timer tick pulse
- TIMER_TICK: {
- duration: 150,
- easing: EASE.EASE_OUT,
- },
-
- // Phase transition
- PHASE_TRANSITION: {
- duration: DURATION.PHASE_CHANGE,
- easing: EASE.EASE_IN_OUT,
- },
-} as const
diff --git a/src/shared/constants/borderRadius.ts b/src/shared/constants/borderRadius.ts
deleted file mode 100644
index fb0e606..0000000
--- a/src/shared/constants/borderRadius.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * TabataFit Border Radius System
- * iOS-native continuous corners (borderCurve: 'continuous')
- */
-
-export const RADIUS = {
- NONE: 0,
- XS: 4, // Progress bar, hairline element
- SM: 8, // Badge, chip, tag
- MD: 12, // Button, input, tip card — iOS standard
- LG: 16, // Standard card — iOS grouped inset
- XL: 20, // Large card, modal, bottom sheet
- '2XL': 24, // Icon container, medium element
- '3XL': 32, // Large element
-
- // Special
- PILL: 9999, // Pill, toggle, progress bar
- FULL: 9999, // Circle (icon button, avatar, streak dot)
-} as const
diff --git a/src/shared/constants/colors.ts b/src/shared/constants/colors.ts
deleted file mode 100644
index 471254c..0000000
--- a/src/shared/constants/colors.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * TabataFit Color System
- * Dark Premium — refined navy backgrounds, green actions, native iOS feel
- */
-
-// ═══════════════════════════════════════════════════════════════════════════
-// NAVY (Backgrounds) — warmer, closer to iOS dark mode tones
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const NAVY = {
- 900: '#0A1628', // Main app background — deep, warm navy
- 800: '#111D2E', // Surface 1 — default cards (grouped inset bg)
- 700: '#192A3E', // Surface 2 — elevated cards
- 600: '#223750', // Active borders, selection
- 500: '#2C4566', // Tertiary surface / hover state
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// GREEN (Action & Health)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const GREEN = {
- 500: '#00C896', // Primary CTA, effort timer, progress
- 600: '#00A67C', // Hover/pressed state
- 700: '#00875F', // Deep active state
- DIM: 'rgba(0,200,150,0.10)', // Badge/chip/card accent background
- BORDER: 'rgba(0,200,150,0.30)', // Card accent border
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// TEXT & BORDERS
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const TEXT = {
- PRIMARY: '#ECEFF4', // High-contrast primary (Nord Snow Storm inspired)
- SECONDARY: '#9BA4B5', // Secondary text
- TERTIARY: '#6B7A8D', // Tertiary, placeholders — lower contrast than before
- MUTED: '#6B7A8D',
- HINT: '#6B7A8D',
- DISABLED: '#3A4555',
-} as const
-
-export const BORDER_COLORS = {
- DIM: 'rgba(150,164,190,0.12)', // Default border — softer
- HOVER: 'rgba(150,164,190,0.20)', // Hover border
- BRAND: 'rgba(0,200,150,0.30)', // Green border
- SEPARATOR: 'rgba(150,164,190,0.08)', // iOS-style thin separator
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// ORANGE (Tabata Tips ONLY)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const ORANGE = {
- 500: '#FF8A5C',
- 600: '#E06A3C',
- DIM: 'rgba(255,138,92,0.10)',
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// SEMANTIC
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const RED = {
- 500: '#FF453A', // iOS system red — timer emergency <10s
- DIM: 'rgba(255,69,58,0.12)',
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// BRAND (backward-compatible aliases)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const BRAND = {
- PRIMARY: GREEN['500'],
- SECONDARY: GREEN['600'],
- DANGER: RED['500'],
- SUCCESS: GREEN['500'],
- INFO: '#64D2FF', // iOS system cyan
- LINK: '#0A84FF', // iOS system blue — for tappable text
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// BACKGROUND COLORS (backward-compatible DARK alias)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const DARK = {
- BASE: NAVY['900'],
- SURFACE: NAVY['800'],
- ELEVATED: NAVY['700'],
- OVERLAY_1: 'rgba(150,164,190,0.05)',
- OVERLAY_2: 'rgba(150,164,190,0.08)',
- OVERLAY_3: 'rgba(150,164,190,0.12)',
- SCRIM: 'rgba(0,0,0,0.6)',
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// PHASE COLORS (Timer phases)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const PHASE = {
- PREP: '#FFB340',
- PREP_LIGHT: 'rgba(255,179,64,0.2)',
-
- WORK: GREEN['500'],
- WORK_LIGHT: GREEN.DIM,
- WORK_GLOW: 'rgba(0,200,150,0.5)',
-
- REST: '#6B7A8D', // Muted tertiary — visual rest signal
- REST_LIGHT: 'rgba(107,122,141,0.2)',
- REST_GLOW: 'rgba(107,122,141,0.5)',
-
- COMPLETE: GREEN['500'],
- COMPLETE_LIGHT: GREEN.DIM,
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// GRADIENTS (video overlays only)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const AMBER = {
- 500: '#FFD60A', // Effort/energy indicator
-} as const
-
-export const GRADIENTS = {
- VIDEO_OVERLAY: ['transparent', 'rgba(0,0,0,0.8)'],
- VIDEO_TOP: ['rgba(0,0,0,0.5)', 'transparent'],
- CARD_OVERLAY: ['transparent', 'rgba(10,22,40,0.85)'] as const,
- ASSESSMENT_CTA: [ORANGE[500], RED[500]] as const,
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// PHASE COLORS (For timer UI)
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const PHASE_COLORS: Record = {
- PREP: {
- fill: PHASE.PREP,
- glow: 'rgba(255,179,64,0.5)',
- },
- WORK: {
- fill: GREEN['500'],
- glow: PHASE.WORK_GLOW,
- },
- REST: {
- fill: PHASE.REST,
- glow: PHASE.REST_GLOW,
- },
- COMPLETE: {
- fill: GREEN['500'],
- glow: 'rgba(0,200,150,0.5)',
- },
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// BARREL EXPORT
-// ═══════════════════════════════════════════════════════════════════════════
-
-// Named export for backward compatibility with player components
-export const BRAND_DANGER = BRAND.DANGER
-
-export const COLORS = {
- ...BRAND,
- ...DARK,
- ...TEXT,
- ...PHASE,
- BRAND,
- DARK,
- TEXT,
- PHASE,
- GREEN,
- ORANGE,
- NAVY,
- BORDER_COLORS,
- GRADIENTS,
-} as const
diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts
deleted file mode 100644
index 7ddaeee..0000000
--- a/src/shared/constants/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * TabataFit Design System Constants
- * Liquid Glass Design
- */
-
-export * from './colors'
-export * from './typography'
-export * from './spacing'
-export * from './borderRadius'
-export * from './animations'
diff --git a/src/shared/constants/spacing.ts b/src/shared/constants/spacing.ts
deleted file mode 100644
index 4ee2de1..0000000
--- a/src/shared/constants/spacing.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * TabataFit Spacing System
- * Base: 4px
- */
-
-export const SPACING = {
- 0: 0,
- 1: 4,
- 2: 8,
- 3: 12,
- 4: 16,
- 5: 20,
- 6: 24,
- 7: 28,
- 8: 32,
- 10: 40,
- 12: 48,
- 14: 56,
- 16: 64,
- 20: 80,
- 24: 96,
-} as const
-
-export const SPACE = {
- NONE: 0,
- XS: 4,
- SM: 8,
- MD: 16,
- LG: 24,
- XL: 32,
- XXL: 48,
-} as const
-
-export const LAYOUT = {
- SCREEN_PADDING: 20,
- CARD_PADDING: 16,
- BUTTON_HEIGHT: 56,
- BUTTON_HEIGHT_SM: 44,
- TAB_BAR_HEIGHT: 83,
- HEADER_HEIGHT: 44,
- TOUCH_TARGET: 44,
- SECTION_GAP: 24,
- LIST_ROW_HEIGHT: 44,
- LIST_HEADER_HEIGHT: 35,
- GROUPED_INSET_HORIZONTAL: 20,
-} as const
diff --git a/src/shared/constants/typography.ts b/src/shared/constants/typography.ts
deleted file mode 100644
index 724d249..0000000
--- a/src/shared/constants/typography.ts
+++ /dev/null
@@ -1,331 +0,0 @@
-/**
- * TabataFit Typography System
- * SF Pro (system font) — premium native iOS feel
- *
- * Uses Apple's Dynamic Type scale with system font.
- * No custom fonts loaded — SF Pro is the default on iOS.
- * fontWeight controls the visual weight; no fontFamily needed for SF Pro.
- *
- * For data/timer: system monospaced via fontVariant: ['tabular-nums']
- */
-
-import { Platform, TextStyle } from 'react-native'
-
-// ═══════════════════════════════════════════════════════════════════════════
-// FONT FAMILIES — System font (SF Pro on iOS, Roboto on Android)
-// ═══════════════════════════════════════════════════════════════════════════
-
-/**
- * On iOS, omitting fontFamily uses SF Pro automatically.
- * On Android, omitting fontFamily uses Roboto.
- * We use 'System' as a semantic marker — React Native resolves it to the
- * platform system font.
- */
-export const FONT_FAMILY = {
- // System font — no fontFamily needed (undefined = system font in RN)
- SANS: undefined,
- SANS_MEDIUM: undefined,
- SANS_SEMIBOLD: undefined,
- SANS_BOLD: undefined,
- // Serif — iOS has New York via serif fontFamily
- SERIF: Platform.OS === 'ios' ? 'Georgia' : 'serif',
- SERIF_ITALIC: Platform.OS === 'ios' ? 'Georgia' : 'serif',
- // Monospace — system mono (SF Mono on iOS, Roboto Mono on Android)
- MONO: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- MONO_MEDIUM: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
-} as const
-
-// Font alias object — maps to FONT_FAMILY values
-const FONT = {
- SANS: FONT_FAMILY.SANS,
- SANS_MEDIUM: FONT_FAMILY.SANS_MEDIUM,
- SANS_SEMIBOLD: FONT_FAMILY.SANS_SEMIBOLD,
- SANS_BOLD: FONT_FAMILY.SANS_BOLD,
- SERIF: FONT_FAMILY.SERIF,
- SERIF_ITALIC: FONT_FAMILY.SERIF_ITALIC,
- MONO: FONT_FAMILY.MONO,
- MONO_MEDIUM: FONT_FAMILY.MONO_MEDIUM,
- // Backward compat
- REGULAR: FONT_FAMILY.SANS,
- MEDIUM: FONT_FAMILY.SANS_MEDIUM,
- SEMIBOLD: FONT_FAMILY.SANS_SEMIBOLD,
- BOLD: FONT_FAMILY.SANS_BOLD,
- BLACK: FONT_FAMILY.SANS_BOLD,
-} as const
-
-// ═══════════════════════════════════════════════════════════════════════════
-// TYPE SCALE — matches Apple HIG Dynamic Type defaults
-// ═══════════════════════════════════════════════════════════════════════════
-
-export const TYPOGRAPHY = {
- // Display / Hero — bold system font for emotional moments
- DISPLAY: {
- fontFamily: FONT.SERIF,
- fontSize: 28,
- fontWeight: '700' as const,
- lineHeight: 34,
- letterSpacing: 0.36,
- } as TextStyle,
-
- HERO: {
- fontFamily: FONT.SERIF,
- fontSize: 34,
- fontWeight: '700' as const,
- lineHeight: 41,
- letterSpacing: 0.37,
- } as TextStyle,
-
- // Large Title — iOS native large title
- LARGE_TITLE: {
- fontSize: 34,
- fontWeight: '700' as const,
- lineHeight: 41,
- letterSpacing: 0.37,
- } as TextStyle,
-
- // Heading 1
- HEADING_1: {
- fontSize: 28,
- fontWeight: '700' as const,
- lineHeight: 34,
- letterSpacing: 0.36,
- } as TextStyle,
-
- // Title 1
- TITLE_1: {
- fontSize: 28,
- fontWeight: '400' as const,
- lineHeight: 34,
- letterSpacing: 0.36,
- } as TextStyle,
-
- // Heading 2 — exercise titles, program cards
- HEADING_2: {
- fontSize: 22,
- fontWeight: '700' as const,
- lineHeight: 28,
- letterSpacing: 0.35,
- } as TextStyle,
-
- TITLE_2: {
- fontSize: 22,
- fontWeight: '400' as const,
- lineHeight: 28,
- letterSpacing: 0.35,
- } as TextStyle,
-
- TITLE_3: {
- fontSize: 20,
- fontWeight: '400' as const,
- lineHeight: 25,
- letterSpacing: 0.38,
- } as TextStyle,
-
- // Headline
- HEADLINE: {
- fontSize: 17,
- fontWeight: '600' as const,
- lineHeight: 22,
- letterSpacing: -0.41,
- } as TextStyle,
-
- // Body
- BODY: {
- fontSize: 17,
- fontWeight: '400' as const,
- lineHeight: 22,
- letterSpacing: -0.41,
- } as TextStyle,
-
- BODY_BOLD: {
- fontSize: 17,
- fontWeight: '600' as const,
- lineHeight: 22,
- letterSpacing: -0.41,
- } as TextStyle,
-
- // Callout
- CALLOUT: {
- fontSize: 16,
- fontWeight: '400' as const,
- lineHeight: 21,
- letterSpacing: -0.32,
- } as TextStyle,
-
- // Subheadline
- SUBHEADLINE: {
- fontSize: 15,
- fontWeight: '400' as const,
- lineHeight: 20,
- letterSpacing: -0.24,
- } as TextStyle,
-
- // Label — small uppercase for tags, metadata
- LABEL: {
- fontSize: 12,
- fontWeight: '500' as const,
- lineHeight: 16,
- letterSpacing: 0.5,
- textTransform: 'uppercase' as const,
- } as TextStyle,
-
- // Footnote
- FOOTNOTE: {
- fontSize: 13,
- fontWeight: '400' as const,
- lineHeight: 18,
- letterSpacing: -0.08,
- } as TextStyle,
-
- // Caption
- CAPTION_1: {
- fontSize: 12,
- fontWeight: '400' as const,
- lineHeight: 16,
- letterSpacing: 0,
- } as TextStyle,
-
- CAPTION_2: {
- fontSize: 11,
- fontWeight: '400' as const,
- lineHeight: 13,
- letterSpacing: 0.07,
- } as TextStyle,
-
- // ─────────────────────────────────────────────────────────────────────────
- // SPECIAL: TIMER — Monospaced, readable from 2 meters
- // ─────────────────────────────────────────────────────────────────────────
-
- TIMER_NUMBER: {
- fontFamily: FONT.MONO,
- fontSize: 88,
- fontWeight: '300' as const,
- lineHeight: 88,
- letterSpacing: -2,
- fontVariant: ['tabular-nums'],
- } as TextStyle,
-
- TIMER_NUMBER_COMPACT: {
- fontFamily: FONT.MONO,
- fontSize: 72,
- fontWeight: '300' as const,
- lineHeight: 72,
- letterSpacing: -1.5,
- fontVariant: ['tabular-nums'],
- } as TextStyle,
-
- TIMER_PHASE: {
- fontSize: 13,
- fontWeight: '600' as const,
- lineHeight: 18,
- letterSpacing: 1.5,
- textTransform: 'uppercase' as const,
- } as TextStyle,
-
- TIMER_ROUND: {
- fontFamily: FONT.MONO,
- fontSize: 17,
- fontWeight: '500' as const,
- lineHeight: 22,
- letterSpacing: 0,
- fontVariant: ['tabular-nums'],
- } as TextStyle,
-
- EXERCISE_NAME: {
- fontSize: 28,
- fontWeight: '700' as const,
- lineHeight: 34,
- letterSpacing: 0.36,
- textAlign: 'center' as const,
- } as TextStyle,
-
- // ─────────────────────────────────────────────────────────────────────────
- // SPECIAL: BUTTON
- // ─────────────────────────────────────────────────────────────────────────
-
- BUTTON_LARGE: {
- fontSize: 17,
- fontWeight: '600' as const,
- lineHeight: 22,
- letterSpacing: -0.41,
- } as TextStyle,
-
- BUTTON_MEDIUM: {
- fontSize: 15,
- fontWeight: '600' as const,
- lineHeight: 20,
- letterSpacing: -0.24,
- } as TextStyle,
-
- BUTTON_SMALL: {
- fontSize: 14,
- fontWeight: '600' as const,
- lineHeight: 18,
- letterSpacing: -0.15,
- } as TextStyle,
-
- // ─────────────────────────────────────────────────────────────────────────
- // SPECIAL: CARD
- // ─────────────────────────────────────────────────────────────────────────
-
- CARD_TITLE: {
- fontSize: 17,
- fontWeight: '600' as const,
- lineHeight: 22,
- letterSpacing: -0.41,
- } as TextStyle,
-
- CARD_SUBTITLE: {
- fontSize: 15,
- fontWeight: '400' as const,
- lineHeight: 20,
- letterSpacing: -0.24,
- } as TextStyle,
-
- CARD_METADATA: {
- fontSize: 13,
- fontWeight: '500' as const,
- lineHeight: 18,
- letterSpacing: -0.08,
- } as TextStyle,
-
- // ─────────────────────────────────────────────────────────────────────────
- // SPECIAL: STATS — Monospaced numerals
- // ─────────────────────────────────────────────────────────────────────────
-
- STAT_VALUE: {
- fontFamily: FONT.MONO,
- fontSize: 34,
- fontWeight: '300' as const,
- lineHeight: 41,
- letterSpacing: -0.5,
- fontVariant: ['tabular-nums'],
- } as TextStyle,
-
- STAT_LABEL: {
- fontSize: 12,
- fontWeight: '500' as const,
- lineHeight: 16,
- letterSpacing: 0.5,
- textTransform: 'uppercase' as const,
- } as TextStyle,
-
- // ─────────────────────────────────────────────────────────────────────────
- // SPECIAL: OVERLINE / SECTION HEADER
- // ─────────────────────────────────────────────────────────────────────────
-
- OVERLINE: {
- fontSize: 12,
- fontWeight: '500' as const,
- lineHeight: 16,
- letterSpacing: 0.5,
- textTransform: 'uppercase' as const,
- } as TextStyle,
-
-} as const
-
-export { FONT }
-
-// Named exports for backward compatibility with player components
-export const FONT_FAMILY_SANS_SEMIBOLD = FONT_FAMILY.SANS_SEMIBOLD
-export const FONT_FAMILY_SANS_BOLD = FONT_FAMILY.SANS_BOLD
diff --git a/src/shared/data/CLAUDE.md b/src/shared/data/CLAUDE.md
deleted file mode 100644
index 94fab2e..0000000
--- a/src/shared/data/CLAUDE.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 11, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6138 | 7:44 PM | 🔵 | Program accent colors contain hardcoded hex values | ~244 |
-
\ No newline at end of file
diff --git a/src/shared/data/index.ts b/src/shared/data/index.ts
deleted file mode 100644
index e59622a..0000000
--- a/src/shared/data/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * TabataFit Data Layer
- * Workout programs are fetched from Supabase via `workoutPrograms.ts`.
- */
-
-export * from './workoutPrograms'
-export { useMusicVibeLabel } from './useTranslatedData'
diff --git a/src/shared/data/useTranslatedData.ts b/src/shared/data/useTranslatedData.ts
deleted file mode 100644
index e7998cd..0000000
--- a/src/shared/data/useTranslatedData.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * TabataFit Translated Data Hooks
- * Wraps raw data objects with t() lookups at render time.
- */
-
-import { useTranslation } from 'react-i18next'
-
-/** Translate a music vibe name */
-export function useMusicVibeLabel(vibe: string): string {
- const { t } = useTranslation('common')
- const vibeKeyMap: Record = {
- electronic: 'musicVibes.electronic',
- 'hip-hop': 'musicVibes.hipHop',
- pop: 'musicVibes.pop',
- rock: 'musicVibes.rock',
- chill: 'musicVibes.chill',
- }
- return t(vibeKeyMap[vibe] ?? vibe, { defaultValue: vibe })
-}
diff --git a/src/shared/data/workoutPrograms.ts b/src/shared/data/workoutPrograms.ts
deleted file mode 100644
index b4cb0fd..0000000
--- a/src/shared/data/workoutPrograms.ts
+++ /dev/null
@@ -1,349 +0,0 @@
-/**
- * Workout Programs Data Access Layer
- * Fetches body-zone workout programs from Supabase with offline caching
- */
-
-import { supabase, isSupabaseConfigured } from '../supabase/client'
-import AsyncStorage from '@react-native-async-storage/async-storage'
-import type {
- WorkoutProgram,
- WorkoutTabata,
- WorkoutProgramRow,
- WorkoutTabataRow,
- WorkoutTimedExerciseRow,
- WarmupBlock,
- StretchBlock,
- TimedExercise,
- BodyZone,
-} from '../types/workoutProgram'
-import type { TabataSession, TabataBlock, TabataExercise, TimedMovement } from '../types/program'
-
-// ─── Constants ──────────────────────────────────────────────────
-
-const CACHE_KEY = 'tabatafit-workout-programs-cache-v3'
-const CACHE_TTL = 1000 * 60 * 60 // 1 hour
-
-// ─── Row Mappers ────────────────────────────────────────────────
-
-function tabataRowToWorkoutTabata(row: WorkoutTabataRow): WorkoutTabata {
- return {
- id: row.id,
- position: row.position,
- exercise1: {
- name: row.exercise_1_name,
- nameEn: row.exercise_1_name_en ?? '',
- tip: row.exercise_1_tip ?? undefined,
- tipEn: row.exercise_1_tip_en ?? undefined,
- modification: row.exercise_1_modification ?? undefined,
- modificationEn: row.exercise_1_modification_en ?? undefined,
- progression: row.exercise_1_progression ?? undefined,
- progressionEn: row.exercise_1_progression_en ?? undefined,
- videoUrl: row.exercise_1_video_url ?? null,
- },
- exercise2: {
- name: row.exercise_2_name,
- nameEn: row.exercise_2_name_en ?? '',
- tip: row.exercise_2_tip ?? undefined,
- tipEn: row.exercise_2_tip_en ?? undefined,
- modification: row.exercise_2_modification ?? undefined,
- modificationEn: row.exercise_2_modification_en ?? undefined,
- progression: row.exercise_2_progression ?? undefined,
- progressionEn: row.exercise_2_progression_en ?? undefined,
- videoUrl: row.exercise_2_video_url ?? null,
- },
- rounds: row.rounds,
- workTime: row.work_time,
- restTime: row.rest_time,
- }
-}
-
-function timedExerciseRowToTimedExercise(row: WorkoutTimedExerciseRow): TimedExercise {
- return {
- name: row.name,
- nameEn: row.name_en ?? '',
- duration: row.duration,
- videoUrl: row.video_url ?? null,
- tip: row.tip ?? undefined,
- tipEn: row.tip_en ?? undefined,
- }
-}
-
-function buildWarmupBlock(rows: WorkoutTimedExerciseRow[]): WarmupBlock {
- const exercises = rows
- .sort((a, b) => a.position - b.position)
- .map(timedExerciseRowToTimedExercise)
- const totalDuration = exercises.reduce((sum, e) => sum + e.duration, 0)
- return { exercises, totalDuration }
-}
-
-function buildStretchBlock(rows: WorkoutTimedExerciseRow[]): StretchBlock {
- const exercises = rows
- .sort((a, b) => a.position - b.position)
- .map(timedExerciseRowToTimedExercise)
- const totalDuration = exercises.reduce((sum, e) => sum + e.duration, 0)
- return { exercises, totalDuration }
-}
-
-/** Placeholder blocks until admin-web seeds real content */
-const DEFAULT_WARMUP: WarmupBlock = {
- exercises: [
- { name: 'Jumping jacks', nameEn: 'Jumping jacks', duration: 45, videoUrl: null },
- { name: 'Rotation des bras', nameEn: 'Arm circles', duration: 30, videoUrl: null },
- { name: 'Montées de genoux', nameEn: 'High knees', duration: 45, videoUrl: null },
- { name: 'Squats légers', nameEn: 'Light squats', duration: 30, videoUrl: null },
- ],
- totalDuration: 150,
-}
-
-const DEFAULT_STRETCH: StretchBlock = {
- exercises: [
- { name: 'Étirement quadriceps', nameEn: 'Quad stretch', duration: 30, videoUrl: null },
- { name: 'Étirement ischio-jambiers', nameEn: 'Hamstring stretch', duration: 30, videoUrl: null },
- { name: 'Étirement épaules', nameEn: 'Shoulder stretch', duration: 30, videoUrl: null },
- { name: 'Respiration profonde', nameEn: 'Deep breathing', duration: 30, videoUrl: null },
- ],
- totalDuration: 120,
-}
-
-function rowsToWorkoutProgram(
- programRow: WorkoutProgramRow,
- tabataRows: WorkoutTabataRow[],
- warmupRows: WorkoutTimedExerciseRow[],
- stretchRows: WorkoutTimedExerciseRow[],
-): WorkoutProgram {
- const warmup = warmupRows.length > 0 ? buildWarmupBlock(warmupRows) : DEFAULT_WARMUP
- const stretch = stretchRows.length > 0 ? buildStretchBlock(stretchRows) : DEFAULT_STRETCH
-
- return {
- id: programRow.id,
- title: programRow.title,
- description: programRow.description,
- bodyZone: programRow.body_zone,
- level: programRow.level,
- isFree: programRow.is_free,
- musicVibe: programRow.music_vibe ?? 'electronic',
- estimatedDuration: programRow.estimated_duration,
- estimatedCalories: programRow.estimated_calories,
- icon: programRow.icon,
- accentColor: programRow.accent_color,
- sortOrder: programRow.sort_order,
- warmup,
- tabatas: tabataRows
- .sort((a, b) => a.position - b.position)
- .map(tabataRowToWorkoutTabata),
- stretch,
- createdAt: programRow.created_at,
- updatedAt: programRow.updated_at,
- }
-}
-
-// ─── Cache ──────────────────────────────────────────────────────
-
-interface CacheEntry {
- programs: WorkoutProgram[]
- timestamp: number
-}
-
-async function getCachedPrograms(): Promise {
- try {
- const raw = await AsyncStorage.getItem(CACHE_KEY)
- if (!raw) return null
- const entry: CacheEntry = JSON.parse(raw)
- if (Date.now() - entry.timestamp > CACHE_TTL) return null
- return entry.programs
- } catch {
- return null
- }
-}
-
-async function setCachedPrograms(programs: WorkoutProgram[]): Promise {
- try {
- const entry: CacheEntry = { programs, timestamp: Date.now() }
- await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(entry))
- } catch {
- // Cache write failure is non-critical
- }
-}
-
-// ─── Fetch Functions ────────────────────────────────────────────
-
-/**
- * Fetch all workout programs with their tabatas, warmup, and stretch.
- * Always fetches fresh from Supabase; falls back to cache if offline.
- */
-export async function fetchAllPrograms(): Promise {
- if (!isSupabaseConfigured()) {
- const cached = await getCachedPrograms()
- return cached ?? []
- }
-
- const { data: programRows, error: progError } = await supabase
- .from('workout_programs')
- .select('*')
- .order('sort_order')
- .order('body_zone')
- .order('level')
- .returns()
-
- if (progError || !programRows?.length) {
- const cached = await getCachedPrograms()
- return cached ?? []
- }
-
- const { data: tabataRows } = await supabase
- .from('program_tabatas')
- .select('*')
- .order('position')
- .returns()
-
- const { data: warmupRows } = await supabase
- .from('workout_warmup_exercises')
- .select('*')
- .order('position')
- .returns()
-
- const { data: stretchRows } = await supabase
- .from('workout_stretch_exercises')
- .select('*')
- .order('position')
- .returns()
-
- const tabatasByProgram = groupBy(tabataRows ?? [], r => r.program_id)
- const warmupByProgram = groupBy(warmupRows ?? [], r => r.program_id)
- const stretchByProgram = groupBy(stretchRows ?? [], r => r.program_id)
-
- const programs = programRows.map(pr =>
- rowsToWorkoutProgram(
- pr,
- tabatasByProgram.get(pr.id) ?? [],
- warmupByProgram.get(pr.id) ?? [],
- stretchByProgram.get(pr.id) ?? [],
- ),
- )
-
- await setCachedPrograms(programs)
- return programs
-}
-
-function groupBy(items: T[], keyFn: (t: T) => K): Map {
- const map = new Map()
- for (const item of items) {
- const key = keyFn(item)
- const bucket = map.get(key) ?? []
- bucket.push(item)
- map.set(key, bucket)
- }
- return map
-}
-
-/** Fetch programs filtered by body zone */
-export async function fetchProgramsByBodyZone(
- bodyZone: BodyZone,
-): Promise {
- const all = await fetchAllPrograms()
- return all.filter(p => p.bodyZone === bodyZone)
-}
-
-/** Fetch a single program by ID */
-export async function fetchProgramById(id: string): Promise {
- const all = await fetchAllPrograms()
- return all.find(p => p.id === id) ?? null
-}
-
-/** Check if an ID refers to a workout program */
-export function isWorkoutProgramId(id: string): boolean {
- return id.startsWith('wp-')
-}
-
-/** Parse a workout program ID like 'wp-{programId}' */
-export function parseWorkoutProgramId(id: string): { programId: string } | null {
- if (!id.startsWith('wp-')) return null
- return { programId: id.slice(3) }
-}
-
-/** Build an ID for a workout program */
-export function buildWorkoutProgramId(programId: string): string {
- return `wp-${programId}`
-}
-
-// ─── Adapter: WorkoutProgram → TabataSession ─────────────────────
-
-function timedExerciseToMovement(e: TimedExercise): TimedMovement {
- return { name: e.name, nameEn: e.nameEn, duration: e.duration }
-}
-
-/**
- * Convert a full WorkoutProgram (warmup + 3 tabatas + stretch) to a TabataSession
- * for the legacy player. Uses program's warmup and stretch blocks directly.
- */
-export function workoutProgramToTabataSession(
- program: WorkoutProgram,
-): TabataSession {
- const blocks: TabataBlock[] = program.tabatas.map((tabata) => {
- const oddExercise: TabataExercise = {
- name: tabata.exercise1.name,
- nameEn: tabata.exercise1.nameEn,
- conseil: tabata.exercise1.tip ?? '',
- conseilEn: tabata.exercise1.tipEn ?? '',
- modification: tabata.exercise1.modification,
- modificationEn: tabata.exercise1.modificationEn,
- progression: tabata.exercise1.progression,
- progressionEn: tabata.exercise1.progressionEn,
- }
- const evenExercise: TabataExercise = {
- name: tabata.exercise2.name,
- nameEn: tabata.exercise2.nameEn,
- conseil: tabata.exercise2.tip ?? '',
- conseilEn: tabata.exercise2.tipEn ?? '',
- modification: tabata.exercise2.modification,
- modificationEn: tabata.exercise2.modificationEn,
- progression: tabata.exercise2.progression,
- progressionEn: tabata.exercise2.progressionEn,
- }
- return {
- id: tabata.id,
- oddExercise,
- evenExercise,
- rounds: tabata.rounds,
- workTime: tabata.workTime,
- restTime: tabata.restTime,
- }
- })
-
- const tabatasDurationSec = blocks.reduce(
- (sum, b) => sum + b.rounds * (b.workTime + b.restTime),
- 0,
- )
- const interBlockRestSec = Math.max(0, blocks.length - 1) * 15 // 15s between blocks
- const totalSec = program.warmup.totalDuration + tabatasDurationSec + interBlockRestSec + program.stretch.totalDuration
- const totalDuration = Math.ceil(totalSec / 60)
-
- const totalRounds = blocks.reduce((sum, b) => sum + b.rounds, 0)
- const calorieMultiplier = program.level === 'Advanced' ? 12 : program.level === 'Intermediate' ? 9 : 7
-
- return {
- id: buildWorkoutProgramId(program.id),
- week: 1,
- order: 1,
- title: program.title,
- titleEn: program.title,
- description: program.description ?? '',
- descriptionEn: program.description ?? '',
- focus: [program.bodyZone],
- focusEn: [program.bodyZone],
- warmup: {
- movements: program.warmup.exercises.map(timedExerciseToMovement),
- totalDuration: program.warmup.totalDuration,
- },
- blocks,
- cooldown: {
- movements: program.stretch.exercises.map(timedExerciseToMovement),
- totalDuration: program.stretch.totalDuration,
- },
- equipment: [],
- totalRounds,
- totalDuration,
- calories: Math.ceil(totalRounds * calorieMultiplier),
- musicVibe: program.musicVibe,
- }
-}
diff --git a/src/shared/hooks/CLAUDE.md b/src/shared/hooks/CLAUDE.md
deleted file mode 100644
index 346e9d3..0000000
--- a/src/shared/hooks/CLAUDE.md
+++ /dev/null
@@ -1,26 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 10, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6008 | 10:03 AM | 🔵 | Kine Timer Hook with Multi-Block State Machine | ~409 |
-
-### Apr 13, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6304 | 11:46 PM | ✅ | Timer final second delay increased to 600ms | ~215 |
-| #6303 | 11:41 PM | 🔴 | TypeScript verification passed for timer threshold changes | ~173 |
-| #6296 | 11:37 PM | 🔴 | Timer transitions at 1 second display improved with 250ms delay | ~263 |
-| #6291 | 11:25 PM | 🔴 | Timer phase transition threshold adjusted | ~229 |
-
-### Apr 17, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6382 | 10:35 AM | 🔵 | Investigated Premium Access Control Logic in usePurchases Hook | ~226 |
-
\ No newline at end of file
diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts
deleted file mode 100644
index dae76e9..0000000
--- a/src/shared/hooks/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * TabataFit Shared Hooks
- */
-
-export { useTimer } from './useTimer'
-export type { TimerPhase } from './useTimer'
-export { useHaptics } from './useHaptics'
-export { useAudio } from './useAudio'
-export { useMusicPlayer } from './useMusicPlayer'
-export { useNotifications, requestNotificationPermissions } from './useNotifications'
-export { usePurchases } from './usePurchases'
-export { useTabataTimer } from './useTabataTimer'
diff --git a/src/shared/hooks/useAudio.ts b/src/shared/hooks/useAudio.ts
deleted file mode 100644
index e0a899e..0000000
--- a/src/shared/hooks/useAudio.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * TabataFit Audio Hook
- * Sound effects for timer events using expo-av
- * Respects userStore soundEffects setting
- */
-
-import { useRef, useEffect, useCallback } from 'react'
-import { Audio } from 'expo-av'
-import { useUserStore } from '../stores'
-
-// Audio assets
-const SOUNDS = {
- countdown: require('../../../assets/audio/countdown.wav'),
- phaseStart: require('../../../assets/audio/phase-start.wav'),
- complete: require('../../../assets/audio/complete.wav'),
-}
-
-type SoundKey = keyof typeof SOUNDS
-
-export function useAudio() {
- const soundEnabled = useUserStore((s) => s.settings.soundEffects)
- const loadedSounds = useRef>({})
-
- // Configure audio session
- useEffect(() => {
- Audio.setAudioModeAsync({
- playsInSilentModeIOS: true,
- staysActiveInBackground: false,
- shouldDuckAndroid: true,
- })
-
- return () => {
- // Unload all sounds on cleanup
- Object.values(loadedSounds.current).forEach(sound => {
- sound.unloadAsync().catch(() => {})
- })
- loadedSounds.current = {}
- }
- }, [])
-
- const getSound = useCallback(async (key: SoundKey): Promise => {
- if (loadedSounds.current[key]) {
- return loadedSounds.current[key]
- }
- try {
- const { sound } = await Audio.Sound.createAsync(SOUNDS[key])
- loadedSounds.current[key] = sound
- return sound
- } catch {
- return null
- }
- }, [])
-
- const play = useCallback(async (key: SoundKey) => {
- if (!soundEnabled) return
- const sound = await getSound(key)
- if (!sound) return
- try {
- await sound.setPositionAsync(0)
- await sound.playAsync()
- } catch {
- // Sound may have been unloaded
- }
- }, [soundEnabled, getSound])
-
- return {
- /** Short beep for countdown ticks (3, 2, 1) */
- countdownBeep: useCallback(() => play('countdown'), [play]),
- /** Ding for phase transitions (work → rest, rest → work) */
- phaseStart: useCallback(() => play('phaseStart'), [play]),
- /** Celebration chime on workout completion */
- workoutComplete: useCallback(() => play('complete'), [play]),
- }
-}
diff --git a/src/shared/hooks/useHaptics.ts b/src/shared/hooks/useHaptics.ts
deleted file mode 100644
index 9a48916..0000000
--- a/src/shared/hooks/useHaptics.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * TabataFit Haptics Hook
- * Centralized haptic feedback respecting user settings
- */
-
-import { useCallback } from 'react'
-import * as Haptics from 'expo-haptics'
-import { useUserStore } from '../stores'
-
-export function useHaptics() {
- const haptics = useUserStore((s) => s.settings.haptics)
-
- const phaseChange = useCallback(() => {
- if (!haptics) return
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
- }, [haptics])
-
- const buttonTap = useCallback(() => {
- if (!haptics) return
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
- }, [haptics])
-
- const countdownTick = useCallback(() => {
- if (!haptics) return
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
- }, [haptics])
-
- const workoutComplete = useCallback(() => {
- if (!haptics) return
- Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
- }, [haptics])
-
- const selection = useCallback(() => {
- if (!haptics) return
- Haptics.selectionAsync()
- }, [haptics])
-
- return {
- phaseChange,
- buttonTap,
- countdownTick,
- workoutComplete,
- selection,
- }
-}
diff --git a/src/shared/hooks/useMusicPlayer.ts b/src/shared/hooks/useMusicPlayer.ts
deleted file mode 100644
index a9a8d63..0000000
--- a/src/shared/hooks/useMusicPlayer.ts
+++ /dev/null
@@ -1,243 +0,0 @@
-/**
- * TabataFit Music Player Hook
- * Manages background music playback synced with workout timer
- * Loads tracks from Supabase Storage based on workout's musicVibe
- */
-
-import { logger } from '../utils/logger'
-
-import { useRef, useEffect, useCallback, useState } from 'react'
-import { Audio, type AVPlaybackStatus } from 'expo-av'
-import { useUserStore } from '../stores'
-import { musicService, type MusicTrack } from '../services/music'
-import type { MusicVibe } from '../types'
-
-interface UseMusicPlayerOptions {
- vibe: MusicVibe
- isPlaying: boolean
- volume?: number
-}
-
-interface UseMusicPlayerReturn {
- /** Current track being played */
- currentTrack: MusicTrack | null
- /** Whether music is loaded and ready */
- isReady: boolean
- /** Error message if loading failed */
- error: string | null
- /** Set volume (0-1) */
- setVolume: (volume: number) => void
- /** Skip to next track */
- nextTrack: () => void
-}
-
-export function useMusicPlayer(options: UseMusicPlayerOptions): UseMusicPlayerReturn {
- const { vibe, isPlaying, volume = 0.5 } = options
-
- const musicEnabled = useUserStore((s) => s.settings.musicEnabled)
- const soundRef = useRef(null)
- const tracksRef = useRef([])
- const currentTrackIndexRef = useRef(0)
-
- const [currentTrack, setCurrentTrack] = useState(null)
- const [isReady, setIsReady] = useState(false)
- const [error, setError] = useState(null)
-
- // Configure audio session for background music
- useEffect(() => {
- Audio.setAudioModeAsync({
- playsInSilentModeIOS: true,
- staysActiveInBackground: true,
- shouldDuckAndroid: true,
- // Mix with other audio (allows sound effects to play over music)
- interruptionModeIOS: 1, // INTERRUPTION_MODE_IOS_MIX_WITH_OTHERS
- interruptionModeAndroid: 1, // INTERRUPTION_MODE_ANDROID_DUCK_OTHERS
- })
-
- return () => {
- // Cleanup on unmount
- if (soundRef.current) {
- soundRef.current.unloadAsync().catch(() => {})
- soundRef.current = null
- }
- }
- }, [])
-
- // Load tracks when vibe changes
- useEffect(() => {
- let isMounted = true
-
- async function loadTracks() {
- try {
- setIsReady(false)
- setError(null)
-
- // Unload current track if any
- if (soundRef.current) {
- await soundRef.current.unloadAsync()
- soundRef.current = null
- }
-
- const tracks = await musicService.loadTracksForVibe(vibe)
-
- if (!isMounted) return
-
- tracksRef.current = tracks
-
- if (tracks.length > 0) {
- // Select random starting track
- currentTrackIndexRef.current = Math.floor(Math.random() * tracks.length)
- const track = tracks[currentTrackIndexRef.current]
- setCurrentTrack(track)
- await loadAndPlayTrack(track, false)
- } else {
- setError('No tracks available')
- }
-
- setIsReady(true)
- } catch (err) {
- if (!isMounted) return
- setError(err instanceof Error ? err.message : 'Failed to load music')
- setIsReady(false)
- }
- }
-
- if (musicEnabled) {
- loadTracks()
- }
-
- return () => {
- isMounted = false
- }
- }, [vibe, musicEnabled])
-
- // Load and prepare a track
- const loadAndPlayTrack = useCallback(async (track: MusicTrack, autoPlay: boolean = true) => {
- try {
- if (soundRef.current) {
- await soundRef.current.unloadAsync()
- }
-
- // For mock tracks without URLs, skip loading
- if (!track.url) {
- logger.log(`[MusicPlayer] Mock track: ${track.title} - ${track.artist}`)
- return
- }
-
- const { sound } = await Audio.Sound.createAsync(
- { uri: track.url },
- {
- shouldPlay: autoPlay && isPlaying && musicEnabled,
- volume: volume,
- isLooping: false,
- positionMillis: 10_000,
- },
- onPlaybackStatusUpdate
- )
-
- soundRef.current = sound
- } catch (err) {
- logger.error('[MusicPlayer] Error loading track:', err)
- }
- }, [isPlaying, musicEnabled, volume])
-
- // Handle playback status updates
- const onPlaybackStatusUpdate = useCallback((status: AVPlaybackStatus) => {
- if (!status.isLoaded) return
-
- // Track finished playing - load next
- if (status.didJustFinish) {
- playNextTrack()
- }
- }, [])
-
- // Play next track
- const playNextTrack = useCallback(async () => {
- if (tracksRef.current.length === 0) return
-
- currentTrackIndexRef.current = (currentTrackIndexRef.current + 1) % tracksRef.current.length
- const nextTrack = tracksRef.current[currentTrackIndexRef.current]
-
- setCurrentTrack(nextTrack)
- await loadAndPlayTrack(nextTrack, isPlaying && musicEnabled)
- }, [isPlaying, musicEnabled, loadAndPlayTrack])
-
- // Handle play/pause based on workout state
- useEffect(() => {
- async function updatePlayback() {
- if (!soundRef.current || !musicEnabled) return
-
- try {
- const status = await soundRef.current.getStatusAsync()
- if (!status.isLoaded) return
-
- if (isPlaying && !status.isPlaying) {
- await soundRef.current.playAsync()
- } else if (!isPlaying && status.isPlaying) {
- await soundRef.current.pauseAsync()
- }
- } catch (err) {
- logger.error('[MusicPlayer] Error updating playback:', err)
- }
- }
-
- updatePlayback()
- }, [isPlaying, musicEnabled])
-
- // Update volume when it changes
- useEffect(() => {
- async function updateVolume() {
- if (!soundRef.current) return
-
- try {
- const status = await soundRef.current.getStatusAsync()
- if (status.isLoaded) {
- await soundRef.current.setVolumeAsync(volume)
- }
- } catch (err) {
- logger.error('[MusicPlayer] Error updating volume:', err)
- }
- }
-
- updateVolume()
- }, [volume])
-
- // Update music enabled setting
- useEffect(() => {
- async function handleMusicToggle() {
- if (!soundRef.current) return
-
- try {
- if (!musicEnabled) {
- await soundRef.current.pauseAsync()
- } else if (isPlaying) {
- await soundRef.current.playAsync()
- }
- } catch (err) {
- logger.error('[MusicPlayer] Error toggling music:', err)
- }
- }
-
- handleMusicToggle()
- }, [musicEnabled, isPlaying])
-
- // Set volume function
- const setVolume = useCallback((newVolume: number) => {
- const clampedVolume = Math.max(0, Math.min(1, newVolume))
- // Volume is controlled via the store or parent component
- // This function can be used to update the store
- }, [])
-
- // Next track function
- const nextTrack = useCallback(async () => {
- await playNextTrack()
- }, [playNextTrack])
-
- return {
- currentTrack,
- isReady,
- error,
- setVolume,
- nextTrack,
- }
-}
diff --git a/src/shared/hooks/useNotifications.ts b/src/shared/hooks/useNotifications.ts
deleted file mode 100644
index 4b0d03d..0000000
--- a/src/shared/hooks/useNotifications.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * TabataFit Notification Hook
- * Manages daily reminder scheduling via expo-notifications
- */
-
-import { useEffect } from 'react'
-import * as Notifications from 'expo-notifications'
-import { useUserStore } from '@/src/shared/stores'
-import i18n from '@/src/shared/i18n'
-
-const REMINDER_ID = 'daily-reminder'
-
-export async function requestNotificationPermissions(): Promise {
- const { status: existing } = await Notifications.getPermissionsAsync()
- if (existing === 'granted') return true
-
- const { status } = await Notifications.requestPermissionsAsync()
- return status === 'granted'
-}
-
-async function scheduleDaily(time: string) {
- await Notifications.cancelAllScheduledNotificationsAsync()
-
- const [hour, minute] = time.split(':').map(Number)
-
- await Notifications.scheduleNotificationAsync({
- identifier: REMINDER_ID,
- content: {
- title: i18n.t('notifications:dailyReminder.title'),
- body: i18n.t('notifications:dailyReminder.body'),
- sound: true,
- },
- trigger: {
- type: Notifications.SchedulableTriggerInputTypes.DAILY,
- hour,
- minute,
- },
- })
-}
-
-async function cancelAll() {
- await Notifications.cancelAllScheduledNotificationsAsync()
-}
-
-export function useNotifications() {
- const reminders = useUserStore((s) => s.settings.reminders)
- const reminderTime = useUserStore((s) => s.settings.reminderTime)
-
- useEffect(() => {
- if (reminders) {
- scheduleDaily(reminderTime)
- } else {
- cancelAll()
- }
- }, [reminders, reminderTime])
-}
diff --git a/src/shared/hooks/usePurchases.ts b/src/shared/hooks/usePurchases.ts
deleted file mode 100644
index 97a14ea..0000000
--- a/src/shared/hooks/usePurchases.ts
+++ /dev/null
@@ -1,331 +0,0 @@
-/**
- * TabataFit Purchases Hook
- * Wraps RevenueCat API for subscription management
- *
- * DEV mode: If StoreKit purchase fails (no sandbox account / product mismatch),
- * shows an Alert offering to simulate the purchase for testing.
- */
-
-import { useState, useEffect, useCallback, useRef } from 'react'
-import { Alert } from 'react-native'
-import { logger } from '../utils/logger'
-import Purchases, {
- CustomerInfo,
- PurchasesOfferings,
- PurchasesPackage,
-} from 'react-native-purchases'
-
-import { useUserStore } from '../stores'
-import { ENTITLEMENT_ID } from '../services/purchases'
-import type { SubscriptionPlan } from '../types'
-
-interface PurchaseResult {
- success: boolean
- cancelled: boolean
- error?: string
-}
-
-interface UsePurchasesReturn {
- isPremium: boolean
- isLoading: boolean
- monthlyPackage: PurchasesPackage | null
- annualPackage: PurchasesPackage | null
- purchasePackage: (pkg: PurchasesPackage) => Promise
- restorePurchases: () => Promise
-}
-
-/**
- * Helper to check if user has premium entitlement
- */
-function hasPremiumEntitlement(info: CustomerInfo | null): boolean {
- if (!info) return false
- return ENTITLEMENT_ID in info.entitlements.active
-}
-
-/**
- * Hook to manage RevenueCat subscriptions
- * Syncs subscription state with userStore
- */
-export function usePurchases(): UsePurchasesReturn {
- const [isLoading, setIsLoading] = useState(true)
- const [offerings, setOfferings] = useState(null)
- const [customerInfo, setCustomerInfo] = useState(null)
-
- const subscription = useUserStore((s) => s.profile.subscription)
- const setSubscription = useUserStore((s) => s.setSubscription)
-
- // Derive premium status from RevenueCat entitlement or local state
- const isPremium = customerInfo
- ? hasPremiumEntitlement(customerInfo)
- : subscription !== 'free'
-
- // Get packages from offerings
- const monthlyPackage = offerings?.current?.monthly ?? null
- const annualPackage = offerings?.current?.annual ?? null
-
- // Sync RevenueCat state to userStore
- const syncSubscriptionToStore = useCallback(
- (info: CustomerInfo | null) => {
- if (!info) return
-
- const hasPremium = hasPremiumEntitlement(info)
- const activeSubscriptions = info.activeSubscriptions
-
- let newPlan: SubscriptionPlan = 'free'
-
- if (hasPremium && activeSubscriptions.length > 0) {
- // Determine plan type from subscription identifier
- const subId = activeSubscriptions[0].toLowerCase()
- if (subId.includes('yearly') || subId.includes('annual')) {
- newPlan = 'premium-yearly'
- } else if (subId.includes('monthly')) {
- newPlan = 'premium-monthly'
- } else {
- // Default to yearly for any premium entitlement
- newPlan = 'premium-yearly'
- }
- }
-
- // Only update if different
- if (subscription !== newPlan) {
- logger.log('[Purchases] Syncing subscription to store:', newPlan)
- setSubscription(newPlan)
- }
- },
- [subscription, setSubscription]
- )
-
- // Keep a stable ref so the mount-only effect can call the latest version
- const syncRef = useRef(syncSubscriptionToStore)
- syncRef.current = syncSubscriptionToStore
-
- // Fetch offerings and customer info on mount
- useEffect(() => {
- const fetchData = async () => {
- try {
- const [offeringsResult, customerInfoResult] = await Promise.all([
- Purchases.getOfferings(),
- Purchases.getCustomerInfo(),
- ])
-
- setOfferings(offeringsResult)
- setCustomerInfo(customerInfoResult)
- syncRef.current(customerInfoResult)
-
- logger.log('[Purchases] Offerings loaded:', {
- hasMonthly: !!offeringsResult.current?.monthly,
- hasAnnual: !!offeringsResult.current?.annual,
- monthlyProductId: offeringsResult.current?.monthly?.product.identifier,
- annualProductId: offeringsResult.current?.annual?.product.identifier,
- monthlyPrice: offeringsResult.current?.monthly?.product.priceString,
- annualPrice: offeringsResult.current?.annual?.product.priceString,
- })
- } catch (error) {
- logger.error('[Purchases] Failed to fetch offerings/customerInfo:', error)
- } finally {
- setIsLoading(false)
- }
- }
-
- fetchData()
- }, []) // empty deps — only runs once per mount
-
- // Listen for customer info changes (renewals, expirations, etc.)
- useEffect(() => {
- const listener = (info: CustomerInfo) => {
- logger.log('[Purchases] Customer info updated')
- setCustomerInfo(info)
- syncRef.current(info)
- }
-
- Purchases.addCustomerInfoUpdateListener(listener)
-
- return () => {
- Purchases.removeCustomerInfoUpdateListener(listener)
- }
- }, [])
-
- // Purchase a package
- const purchasePackage = useCallback(
- async (pkg: PurchasesPackage): Promise => {
- try {
- logger.log('[Purchases] Starting purchase for:', pkg.identifier, pkg.product.identifier)
- const { customerInfo: newInfo } = await Purchases.purchasePackage(pkg)
-
- setCustomerInfo(newInfo)
- syncSubscriptionToStore(newInfo)
-
- logger.log('[Purchases] Active entitlements:', Object.keys(newInfo.entitlements.active))
- logger.log('[Purchases] All entitlements:', Object.keys(newInfo.entitlements.all))
- logger.log('[Purchases] Looking for entitlement:', ENTITLEMENT_ID)
- const success = hasPremiumEntitlement(newInfo)
- logger.log('[Purchases] Purchase result:', { success })
-
- return { success, cancelled: false }
- } catch (error: unknown) {
- const purchaseError = error as { userCancelled?: boolean; message?: string }
- // Handle user cancellation
- if (purchaseError.userCancelled) {
- logger.log('[Purchases] Purchase cancelled by user')
- return { success: false, cancelled: true }
- }
-
- logger.error('[Purchases] Purchase error:', error)
-
- // DEV mode: offer to simulate the purchase when StoreKit fails
- if (__DEV__) {
- return new Promise((resolve) => {
- Alert.alert(
- 'Purchase failed (DEV)',
- `StoreKit error: ${purchaseError.message || 'Unknown error'}\n\nProduct: ${pkg.product.identifier}\n\nSimulate a successful purchase for testing?`,
- [
- {
- text: 'Cancel',
- style: 'cancel',
- onPress: () =>
- resolve({ success: false, cancelled: true }),
- },
- {
- text: 'Simulate Purchase',
- onPress: () => {
- // Determine plan from package identifier
- const id = pkg.product.identifier.toLowerCase()
- const plan: SubscriptionPlan =
- id.includes('annual') || id.includes('year')
- ? 'premium-yearly'
- : 'premium-monthly'
- setSubscription(plan)
-
- // DEV: Create mock customerInfo so isPremium returns true
- const mockCustomerInfo = {
- entitlements: {
- active: {
- [ENTITLEMENT_ID]: {
- identifier: ENTITLEMENT_ID,
- isActive: true,
- willRenew: true,
- periodType: 'NORMAL',
- latestPurchaseDate: new Date().toISOString(),
- originalPurchaseDate: new Date().toISOString(),
- expirationDate: null,
- store: 'APP_STORE',
- productIdentifier: plan === 'premium-yearly' ? 'tabatafit.premium.yearly' : 'tabatafit.premium.monthly',
- isSandbox: true,
- }
- }
- },
- activeSubscriptions: [plan === 'premium-yearly' ? 'tabatafit.premium.yearly' : 'tabatafit.premium.monthly'],
- allPurchasedProductIdentifiers: [plan === 'premium-yearly' ? 'tabatafit.premium.yearly' : 'tabatafit.premium.monthly'],
- firstSeen: new Date().toISOString(),
- originalAppUserId: 'dev-user',
- originalApplicationVersion: '1.0',
- requestDate: new Date().toISOString(),
- managementURL: null,
- nonSubscriptionTransactions: [],
- } as unknown as CustomerInfo
-
- setCustomerInfo(mockCustomerInfo)
- syncSubscriptionToStore(mockCustomerInfo)
-
- logger.log('[Purchases] DEV: Simulated purchase →', plan)
- resolve({ success: true, cancelled: false })
- },
- },
- ]
- )
- })
- }
-
- return {
- success: false,
- cancelled: false,
- error: purchaseError.message || 'Purchase failed',
- }
- }
- },
- [syncSubscriptionToStore, setSubscription]
- )
-
- // Restore purchases
- const restorePurchases = useCallback(async (): Promise => {
- try {
- logger.log('[Purchases] Restoring purchases...')
- const restoredInfo = await Purchases.restorePurchases()
-
- setCustomerInfo(restoredInfo)
- syncSubscriptionToStore(restoredInfo)
-
- const hasPremium = hasPremiumEntitlement(restoredInfo)
- logger.log('[Purchases] Restore result:', { hasPremium })
-
- return hasPremium
- } catch (error) {
- logger.error('[Purchases] Restore failed:', error)
-
- // DEV mode: offer to simulate restore
- if (__DEV__) {
- return new Promise((resolve) => {
- Alert.alert(
- 'Restore failed (DEV)',
- 'Simulate a restored premium subscription?',
- [
- { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
- {
- text: 'Simulate Restore',
- onPress: () => {
- const plan: SubscriptionPlan = 'premium-yearly'
- setSubscription(plan)
-
- // DEV: Create mock customerInfo so isPremium returns true
- const mockCustomerInfo = {
- entitlements: {
- active: {
- [ENTITLEMENT_ID]: {
- identifier: ENTITLEMENT_ID,
- isActive: true,
- willRenew: true,
- periodType: 'NORMAL',
- latestPurchaseDate: new Date().toISOString(),
- originalPurchaseDate: new Date().toISOString(),
- expirationDate: null,
- store: 'APP_STORE',
- productIdentifier: 'tabatafit.premium.yearly',
- isSandbox: true,
- }
- }
- },
- activeSubscriptions: ['tabatafit.premium.yearly'],
- allPurchasedProductIdentifiers: ['tabatafit.premium.yearly'],
- firstSeen: new Date().toISOString(),
- originalAppUserId: 'dev-user',
- originalApplicationVersion: '1.0',
- requestDate: new Date().toISOString(),
- managementURL: null,
- nonSubscriptionTransactions: [],
- } as unknown as CustomerInfo
-
- setCustomerInfo(mockCustomerInfo)
- syncSubscriptionToStore(mockCustomerInfo)
-
- logger.log('[Purchases] DEV: Simulated restore → premium-yearly')
- resolve(true)
- },
- },
- ]
- )
- })
- }
-
- return false
- }
- }, [syncSubscriptionToStore, setSubscription])
-
- return {
- isPremium,
- isLoading,
- monthlyPackage,
- annualPackage,
- purchasePackage,
- restorePurchases,
- }
-}
diff --git a/src/shared/hooks/useTabataTimer.ts b/src/shared/hooks/useTabataTimer.ts
deleted file mode 100644
index 3a08bf5..0000000
--- a/src/shared/hooks/useTabataTimer.ts
+++ /dev/null
@@ -1,391 +0,0 @@
-/**
- * Tabata Timer Hook
- * Multi-block timer supporting warmup, tabata blocks, inter-block rest, and cooldown
- *
- * State machine:
- * WARMUP → BLOCK(WORK↔REST × rounds) → INTER_BLOCK_REST → next block → COOLDOWN → COMPLETE
- */
-
-import { useRef, useEffect, useCallback, useMemo } from 'react'
-import { create } from 'zustand'
-import type { TabataSession, TabataExercise, TabataTimerPhase, TabataBlock, TimedMovement } from '../types/program'
-
-// ─── Tabata Player Store ─────────────────────────────────────────
-
-interface TabataPlayerState {
- session: TabataSession | null
- phase: TabataTimerPhase
- timeRemaining: number
- currentBlockIndex: number
- currentRound: number
- currentWarmupIndex: number
- currentCooldownIndex: number
- isPaused: boolean
- isRunning: boolean
- calories: number
- startedAt: number | null
-
- loadSession: (session: TabataSession) => void
- setPhase: (phase: TabataTimerPhase) => void
- setTimeRemaining: (time: number) => void
- setCurrentBlock: (index: number) => void
- setCurrentRound: (round: number) => void
- setWarmupIndex: (index: number) => void
- setCooldownIndex: (index: number) => void
- setPaused: (paused: boolean) => void
- setRunning: (running: boolean) => void
- addCalories: (amount: number) => void
- reset: () => void
-}
-
-const INITIAL_STATE = {
- session: null as TabataSession | null,
- phase: 'WARMUP' as TabataTimerPhase,
- timeRemaining: 0,
- currentBlockIndex: 0,
- currentRound: 1,
- currentWarmupIndex: 0,
- currentCooldownIndex: 0,
- isPaused: false,
- isRunning: false,
- calories: 0,
- startedAt: null as number | null,
-}
-
-export const useTabataPlayerStore = create((set) => ({
- ...INITIAL_STATE,
-
- loadSession: (session) =>
- set({
- session,
- phase: 'WARMUP',
- timeRemaining: session.warmup.movements[0]?.duration ?? 0,
- currentBlockIndex: 0,
- currentRound: 1,
- currentWarmupIndex: 0,
- currentCooldownIndex: 0,
- isPaused: false,
- isRunning: false,
- calories: 0,
- startedAt: null,
- }),
-
- setPhase: (phase) => set({ phase }),
- setTimeRemaining: (time) => set({ timeRemaining: time }),
- setCurrentBlock: (index) => set({ currentBlockIndex: index }),
- setCurrentRound: (round) => set({ currentRound: round }),
- setWarmupIndex: (index) => set({ currentWarmupIndex: index }),
- setCooldownIndex: (index) => set({ currentCooldownIndex: index }),
- setPaused: (paused) => set({ isPaused: paused }),
- setRunning: (running) =>
- set((state) => ({
- isRunning: running,
- startedAt: running && !state.startedAt ? Date.now() : state.startedAt,
- })),
- addCalories: (amount) => set((state) => ({ calories: state.calories + amount })),
- reset: () => set(INITIAL_STATE),
-}))
-
-// ─── Hook Return Type ──────────────────────────────────────────
-
-export interface UseTabataTimerReturn {
- phase: TabataTimerPhase
- timeRemaining: number
- currentRound: number
- totalRounds: number
- currentBlockIndex: number
- totalBlocks: number
- currentExercise: TabataExercise | null
- nextExercise: TabataExercise | null
- currentConseil: string
- isOddRound: boolean
- progress: number
- isPaused: boolean
- isRunning: boolean
- isComplete: boolean
- calories: number
- currentWarmupMovement: TimedMovement | null
- currentCooldownMovement: TimedMovement | null
- start: () => void
- pause: () => void
- resume: () => void
- skip: () => void
- stop: () => void
-}
-
-// ─── Constants ─────────────────────────────────────────────────
-
-const INTER_BLOCK_REST_SECONDS = 60
-
-// ─── Hook ──────────────────────────────────────────────────────
-
-export function useTabataTimer(session: TabataSession | null): UseTabataTimerReturn {
- const store = useTabataPlayerStore()
- const intervalRef = useRef | null>(null)
-
- // Load session on mount
- useEffect(() => {
- if (session) {
- store.loadSession(session)
- }
- return () => {
- if (intervalRef.current) clearInterval(intervalRef.current)
- }
- }, [session?.id])
-
- const s = session
- const currentBlock: TabataBlock | null = s?.blocks[store.currentBlockIndex] ?? null
-
- // ─── Computed values ─────────────────────────────────────────
-
- const isOddRound = store.currentRound % 2 === 1
-
- const currentExercise: TabataExercise | null = useMemo(() => {
- if (!currentBlock) return null
- return isOddRound ? currentBlock.oddExercise : currentBlock.evenExercise
- }, [currentBlock, isOddRound])
-
- const currentConseil = currentExercise?.conseil ?? ''
-
- const nextExercise: TabataExercise | null = useMemo(() => {
- if (!currentBlock) return null
- const nextOdd = !isOddRound
- return nextOdd ? currentBlock.oddExercise : currentBlock.evenExercise
- }, [currentBlock, isOddRound])
-
- const totalRounds = s?.totalRounds ?? 0
-
- const progress = useMemo(() => {
- if (!s || store.phase === 'COMPLETE') return 1
- if (store.phase === 'WARMUP' || store.phase === 'COOLDOWN') return 0
-
- // Progress across all blocks
- const completedBlockRounds = store.currentBlockIndex * (currentBlock?.rounds ?? 8)
- const total = s.totalRounds
- if (total === 0) return 0
- return (completedBlockRounds + store.currentRound - 1) / total
- }, [s, store.phase, store.currentBlockIndex, store.currentRound, currentBlock])
-
- const currentWarmupMovement: TimedMovement | null =
- s?.warmup.movements[store.currentWarmupIndex] ?? null
-
- const currentCooldownMovement: TimedMovement | null =
- s?.cooldown.movements[store.currentCooldownIndex] ?? null
-
- // ─── Phase durations ─────────────────────────────────────────
-
- const phaseDuration = useMemo(() => {
- switch (store.phase) {
- case 'WARMUP':
- return currentWarmupMovement?.duration ?? 0
- case 'WORK':
- return currentBlock?.workTime ?? 20
- case 'REST':
- return currentBlock?.restTime ?? 10
- case 'INTER_BLOCK_REST':
- return INTER_BLOCK_REST_SECONDS
- case 'COOLDOWN':
- return currentCooldownMovement?.duration ?? 0
- case 'COMPLETE':
- return 0
- }
- }, [store.phase, currentBlock, currentWarmupMovement, currentCooldownMovement])
-
- // ─── Timer tick ──────────────────────────────────────────────
- // Uses recursive setTimeout so the last second (showing "1")
- // only holds for 600ms before transitioning, instead of the full 1000ms.
-
- useEffect(() => {
- if (!store.isRunning || store.isPaused || store.phase === 'COMPLETE' || !s) {
- if (intervalRef.current) {
- clearTimeout(intervalRef.current)
- intervalRef.current = null
- }
- return
- }
-
- function scheduleTick() {
- const state = useTabataPlayerStore.getState()
- const isLastSecond = state.timeRemaining <= 1.9
- const delay = isLastSecond ? 600 : 1000
-
- intervalRef.current = setTimeout(() => {
- const curState = useTabataPlayerStore.getState()
- if (!curState.isRunning || curState.isPaused || curState.phase === 'COMPLETE' || !s) return
-
- if (curState.timeRemaining <= 1.9) {
- transitionPhase(curState, s)
- // Don't reschedule — phase change triggers effect re-run
- } else {
- useTabataPlayerStore.getState().setTimeRemaining(curState.timeRemaining - 1)
- scheduleTick()
- }
- }, delay)
- }
-
- scheduleTick()
-
- return () => {
- if (intervalRef.current) {
- clearTimeout(intervalRef.current)
- intervalRef.current = null
- }
- }
- }, [store.isRunning, store.isPaused, store.phase, s?.id])
-
- // ─── Phase transitions ───────────────────────────────────────
-
- function transitionPhase(state: TabataPlayerState, session: TabataSession) {
- const { phase, currentBlockIndex, currentRound } = state
- const block = session.blocks[currentBlockIndex]
-
- switch (phase) {
- case 'WARMUP': {
- const nextIdx = state.currentWarmupIndex + 1
- if (nextIdx < session.warmup.movements.length) {
- // Next warmup movement
- useTabataPlayerStore.getState().setWarmupIndex(nextIdx)
- useTabataPlayerStore.getState().setTimeRemaining(session.warmup.movements[nextIdx].duration)
- } else {
- // Warmup done → start first block
- useTabataPlayerStore.getState().setPhase('WORK')
- useTabataPlayerStore.getState().setTimeRemaining(block?.workTime ?? 20)
- }
- break
- }
-
- case 'WORK': {
- // Add calories
- const caloriesPerRound = session.calories / session.totalRounds
- useTabataPlayerStore.getState().addCalories(Math.round(caloriesPerRound))
-
- useTabataPlayerStore.getState().setPhase('REST')
- useTabataPlayerStore.getState().setTimeRemaining(block?.restTime ?? 10)
- break
- }
-
- case 'REST': {
- const blockRounds = block?.rounds ?? 8
- if (currentRound >= blockRounds) {
- // Block complete
- const nextBlockIdx = currentBlockIndex + 1
- if (nextBlockIdx < session.blocks.length) {
- // Inter-block rest
- useTabataPlayerStore.getState().setPhase('INTER_BLOCK_REST')
- useTabataPlayerStore.getState().setTimeRemaining(INTER_BLOCK_REST_SECONDS)
- useTabataPlayerStore.getState().setCurrentBlock(nextBlockIdx)
- useTabataPlayerStore.getState().setCurrentRound(1)
- } else {
- // All blocks done → cooldown
- useTabataPlayerStore.getState().setPhase('COOLDOWN')
- useTabataPlayerStore.getState().setTimeRemaining(session.cooldown.movements[0]?.duration ?? 0)
- useTabataPlayerStore.getState().setCooldownIndex(0)
- }
- } else {
- // Next round in same block
- useTabataPlayerStore.getState().setPhase('WORK')
- useTabataPlayerStore.getState().setTimeRemaining(block?.workTime ?? 20)
- useTabataPlayerStore.getState().setCurrentRound(currentRound + 1)
- }
- break
- }
-
- case 'INTER_BLOCK_REST': {
- useTabataPlayerStore.getState().setPhase('WORK')
- useTabataPlayerStore.getState().setTimeRemaining(
- session.blocks[currentBlockIndex]?.workTime ?? 20
- )
- break
- }
-
- case 'COOLDOWN': {
- const nextIdx = state.currentCooldownIndex + 1
- if (nextIdx < session.cooldown.movements.length) {
- useTabataPlayerStore.getState().setCooldownIndex(nextIdx)
- useTabataPlayerStore.getState().setTimeRemaining(session.cooldown.movements[nextIdx].duration)
- } else {
- // Complete
- useTabataPlayerStore.getState().setPhase('COMPLETE')
- useTabataPlayerStore.getState().setTimeRemaining(0)
- useTabataPlayerStore.getState().setRunning(false)
- }
- break
- }
- }
- }
-
- // ─── Controls ────────────────────────────────────────────────
-
- const start = useCallback(() => {
- store.setRunning(true)
- store.setPaused(false)
- }, [])
-
- const pause = useCallback(() => {
- store.setPaused(true)
- }, [])
-
- const resume = useCallback(() => {
- store.setPaused(false)
- }, [])
-
- const skip = useCallback(() => {
- const state = useTabataPlayerStore.getState()
- if (!s) return
-
- switch (state.phase) {
- case 'WARMUP':
- // Skip to first block WORK
- useTabataPlayerStore.getState().setPhase('WORK')
- useTabataPlayerStore.getState().setTimeRemaining(s.blocks[0]?.workTime ?? 20)
- break
- case 'WORK':
- useTabataPlayerStore.getState().setPhase('REST')
- useTabataPlayerStore.getState().setTimeRemaining(currentBlock?.restTime ?? 10)
- break
- case 'REST':
- transitionPhase(state, s)
- break
- case 'INTER_BLOCK_REST':
- useTabataPlayerStore.getState().setPhase('WORK')
- useTabataPlayerStore.getState().setTimeRemaining(
- s.blocks[state.currentBlockIndex]?.workTime ?? 20
- )
- break
- case 'COOLDOWN':
- useTabataPlayerStore.getState().setPhase('COMPLETE')
- useTabataPlayerStore.getState().setTimeRemaining(0)
- useTabataPlayerStore.getState().setRunning(false)
- break
- }
- }, [s, currentBlock])
-
- const stop = useCallback(() => {
- useTabataPlayerStore.getState().reset()
- }, [])
-
- return {
- phase: store.phase,
- timeRemaining: store.timeRemaining,
- currentRound: store.currentRound,
- totalRounds,
- currentBlockIndex: store.currentBlockIndex,
- totalBlocks: s?.blocks.length ?? 0,
- currentExercise,
- nextExercise,
- currentConseil,
- isOddRound,
- progress,
- isPaused: store.isPaused,
- isRunning: store.isRunning,
- isComplete: store.phase === 'COMPLETE',
- calories: store.calories,
- currentWarmupMovement,
- currentCooldownMovement,
- start,
- pause,
- resume,
- skip,
- stop,
- }
-}
diff --git a/src/shared/hooks/useTimer.ts b/src/shared/hooks/useTimer.ts
deleted file mode 100644
index 518ba79..0000000
--- a/src/shared/hooks/useTimer.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-/**
- * TabataFit Timer Hook
- * Extracted from player/[id].tsx — reusable timer logic
- */
-
-import { useRef, useEffect, useCallback } from 'react'
-import { usePlayerStore } from '../stores'
-import i18n from '@/src/shared/i18n'
-import type { Workout } from '../types'
-
-export type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
-
-interface UseTimerReturn {
- phase: TimerPhase
- timeRemaining: number
- currentRound: number
- totalRounds: number
- currentExercise: string
- nextExercise: string | undefined
- progress: number
- isPaused: boolean
- isRunning: boolean
- isComplete: boolean
- calories: number
- start: () => void
- pause: () => void
- resume: () => void
- skip: () => void
- stop: () => void
-}
-
-export function useTimer(workout: Workout | null): UseTimerReturn {
- const store = usePlayerStore()
- const intervalRef = useRef | null>(null)
-
- // Load workout into store on mount
- useEffect(() => {
- if (workout) {
- store.loadWorkout(workout)
- }
- return () => {
- if (intervalRef.current) clearInterval(intervalRef.current)
- }
- }, [workout?.id])
-
- const w = workout ?? {
- prepTime: 10,
- workTime: 20,
- restTime: 10,
- rounds: 8,
- exercises: [{ name: i18n.t('screens:player.phases.prep'), duration: 20 }],
- }
-
- // Calculate phase duration
- const phaseDuration =
- store.phase === 'PREP' ? w.prepTime :
- store.phase === 'WORK' ? w.workTime :
- store.phase === 'REST' ? w.restTime : 0
-
- const progress = phaseDuration > 0 ? 1 - store.timeRemaining / phaseDuration : 1
-
- // Exercise index based on current round
- const exerciseIndex = (store.currentRound - 1) % w.exercises.length
- const currentExercise = w.exercises[exerciseIndex]?.name ?? ''
- const nextExercise = w.exercises[(exerciseIndex + 1) % w.exercises.length]?.name
-
- // Timer tick — uses recursive setTimeout so the last second (showing "1")
- // only holds for 600ms before transitioning, instead of the full 1000ms.
- useEffect(() => {
- if (!store.isRunning || store.isPaused || store.phase === 'COMPLETE') {
- if (intervalRef.current) {
- clearTimeout(intervalRef.current)
- intervalRef.current = null
- }
- return
- }
-
- function scheduleTick() {
- const state = usePlayerStore.getState()
- const isLastSecond = state.timeRemaining <= 1.9
- const delay = isLastSecond ? 600 : 1000
-
- intervalRef.current = setTimeout(() => {
- const s = usePlayerStore.getState()
- if (!s.isRunning || s.isPaused || s.phase === 'COMPLETE') return
-
- if (s.timeRemaining <= 1.9) {
- // Phase transition
- if (s.phase === 'PREP') {
- store.setPhase('WORK')
- store.setTimeRemaining(w.workTime)
- } else if (s.phase === 'WORK') {
- const caloriesPerRound = workout ? Math.round(workout.calories / workout.rounds) : 5
- store.addCalories(caloriesPerRound)
- store.setPhase('REST')
- store.setTimeRemaining(w.restTime)
- } else if (s.phase === 'REST') {
- if (s.currentRound >= (workout?.rounds ?? 8)) {
- store.setPhase('COMPLETE')
- store.setTimeRemaining(0)
- store.setRunning(false)
- } else {
- store.setPhase('WORK')
- store.setTimeRemaining(w.workTime)
- store.setCurrentRound(s.currentRound + 1)
- }
- }
- // Don't reschedule — phase change triggers effect re-run
- } else {
- store.setTimeRemaining(s.timeRemaining - 1)
- scheduleTick()
- }
- }, delay)
- }
-
- scheduleTick()
-
- return () => {
- if (intervalRef.current) {
- clearTimeout(intervalRef.current)
- intervalRef.current = null
- }
- }
- }, [store.isRunning, store.isPaused, store.phase])
-
- const start = useCallback(() => {
- store.setRunning(true)
- store.setPaused(false)
- }, [])
-
- const pause = useCallback(() => {
- store.setPaused(true)
- }, [])
-
- const resume = useCallback(() => {
- store.setPaused(false)
- }, [])
-
- const skip = useCallback(() => {
- const s = usePlayerStore.getState()
- if (s.phase === 'PREP') {
- store.setPhase('WORK')
- store.setTimeRemaining(w.workTime)
- } else if (s.phase === 'WORK') {
- store.setPhase('REST')
- store.setTimeRemaining(w.restTime)
- } else if (s.phase === 'REST') {
- if (s.currentRound >= (workout?.rounds ?? 8)) {
- store.setPhase('COMPLETE')
- store.setTimeRemaining(0)
- store.setRunning(false)
- } else {
- store.setPhase('WORK')
- store.setTimeRemaining(w.workTime)
- store.setCurrentRound(s.currentRound + 1)
- }
- }
- }, [])
-
- const stop = useCallback(() => {
- store.reset()
- }, [])
-
- return {
- phase: store.phase as TimerPhase,
- timeRemaining: store.timeRemaining,
- currentRound: store.currentRound,
- totalRounds: workout?.rounds ?? 8,
- currentExercise,
- nextExercise: store.phase === 'REST' ? nextExercise : undefined,
- progress,
- isPaused: store.isPaused,
- isRunning: store.isRunning,
- isComplete: store.phase === 'COMPLETE',
- calories: store.calories,
- start,
- pause,
- resume,
- skip,
- stop,
- }
-}
diff --git a/src/shared/i18n/index.ts b/src/shared/i18n/index.ts
deleted file mode 100644
index d9286ad..0000000
--- a/src/shared/i18n/index.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * TabataFit i18n Setup
- * expo-localization + i18next + react-i18next
- * Auto-detects device locale, falls back to English
- */
-
-import i18n from 'i18next'
-import { initReactI18next } from 'react-i18next'
-import { getLocales } from 'expo-localization'
-
-// EN
-import enCommon from './locales/en/common.json'
-import enScreens from './locales/en/screens.json'
-import enContent from './locales/en/content.json'
-import enNotifications from './locales/en/notifications.json'
-
-// FR
-import frCommon from './locales/fr/common.json'
-import frScreens from './locales/fr/screens.json'
-import frContent from './locales/fr/content.json'
-import frNotifications from './locales/fr/notifications.json'
-
-// ES
-import esCommon from './locales/es/common.json'
-import esScreens from './locales/es/screens.json'
-import esContent from './locales/es/content.json'
-import esNotifications from './locales/es/notifications.json'
-
-// DE
-import deCommon from './locales/de/common.json'
-import deScreens from './locales/de/screens.json'
-import deContent from './locales/de/content.json'
-import deNotifications from './locales/de/notifications.json'
-
-const deviceLanguage = getLocales()[0]?.languageCode ?? 'en'
-
-i18n
- .use(initReactI18next)
- .init({
- compatibilityJSON: 'v4',
- lng: deviceLanguage,
- fallbackLng: 'en',
- ns: ['common', 'screens', 'content', 'notifications'],
- defaultNS: 'common',
- resources: {
- en: {
- common: enCommon,
- screens: enScreens,
- content: enContent,
- notifications: enNotifications,
- },
- fr: {
- common: frCommon,
- screens: frScreens,
- content: frContent,
- notifications: frNotifications,
- },
- es: {
- common: esCommon,
- screens: esScreens,
- content: esContent,
- notifications: esNotifications,
- },
- de: {
- common: deCommon,
- screens: deScreens,
- content: deContent,
- notifications: deNotifications,
- },
- },
- interpolation: {
- escapeValue: false,
- },
- })
-
-export default i18n
diff --git a/src/shared/i18n/locales/de/CLAUDE.md b/src/shared/i18n/locales/de/CLAUDE.md
deleted file mode 100644
index ab70ec0..0000000
--- a/src/shared/i18n/locales/de/CLAUDE.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 20, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5391 | 10:20 PM | 🟣 | RevenueCat subscription system fully integrated and tested | ~656 |
-| #5384 | 9:28 PM | 🟣 | Implemented RevenueCat subscription system | ~190 |
-| #5374 | 7:54 PM | 🟣 | German Localization Added for Paywall Restore Purchases | ~184 |
-| #5373 | " | 🟣 | German Localization Added for Profile Subscription Section | ~163 |
-
\ No newline at end of file
diff --git a/src/shared/i18n/locales/de/common.json b/src/shared/i18n/locales/de/common.json
deleted file mode 100644
index 11360f7..0000000
--- a/src/shared/i18n/locales/de/common.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "start": "START",
- "continue": "Weiter",
- "next": "Weiter",
- "done": "Fertig",
- "seeAll": "Alle anzeigen",
- "back": "Zurück",
- "share": "Teilen",
- "cancel": "Abbrechen",
- "save": "Speichern",
- "loading": "Laden...",
- "offline": "Keine Internetverbindung",
-
- "levels": {
- "beginner": "Anfänger",
- "intermediate": "Fortgeschritten",
- "advanced": "Profi"
- },
-
- "categories": {
- "all": "Alle",
- "fullBody": "Ganzkörper",
- "core": "Core",
- "upperBody": "Oberkörper",
- "lowerBody": "Unterkörper",
- "cardio": "Cardio"
- },
-
- "days": {
- "sun": "So",
- "mon": "Mo",
- "tue": "Di",
- "wed": "Mi",
- "thu": "Do",
- "fri": "Fr",
- "sat": "Sa"
- },
-
- "greetings": {
- "morning": "Guten Morgen",
- "afternoon": "Guten Tag",
- "evening": "Guten Abend"
- },
-
- "units": {
- "min": "Min",
- "cal": "kcal",
- "minUnit": "{{count}} Min",
- "calUnit": "{{count}} kcal",
- "sWork": "{{count}}s Arbeit",
- "perYear": "pro Jahr",
- "perMonth": "pro Monat",
- "perWeek": "/Woche"
- },
-
- "plurals": {
- "workout_one": "{{count}} Workout",
- "workout_other": "{{count}} Workouts",
- "day_one": "{{count}} Tag",
- "day_other": "{{count}} Tage",
- "round_one": "{{count}} Runde",
- "round_other": "{{count}} Runden",
- "week_one": "{{count}} Woche",
- "week_other": "{{count}} Wochen"
- },
-
- "workoutMeta": "{{duration}} Min \u00B7 {{level}} \u00B7 {{calories}} kcal",
- "durationLevel": "{{duration}} Min \u00B7 {{level}}",
- "calMin": "{{calories}} kcal \u00B7 {{duration}} Min",
-
- "musicVibes": {
- "electronic": "Electronic",
- "hipHop": "Hip-Hop",
- "pop": "Pop",
- "rock": "Rock",
- "chill": "Chill"
- }
-}
diff --git a/src/shared/i18n/locales/de/content.json b/src/shared/i18n/locales/de/content.json
deleted file mode 100644
index e86a0cf..0000000
--- a/src/shared/i18n/locales/de/content.json
+++ /dev/null
@@ -1,322 +0,0 @@
-{
- "workouts": {
- "1": "Ganzkörper Ignite",
- "2": "Total Body Blast",
- "3": "Power Surge",
- "4": "Morgen-Weckruf",
- "5": "Ausdauer-Aufbau",
- "6": "Schneller Burn",
- "7": "Funktioneller Flow",
- "8": "Athletische Power",
- "9": "Schweiß-Session",
- "10": "Total Tone",
- "11": "Core Crusher",
- "12": "Bauch-Shredder",
- "13": "Core Grundlagen",
- "14": "Schräge-Inferno",
- "15": "Core Ausdauer",
- "16": "Sanftes Core",
- "17": "Core Power",
- "18": "360 Core",
- "19": "Core Stabilität",
- "20": "Core Marathon",
- "21": "Oberkörper Blitz",
- "22": "Arm-Sculptor",
- "23": "Liegestütz-Meisterung",
- "24": "Schulter-Shredder",
- "25": "Brust & Rücken",
- "26": "Eigengewicht Oberkörper",
- "27": "Kraft Tabata",
- "28": "Straffen & Definieren",
- "29": "Power-Arme",
- "30": "Oberkörper Ausdauer",
- "31": "Unterkörper Burn",
- "32": "Beintag Tabata",
- "33": "Gesäß-Aktivierung",
- "34": "Explosive Beine",
- "35": "Kniebeugen Challenge",
- "36": "Unterkörper Power",
- "37": "Knieschonende Beine",
- "38": "Sprint Tabata",
- "39": "Beine & Po",
- "40": "Beintag Marathon",
- "41": "HIIT Extrem",
- "42": "Cardio Blast",
- "43": "Dance Cardio",
- "44": "Fettverbrennung Express",
- "45": "Low Impact Cardio",
- "46": "Cardio Inferno",
- "47": "Sonnenaufgang Flow",
- "48": "Power Hour",
- "49": "Tiefe Dehnung",
- "50": "Cardio Marathon"
- },
- "exercises": {
- "jumping-jacks": "Hampelmänner",
- "squats": "Kniebeugen",
- "push-ups": "Liegestütze",
- "high-knees": "Kniehebelauf",
- "burpees": "Burpees",
- "lunges": "Ausfallschritte",
- "push-up-to-row": "Liegestütz mit Rudern",
- "mountain-climbers": "Bergsteiger",
- "thrusters": "Thrusters",
- "box-jumps": "Box Jumps",
- "renegade-rows": "Renegade Rows",
- "tuck-jumps": "Hocksprünge",
- "arm-circles": "Armkreisen",
- "bodyweight-squats": "Kniebeugen",
- "knee-push-ups": "Knie-Liegestütze",
- "marching-in-place": "Marschieren auf der Stelle",
- "devil-press": "Devil Press",
- "squat-jumps": "Sprung-Kniebeugen",
- "push-up-burpees": "Liegestütz-Burpees",
- "plank-jacks": "Plank Jacks",
- "star-jumps": "Sternsprünge",
- "wall-sit": "Wandsitzen",
- "tricep-dips": "Trizeps-Dips",
- "butt-kicks": "Anfersen",
- "inchworms": "Inchworms",
- "curtsy-lunges": "Knicks-Ausfallschritte",
- "bear-crawls": "Bärenkrabbeln",
- "squat-pulses": "Kniebeugen-Pulse",
- "clean-and-press": "Clean & Press",
- "lateral-bounds": "Seitsprünge",
- "slam-ball": "Slam Ball",
- "sprawls": "Sprawls",
- "jumping-lunges": "Sprung-Ausfallschritte",
- "push-up-to-t": "Liegestütz zum T",
- "speed-skaters": "Speed Skaters",
- "plank-shoulder-taps": "Plank Schultertippen",
- "side-lunges": "Seitliche Ausfallschritte",
- "diamond-push-ups": "Diamant-Liegestütze",
- "glute-bridges": "Gesäßbrücke",
- "toe-touches": "Zehenberührungen",
- "crunches": "Crunches",
- "russian-twists": "Russian Twists",
- "leg-raises": "Beinheben",
- "plank-hold": "Plank halten",
- "v-ups": "V-Ups",
- "bicycle-crunches": "Fahrrad-Crunches",
- "dragon-flags": "Dragon Flags",
- "flutter-kicks": "Flatterkicks",
- "dead-bug": "Dead Bug",
- "bird-dog": "Bird Dog",
- "side-plank-l": "Seitplank (L)",
- "side-plank-r": "Seitplank (R)",
- "woodchoppers": "Holzhacker",
- "side-crunches": "Seitliche Crunches",
- "windshield-wipers": "Scheibenwischer",
- "plank-hip-dips": "Plank Hüftsenken",
- "hollow-body-hold": "Hollow Body Hold",
- "turkish-get-ups": "Turkish Get-ups",
- "ab-rollouts": "Bauchrad-Rollouts",
- "pallof-press": "Pallof Press",
- "pelvic-tilts": "Beckenkippen",
- "modified-crunches": "Modifizierte Crunches",
- "supine-leg-march": "Rückenlage Beinmarsch",
- "cat-cow": "Katze-Kuh",
- "plank-to-push-up": "Plank zu Liegestütz",
- "reverse-crunches": "Umgekehrte Crunches",
- "bear-hold": "Bärenhalten",
- "l-sits": "L-Sits",
- "dragon-flag-negatives": "Dragon Flag Negatives",
- "hanging-knee-raises": "Hängendes Knieheben",
- "plank-walkouts": "Plank Walkouts",
- "forearm-plank": "Unterarm-Plank",
- "slow-bicycle": "Langsamer Fahrrad-Crunch",
- "glute-bridge-march": "Gesäßbrücke Marsch",
- "prone-cobra": "Bauchlage Kobra",
- "stir-the-pot": "Topfrühren",
- "ab-wheel-rollout": "Bauchrad-Rollout",
- "hanging-leg-raises": "Hängendes Beinheben",
- "dumbbell-rows": "Kurzhantel-Rudern",
- "shoulder-press": "Schulterdrücken",
- "bicep-curls": "Bizeps-Curls",
- "tricep-extensions": "Trizeps-Streckung",
- "lateral-raises": "Seitheben",
- "front-raises": "Frontheben",
- "wide-push-ups": "Breite Liegestütze",
- "decline-push-ups": "Decline Liegestütze",
- "explosive-push-ups": "Explosive Liegestütze",
- "arnold-press": "Arnold Press",
- "upright-rows": "Aufrechtes Rudern",
- "face-pulls": "Face Pulls",
- "handstand-hold": "Handstand halten",
- "chest-press": "Brustpresse",
- "bent-over-rows": "Vorgebeugtes Rudern",
- "chest-flyes": "Brustfliegen",
- "pull-ups": "Klimmzüge",
- "tricep-dips-chair": "Trizeps-Dips (Stuhl)",
- "clean-and-jerk": "Clean & Jerk",
- "turkish-get-up": "Turkish Get-up",
- "overhead-press": "Überkopfdrücken",
- "hammer-curls": "Hammer-Curls",
- "overhead-tricep": "Überkopf Trizeps",
- "reverse-flyes": "Reverse Flyes",
- "concentration-curls": "Konzentrations-Curls",
- "skull-crushers": "Skull Crushers",
- "zottman-curls": "Zottman Curls",
- "dips": "Dips",
- "barbell-press": "Langhantel-Drücken",
- "dumbbell-snatch": "Kurzhantel-Reißen",
- "plyo-push-ups": "Plyo Liegestütze",
- "calf-raises": "Wadenheben",
- "jump-squats": "Sprung-Kniebeugen",
- "walking-lunges": "Geh-Ausfallschritte",
- "step-ups": "Step-ups",
- "donkey-kicks": "Donkey Kicks",
- "fire-hydrants": "Fire Hydrants",
- "clamshells": "Clamshells",
- "pistol-squats": "Pistol Squats",
- "broad-jumps": "Weitsprünge",
- "goblet-squats": "Goblet Squats",
- "sumo-squats": "Sumo-Kniebeugen",
- "pulse-squats": "Puls-Kniebeugen",
- "front-squats": "Front-Kniebeugen",
- "romanian-deadlifts": "Rumänisches Kreuzheben",
- "split-squats": "Split Squats",
- "power-cleans": "Power Cleans",
- "standing-kickbacks": "Stehende Kickbacks",
- "side-leg-raises": "Seitliches Beinheben",
- "high-knees-sprint": "Kniehebelauf Sprint",
- "lateral-shuffles": "Seitliches Shuffeln",
- "butt-kick-sprint": "Anfersen Sprint",
- "banded-squats": "Kniebeugen mit Band",
- "hip-thrusts": "Hip Thrusts",
- "lateral-band-walks": "Seitliches Bandgehen",
- "step-back-lunges": "Rückwärts-Ausfallschritte",
- "back-squats": "Back Squats",
- "leg-press-jumps": "Beinpressen-Sprünge",
- "box-jump-overs": "Box Jump Overs",
- "grapevine": "Grapevine",
- "step-touch": "Step Touch",
- "cha-cha-slide": "Cha-Cha Slide",
- "jump-rope": "Seilspringen",
- "step-touches": "Step Touches",
- "boxing-punches": "Box-Schläge",
- "side-steps": "Seitschritte",
- "burpee-tuck-jumps": "Burpee Hocksprünge",
- "sprint-in-place": "Sprint auf der Stelle",
- "plyo-lunges": "Plyo Ausfallschritte",
- "sun-salutation": "Sonnengruß",
- "light-jog": "Leichtes Joggen",
- "arm-swings": "Armschwingen",
- "gentle-twists": "Sanfte Drehungen",
- "double-unders": "Double Unders",
- "box-jump-burpees": "Box Jump Burpees",
- "sprint-intervals": "Sprint-Intervalle",
- "forward-fold": "Vorbeuge",
- "pigeon-pose": "Taube",
- "spinal-twist": "Wirbelsäulendrehung",
- "butterfly-stretch": "Schmetterlingsdehnung"
- },
- "equipment": {
- "no-equipment-required": "Kein Equipment nötig",
- "yoga-mat-optional": "Yogamatte optional",
- "yoga-mat": "Yogamatte",
- "dumbbells-optional": "Kurzhanteln optional",
- "dumbbells": "Kurzhanteln",
- "kettlebell-recommended": "Kettlebell empfohlen",
- "kettlebell": "Kettlebell",
- "resistance-band-optional": "Widerstandsband optional",
- "resistance-band": "Widerstandsband",
- "medicine-ball": "Medizinball",
- "light-dumbbells": "Leichte Kurzhanteln",
- "pull-up-bar": "Klimmzugstange",
- "full-gym-setup": "Voll ausgestattetes Fitnessstudio",
- "box-or-step": "Box oder Stufe",
- "chair-for-balance": "Stuhl für Balance",
- "jump-rope-optional": "Springseil optional",
- "jump-rope": "Springseil",
- "stability-ball": "Gymnastikball",
- "barbell": "Langhantel",
- "dumbbell-optional": "Kurzhantel optional"
- },
- "collections": {
- "morning-energizer": {
- "title": "Morgen-Energie",
- "description": "Starte richtig in den Tag"
- },
- "no-equipment": {
- "title": "Ohne Equipment",
- "description": "Trainiere überall"
- },
- "7-day-burn": {
- "title": "7-Tage Burn Challenge",
- "description": "Verwandlung in einer Woche"
- },
- "quick-intense": {
- "title": "Kurz & Intensiv",
- "description": "Maximale Anstrengung in 4 Minuten"
- },
- "core-focus": {
- "title": "Core-Fokus",
- "description": "Baue ein solides Fundament"
- },
- "leg-day": {
- "title": "Beintag",
- "description": "Überspringe nie den Beintag"
- }
- },
- "programs": {
- "beginner-journey": {
- "title": "Einsteiger-Reise",
- "description": "Deine ersten Schritte in die Tabata-Fitness"
- },
- "strength-builder": {
- "title": "Kraft-Aufbau",
- "description": "Progressiver Kraftaufbau"
- },
- "fat-burn-protocol": {
- "title": "Fettverbrennungs-Protokoll",
- "description": "Maximales Kalorienverbrennungsprogramm"
- },
- "upper-body": {
- "title": "Oberk\u00f6rper",
- "description": "Schultern, Brust, R\u00fccken und Arme mit physiotherapeutischen Progressionen"
- },
- "lower-body": {
- "title": "Unterk\u00f6rper",
- "description": "Kr\u00e4ftige Beine, Ges\u00e4\u00df und H\u00fcften mit gelenkschonenden Bewegungen"
- },
- "full-body": {
- "title": "Ganzk\u00f6rper",
- "description": "Komplette K\u00f6rpertransformation nur mit deinem eigenen K\u00f6rpergewicht"
- }
- },
- "achievements": {
- "first-burn": {
- "title": "Erster Burn",
- "description": "Schließe dein erstes Workout ab"
- },
- "week-warrior": {
- "title": "Wochen-Krieger",
- "description": "7 Tage in Folge"
- },
- "century-club": {
- "title": "Century Club",
- "description": "Verbrenne insgesamt 100 Kalorien"
- },
- "iron-will": {
- "title": "Eiserner Wille",
- "description": "Schließe 10 Workouts ab"
- },
- "tabata-master": {
- "title": "Tabata-Meister",
- "description": "Schließe 50 Workouts ab"
- },
- "marathon-burner": {
- "title": "Marathon-Burner",
- "description": "Trainiere insgesamt 100 Minuten"
- },
- "unstoppable": {
- "title": "Unaufhaltbar",
- "description": "30 Tage in Folge"
- },
- "calorie-crusher": {
- "title": "Kalorien-Crusher",
- "description": "Verbrenne insgesamt 1000 Kalorien"
- }
- }
-}
diff --git a/src/shared/i18n/locales/de/notifications.json b/src/shared/i18n/locales/de/notifications.json
deleted file mode 100644
index cbeb2f8..0000000
--- a/src/shared/i18n/locales/de/notifications.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "dailyReminder": {
- "title": "Zeit für dein Tabata!",
- "body": "4 Minuten, die deinen Tag verändern."
- }
-}
diff --git a/src/shared/i18n/locales/de/screens.json b/src/shared/i18n/locales/de/screens.json
deleted file mode 100644
index afda39a..0000000
--- a/src/shared/i18n/locales/de/screens.json
+++ /dev/null
@@ -1,465 +0,0 @@
-{
- "tabs": {
- "home": "Start",
- "explore": "Entdecken",
- "programs": "Programme",
- "activity": "Aktivität",
- "progression": "Fortschritt",
- "profile": "Profil"
- },
- "home": {
- "readyToCrush": "Bereit, heute alles zu geben?",
- "featured": "EMPFOHLEN",
- "recent": "Zuletzt",
- "popularThisWeek": "Beliebt diese Woche",
- "collections": "Sammlungen",
- "chooseYourPath": "Wähle deinen Weg",
- "continueYourJourney": "Setze deine Reise fort",
- "yourPrograms": "Deine Programme",
- "programsSubtitle": "Entwickelt von Physiotherapeuten",
- "switchProgram": "Programm wechseln",
- "statsStreak": "Serie",
- "statsThisWeek": "Diese Woche",
- "statsMinutes": "Minuten",
- "upperBody": "Oberkörper",
- "lowerBody": "Unterkörper",
- "fullBody": "Ganzkörper",
- "programsByZone": "Programme nach Zone",
- "filterAll": "Alle",
- "tabataCount": "{{count}} Tabatas",
- "freeBadge": "KOSTENLOS",
- "premiumBadge": "PREMIUM",
- "startProgram": "Starten",
- "continueProgram": "Fortsetzen",
- "unlockPremium": "Premium freischalten",
- "tabataPrograms": "Physio Programme",
- "tabataProgramsSubtitle": "Rehabilitation und Physiotherapie Programme",
- "recommendedNext": "Sitzung fortsetzen",
- "mascotStreak": "{{count}}-Tage-Serie{{name}}! 🔥",
- "mascotReady": "Bereit zu trainieren{{name}}?",
- "statsCompleted": "Programme",
- "zoneSubtitle": "3 Level · Anfänger → Fortgeschrittene",
- "zoneDescUpper": "Schultern, Brust, Arme & Rumpfkraft",
- "zoneDescLower": "Beine, Gesäß & Hüftmobilität",
- "zoneDescFull": "Ganzkörperkräftigung & Ausdauer",
- "zoneLevels": "3 Level",
- "zonePrograms": "{{count}} Programme"
- },
- "explore": {
- "title": "Entdecken",
- "collections": "Sammlungen",
- "featured": "Empfohlen",
- "allWorkouts": "Alle Workouts",
- "trainers": "Trainer",
- "noResults": "Keine Workouts gefunden",
- "tryAdjustingFilters": "Versuchen Sie, Ihre Filter oder Suche anzupassen",
- "loading": "Wird geladen...",
- "filterCategory": "Kategorie",
- "filterLevel": "Niveau",
- "filterEquipment": "Ausrüstung",
- "filterDuration": "Dauer",
- "clearFilters": "Löschen",
- "workoutsCount": "{{count}} Workouts",
- "workouts": "Workouts",
- "equipmentOptions": {
- "none": "Ohne Ausrüstung",
- "band": "Widerstandsband",
- "dumbbells": "Hanteln",
- "mat": "Matte"
- },
- "allEquipment": "Alle Ausrüstung",
- "searchPlaceholder": "Workouts, Trainer suchen...",
- "recommendedForYou": "Empfohlen für dich",
- "tryNewCategory": "Probiere etwas Neues",
- "startFirstWorkout": "Schließe dein erstes Workout ab für personalisierte Empfehlungen",
- "filters": "Filter",
- "activeFilters": "{{count}} aktiv",
- "applyFilters": "Anwenden",
- "resetFilters": "Zurücksetzen",
- "errorTitle": "Workouts konnten nicht geladen werden",
- "errorRetry": "Tippe zum Wiederholen",
- "featuredCollection": "Empfohlene Sammlung"
- },
- "activity": {
- "title": "Aktivität",
- "dayStreak": "Tage in Folge",
- "longest": "LÄNGSTE",
- "workouts": "Workouts",
- "minutes": "Minuten",
- "calories": "Kalorien",
- "bestStreak": "Beste Serie",
- "thisWeek": "Diese Woche",
- "ofDays": "{{completed}} von 7 Tagen",
- "recent": "Zuletzt",
- "today": "Heute",
- "yesterday": "Gestern",
- "daysAgo": "vor {{count}} T.",
- "achievements": "Erfolge",
- "emptyTitle": "Noch keine Aktivität",
- "emptySubtitle": "Absolviere dein erstes Workout und deine Statistiken erscheinen hier.",
- "startFirstWorkout": "Starte dein erstes Workout"
- },
- "profile": {
- "title": "Profil",
- "guest": "Gast",
- "memberSince": "Mitglied seit {{date}}",
- "sectionAccount": "KONTO",
- "sectionWorkout": "WORKOUT",
- "sectionNotifications": "BENACHRICHTIGUNGEN",
- "sectionAbout": "ÜBER",
- "sectionSubscription": "ABONNEMENT",
- "email": "E-Mail",
- "plan": "Plan",
- "freePlan": "Kostenlos",
- "restorePurchases": "Käufe wiederherstellen",
- "hapticFeedback": "Haptisches Feedback",
- "soundEffects": "Soundeffekte",
- "voiceCoaching": "Sprachcoaching",
- "dailyReminders": "Tägliche Erinnerungen",
- "reminderTime": "Erinnerungszeit",
- "reminderFooter": "Erhalte eine tägliche Erinnerung, um deine Serie zu halten",
- "workoutSettingsFooter": "Passe dein Workout-Erlebnis an",
- "upgradeTitle": "TabataFit+ freischalten",
- "upgradeDescription": "Unbegrenzte Workouts, Offline-Downloads und mehr.",
- "learnMore": "Mehr erfahren",
- "version": "Version",
- "privacyPolicy": "Datenschutzrichtlinie",
- "termsOfService": "Nutzungsbedingungen",
- "signOut": "Abmelden",
- "statsWorkouts": "Workouts",
- "statsStreak": "Tage in Folge",
- "statsCalories": "Kalorien",
- "faq": "FAQ",
- "contactUs": "Kontakt",
- "rateApp": "App bewerten",
- "sectionPremium": "Auf Premium upgraden",
- "sectionPersonalization": "PERSONALISIERUNG",
- "personalization": "Personalisierung",
- "personalizationEnabled": "KI-gestützte Empfehlungen aktiv",
- "personalizationDisabled": "Aktivieren für personalisierte Workouts",
- "enablePersonalization": "Personalisierung aktivieren",
- "deleteData": "Meine Daten löschen"
- },
- "sync": {
- "title": "Personalisierte Workouts freischalten",
- "benefits": {
- "recommendations": "KI-gestützte Empfehlungen basierend auf Ihrem Fortschritt",
- "adaptive": "Adaptive Schwierigkeit, die mit Ihnen wächst",
- "sync": "Synchronisieren Sie Ihren Fortschritt über alle Geräte",
- "secure": "Ihre Daten sind verschlüsselt und sicher"
- },
- "privacy": "Wir speichern Ihren Trainingsverlauf, um Ihr personalisiertes Programm zu erstellen. Sie können diese Daten jederzeit in den Einstellungen löschen.",
- "primaryButton": "Mein Erlebnis personalisieren",
- "secondaryButton": "Mit generischen Programmen fortfahren"
- },
- "dataDeletion": {
- "title": "Ihre Daten löschen?",
- "description": "Dies löscht dauerhaft Ihren synchronisierten Trainingsverlauf und Ihre Personalisierungsdaten von unseren Servern.",
- "note": "Ihr lokaler Trainingsverlauf wird auf diesem Gerät gespeichert. Sie fahren mit generischen Programmen fort.",
- "deleteButton": "Meine Daten löschen",
- "cancelButton": "Meine Daten behalten"
- },
- "player": {
- "phases": {
- "prep": "BEREIT MACHEN",
- "work": "LOS",
- "rest": "PAUSE",
- "complete": "FERTIG",
- "warmup": "AUFWÄRMEN",
- "stretch": "DEHNEN",
- "trans": "ÜBERGANG"
- },
- "current": "Aktuell",
- "next": "Nächste: ",
- "round": "Runde",
- "burnBar": "Burn-Balken",
- "communityAvg": "Community-Durchschnitt: {{calories}} kcal",
- "workoutComplete": "Workout abgeschlossen!",
- "greatJob": "Super gemacht!",
- "rounds": "Runden",
- "calories": "Kalorien",
- "minutes": "Minuten",
- "mute": "Stumm",
- "unmute": "Ton an",
- "quitTitle": "Workout beenden?",
- "quitMessage": "Dein Fortschritt geht verloren.",
- "quitConfirm": "Beenden",
- "quitCancel": "Abbrechen",
- "warmupTitle": "Aufwärmen",
- "stretchTitle": "Dehnen",
- "nextBlock": "Nächster: Block {{num}}",
- "sessionComplete": "Workout abgeschlossen!",
- "greatWork": "Großartige Arbeit",
- "blocks": "Blöcke"
- },
- "complete": {
- "title": "WORKOUT ABGESCHLOSSEN",
- "caloriesLabel": "KALORIEN",
- "minutesLabel": "MINUTEN",
- "completeLabel": "ABGESCHLOSSEN",
- "burnBar": "Burn-Balken",
- "burnBarResult": "Du hast {{percentile}}% der Nutzer übertroffen!",
- "streakTitle": "{{count}} Tage in Folge!",
- "streakSubtitle": "Bleib am Ball!",
- "shareWorkout": "Teile dein Workout",
- "shareText": "Ich habe gerade {{title}} abgeschlossen! 🔥 {{calories}} Kalorien in {{duration}} Minuten.",
- "recommendedNext": "Empfohlen als Nächstes",
- "backToHome": "Zurück zur Startseite",
- "feedbackTitle": "Wie war das Workout?",
- "feedbackEasy": "Einfach",
- "feedbackPerfect": "Perfekt",
- "feedbackHard": "Schwer",
- "thisWeek": "Diese Woche",
- "share": "Teilen",
- "shareTitle": "Einheit abgeschlossen · {{minutes}} Min.",
- "streakDays_one": "{{count}} Tag am Stück",
- "streakDays_other": "{{count}} Tage am Stück",
- "streakRecord_one": "Rekord: {{count}} Tag",
- "streakRecord_other": "Rekord: {{count}} Tage"
- },
- "collection": {
- "notFound": "Sammlung nicht gefunden",
- "minTotal": "{{count}} Min insgesamt"
- },
- "category": {
- "allLevels": "Alle Stufen"
- },
- "workout": {
- "notFound": "Workout nicht gefunden",
- "whatYoullNeed": "Was du brauchst",
- "exercises": "Übungen ({{count}} Runden)",
- "repeatRounds": "Wiederholen × {{count}} Runden",
- "music": "Musik",
- "musicMix": "{{vibe}} Mix",
- "curatedForWorkout": "Zusammengestellt für dein Workout",
- "startWorkout": "WORKOUT STARTEN",
- "unlockWithPremium": "MIT TABATAFIT+ FREISCHALTEN"
- },
- "paywall": {
- "subtitle": "Schalte alle Funktionen frei und erreiche deine Ziele schneller",
- "features": {
- "music": "Premium-Musik",
- "workouts": "Unbegrenzte Workouts",
- "stats": "Erweiterte Statistiken",
- "calories": "Kalorienverfolgung",
- "reminders": "Tägliche Erinnerungen",
- "ads": "Keine Werbung"
- },
- "yearly": "Jährlich",
- "monthly": "Monatlich",
- "perYear": "pro Jahr",
- "perMonth": "pro Monat",
- "save50": "50% SPAREN",
- "equivalent": "Nur {{price}}/Monat",
- "subscribe": "Jetzt Abonnieren",
- "trialCta": "Kostenlos Testen",
- "processing": "Verarbeitung...",
- "restore": "Käufe Wiederherstellen",
- "terms": "Die Zahlung wird bei Bestätigung deiner Apple-ID belastet. Das Abonnement verlängert sich automatisch, sofern es nicht mindestens 24 Stunden vor Ablauf des Zeitraums gekündigt wird. Verwalte es in den Kontoeinstellungen.",
- "termsLink": "Nutzungsbedingungen",
- "privacyLink": "Datenschutzrichtlinie"
- },
- "onboarding": {
- "problem": {
- "title": "Du hast keine Stunde\nfürs Fitnessstudio.",
- "subtitle1": "Niemand hat das.",
- "subtitle2": "Aber du willst Ergebnisse. Wir haben die Lösung.",
- "cta": "Zeig mir wie"
- },
- "empathy": {
- "title": "Was hält dich zurück?",
- "chooseUpTo": "Wähle bis zu 2",
- "noTime": "Keine Zeit",
- "lowMotivation": "Wenig Motivation",
- "noKnowledge": "Weiß nicht, was ich tun soll",
- "noGym": "Kein Fitnessstudio"
- },
- "solution": {
- "title": "4 Minuten.\nWirklich transformativ.",
- "tabataCalories": "85 kcal",
- "cardioCalories": "90 kcal",
- "tabata": "Tabata",
- "cardio": "Cardio",
- "tabataDuration": "4 Min",
- "cardioDuration": "30 Min",
- "vs": "VS",
- "citation": "\"Die Tabata-Methode steigert aerobe und anaerobe Kapazität gleichzeitig.\"",
- "citationAuthor": "— Dr. Izumi Tabata, 1996",
- "cta": "Ich bin überzeugt"
- },
- "wow": {
- "title": "Deine App, vorab.",
- "subtitle": "Alles, was du brauchst, um dich zu verwandeln.",
- "card1Title": "Der perfekte Timer",
- "card1Subtitle": "LOS, PAUSE, WIEDERHOLEN — präzise getaktete Phasen mit visuellem Feedback.",
- "card2Title": "50+ Experten-Workouts",
- "card2Subtitle": "Von 4-Minuten-Sprints bis 20-Minuten-Burns. Anfänger bis Fortgeschrittene.",
- "card3Title": "Intelligentes Coaching",
- "card3Subtitle": "Sprachhinweise und haptisches Feedback halten dich in der Zone.",
- "card4Title": "Verfolge deinen Fortschritt",
- "card4Subtitle": "Wöchentliche Serien, Kalorientracking und persönliche Rekorde."
- },
- "personalization": {
- "title": "Lass uns deine\nerste Woche planen.",
- "yourName": "DEIN NAME",
- "namePlaceholder": "Gib deinen Namen ein",
- "fitnessLevel": "FITNESSLEVEL",
- "yourGoal": "DEIN ZIEL",
- "weeklyFrequency": "WÖCHENTLICHE HÄUFIGKEIT",
- "readyMessage": "Dein personalisiertes Programm ist bereit.",
- "goals": {
- "weightLoss": "Abnehmen",
- "cardio": "Cardio",
- "strength": "Kraft",
- "wellness": "Wohlbefinden"
- },
- "frequencies": {
- "2x": "2x / Woche",
- "3x": "3x / Woche",
- "5x": "5x / Woche"
- }
- },
- "paywall": {
- "title": "Bleib am Ball.\nOhne Grenzen.",
- "features": {
- "unlimited": "Unbegrenzte Workouts",
- "offline": "Offline-Downloads",
- "stats": "Erweiterte Statistiken & Apple Watch",
- "noAds": "Keine Werbung + Familienfreigabe"
- },
- "bestValue": "BESTES ANGEBOT",
- "yearlyPrice": "49,99 $",
- "monthlyPrice": "6,99 $",
- "savePercent": "42% sparen",
- "trialCta": "KOSTENLOS TESTEN (7 Tage)",
- "guarantees": "Jederzeit kündbar · 30-Tage-Geld-zurück-Garantie",
- "restorePurchases": "Käufe wiederherstellen",
- "skipButton": "Ohne Abo fortfahren"
- }
- },
- "programs": {
- "title": "Programme",
- "weeks": "Wochen",
- "week": "Woche",
- "workouts": "Workouts",
- "workout": "Workout",
- "minutes": "Minuten",
- "min": "min",
- "perWeek": "/Woche",
- "equipment": "Ausrüstung",
- "optional": "(optional)",
- "bodyweightOnly": "Nur Körpergewicht",
- "focusAreas": "Schwerpunkte",
- "exercises": "Übungen",
- "of": "von",
- "complete": "abgeschlossen",
- "completed": "Abgeschlossen",
- "notStarted": "Nicht gestartet",
- "inProgress": "In Bearbeitung",
- "allWorkoutsComplete": "Alle Workouts abgeschlossen!",
- "status": {
- "notStarted": "Nicht gestartet",
- "inProgress": "In Bearbeitung",
- "complete": "Abgeschlossen",
- "completed": "Abgeschlossen"
- },
- "yourProgress": "Ihr Fortschritt",
- "trainingPlan": "Trainingsplan",
- "current": "Aktuell",
- "startProgram": "Programm starten",
- "continue": "Fortsetzen",
- "continueTraining": "Training fortsetzen",
- "restart": "Neu starten",
- "restartProgram": "Programm neu starten"
- },
- "workoutProgram": {
- "tabata": "Tabata",
- "exercise1": "Übung 1",
- "exercise2": "Übung 2",
- "rounds": "{{count}} Runden",
- "startProgram": "Programm starten",
- "tabataLabel": "Tabata {{position}}",
- "beginner": "Anfänger",
- "intermediate": "Mittelstufe",
- "advanced": "Fortgeschritten"
- },
- "terms": {
- "title": "Nutzungsbedingungen",
- "lastUpdated": "Letzte Aktualisierung: März 2026",
- "acceptance": {
- "title": "Akzeptanz",
- "content": "Durch die Nutzung von TabataFit akzeptierst du diese Nutzungsbedingungen."
- },
- "service": {
- "title": "Beschreibung des Dienstes",
- "content": "TabataFit ist eine Fitness-App für Tabata-Training mit Timer, Programmen und Fortschrittsverfolgung."
- },
- "subscription": {
- "title": "Abonnement",
- "content": "Premium-Funktionen erfordern ein Abonnement. Die Abrechnung erfolgt über dein Apple-ID-Konto. Die automatische Verlängerung kann in den Kontoeinstellungen deaktiviert werden."
- },
- "liability": {
- "title": "Haftungsbeschränkung",
- "content": "TabataFit übernimmt keine Haftung für Verletzungen. Konsultiere vor Beginn eines Trainingsprogramms einen Arzt."
- },
- "ip": {
- "title": "Geistiges Eigentum",
- "content": "Alle Inhalte sind durch das Urheberrecht geschützt."
- },
- "contact": {
- "title": "Kontakt",
- "content": "Bei Fragen: support@tabatafit.app"
- }
- },
- "zone": {
- "chooseYourFocus": "Wähle deinen Fokus",
- "upperBody": {
- "label": "Oberkörper",
- "description": "Arme, Schultern, Brust und Rücken."
- },
- "lowerBody": {
- "label": "Unterkörper",
- "description": "Beine, Gesäß und Rumpf."
- },
- "fullBody": {
- "label": "Ganzkörper",
- "description": "Komplette Workouts, jede Muskelgruppe."
- },
- "chooseLevel": "Wähle dein Level",
- "emptyPrograms": "Noch keine Programme auf diesem Level."
- },
- "settings": {
- "title": "Einstellungen",
- "sectionProfile": "PROFIL",
- "name": "Name",
- "subscription": "Abonnement",
- "premium": "Premium",
- "free": "Kostenlos",
- "upgradePremium": "Auf Premium upgraden",
- "sectionPrefs": "EINSTELLUNGEN",
- "haptics": "Vibration",
- "soundEffects": "Soundeffekte",
- "voiceCoaching": "Sprach-Coach",
- "music": "Musik",
- "sectionLegal": "RECHTLICHES",
- "terms": "Nutzungsbedingungen",
- "privacy": "Datenschutz",
- "sectionData": "DATEN",
- "resetProgress": "Fortschritt zurücksetzen",
- "resetTitle": "Fortschritt zurücksetzen?",
- "resetMessage": "Diese Aktion ist unwiderruflich. Deine Serie, dein Verlauf und abgeschlossene Programme werden gelöscht.",
- "resetConfirm": "Zurücksetzen",
- "version": "TabataGo · v1.0"
- },
- "program": {
- "notFound": "Programm nicht gefunden",
- "warmup": "Aufwärmen",
- "stretch": "Dehnen",
- "exercise1": "Übung 1",
- "exercise2": "Übung 2",
- "tabataLabel": "Tabata {{num}}",
- "tabataSubtitle": "{{rounds}} Runden · {{work}}s / {{rest}}s",
- "startSession": "Einheit starten",
- "unlockPremium": "Mit Premium freischalten"
- }
-}
diff --git a/src/shared/i18n/locales/en/CLAUDE.md b/src/shared/i18n/locales/en/CLAUDE.md
deleted file mode 100644
index d7e2504..0000000
--- a/src/shared/i18n/locales/en/CLAUDE.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 20, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5391 | 10:20 PM | 🟣 | RevenueCat subscription system fully integrated and tested | ~656 |
-| #5384 | 9:28 PM | 🟣 | Implemented RevenueCat subscription system | ~190 |
-| #5366 | 7:51 PM | 🟣 | Restore Purchases Translation Added to Paywall Section | ~153 |
-| #5365 | " | 🟣 | English Localization Added for Subscription Section | ~160 |
-
\ No newline at end of file
diff --git a/src/shared/i18n/locales/en/common.json b/src/shared/i18n/locales/en/common.json
deleted file mode 100644
index 77ad88d..0000000
--- a/src/shared/i18n/locales/en/common.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "start": "START",
- "continue": "Continue",
- "next": "Next",
- "done": "Done",
- "seeAll": "See All",
- "back": "Back",
- "share": "Share",
- "cancel": "Cancel",
- "save": "Save",
- "loading": "Loading...",
- "offline": "No internet connection",
-
- "levels": {
- "beginner": "Beginner",
- "intermediate": "Intermediate",
- "advanced": "Advanced"
- },
-
- "categories": {
- "all": "All",
- "fullBody": "Full Body",
- "core": "Core",
- "upperBody": "Upper Body",
- "lowerBody": "Lower Body",
- "cardio": "Cardio"
- },
-
- "days": {
- "sun": "Sun",
- "mon": "Mon",
- "tue": "Tue",
- "wed": "Wed",
- "thu": "Thu",
- "fri": "Fri",
- "sat": "Sat"
- },
-
- "greetings": {
- "morning": "Good morning",
- "afternoon": "Good afternoon",
- "evening": "Good evening"
- },
-
- "units": {
- "min": "min",
- "cal": "cal",
- "minUnit": "{{count}} min",
- "calUnit": "{{count}} cal",
- "sWork": "{{count}}s work",
- "perYear": "per year",
- "perMonth": "per month",
- "perWeek": "/week"
- },
-
- "plurals": {
- "workout_one": "{{count}} workout",
- "workout_other": "{{count}} workouts",
- "day_one": "{{count}} day",
- "day_other": "{{count}} days",
- "round_one": "{{count}} round",
- "round_other": "{{count}} rounds",
- "week_one": "{{count}} week",
- "week_other": "{{count}} weeks"
- },
-
- "workoutMeta": "{{duration}} min \u00B7 {{level}} \u00B7 {{calories}} cal",
- "durationLevel": "{{duration}} min \u00B7 {{level}}",
- "calMin": "{{calories}} cal \u00B7 {{duration}} min",
-
- "musicVibes": {
- "electronic": "Electronic",
- "hipHop": "Hip-Hop",
- "pop": "Pop",
- "rock": "Rock",
- "chill": "Chill"
- }
-}
diff --git a/src/shared/i18n/locales/en/content.json b/src/shared/i18n/locales/en/content.json
deleted file mode 100644
index ea067aa..0000000
--- a/src/shared/i18n/locales/en/content.json
+++ /dev/null
@@ -1,322 +0,0 @@
-{
- "workouts": {
- "1": "Full Body Ignite",
- "2": "Total Body Blast",
- "3": "Power Surge",
- "4": "Morning Wake-Up",
- "5": "Endurance Builder",
- "6": "Quick Burn",
- "7": "Functional Flow",
- "8": "Athletic Power",
- "9": "Sweat Session",
- "10": "Total Tone",
- "11": "Core Crusher",
- "12": "Ab Shredder",
- "13": "Core Foundations",
- "14": "Oblique Inferno",
- "15": "Core Endurance",
- "16": "Gentle Core",
- "17": "Core Power",
- "18": "360 Core",
- "19": "Core Stability",
- "20": "Core Marathon",
- "21": "Upper Body Blitz",
- "22": "Arm Sculptor",
- "23": "Push-Up Mastery",
- "24": "Shoulder Shredder",
- "25": "Chest & Back",
- "26": "Bodyweight Upper",
- "27": "Strength Tabata",
- "28": "Tone & Define",
- "29": "Power Arms",
- "30": "Upper Endurance",
- "31": "Lower Body Burn",
- "32": "Leg Day Tabata",
- "33": "Glute Activator",
- "34": "Explosive Legs",
- "35": "Squat Challenge",
- "36": "Lower Power",
- "37": "Knee-Friendly Legs",
- "38": "Sprint Tabata",
- "39": "Legs & Glutes",
- "40": "Leg Day Marathon",
- "41": "HIIT Extreme",
- "42": "Cardio Blast",
- "43": "Dance Cardio",
- "44": "Fat Burn Express",
- "45": "Low Impact Cardio",
- "46": "Cardio Inferno",
- "47": "Sunrise Flow",
- "48": "Power Hour",
- "49": "Deep Stretch",
- "50": "Cardio Marathon"
- },
- "exercises": {
- "jumping-jacks": "Jumping Jacks",
- "squats": "Squats",
- "push-ups": "Push-ups",
- "high-knees": "High Knees",
- "burpees": "Burpees",
- "lunges": "Lunges",
- "push-up-to-row": "Push-up to Row",
- "mountain-climbers": "Mountain Climbers",
- "thrusters": "Thrusters",
- "box-jumps": "Box Jumps",
- "renegade-rows": "Renegade Rows",
- "tuck-jumps": "Tuck Jumps",
- "arm-circles": "Arm Circles",
- "bodyweight-squats": "Bodyweight Squats",
- "knee-push-ups": "Knee Push-ups",
- "marching-in-place": "Marching in Place",
- "devil-press": "Devil Press",
- "squat-jumps": "Squat Jumps",
- "push-up-burpees": "Push-up Burpees",
- "plank-jacks": "Plank Jacks",
- "star-jumps": "Star Jumps",
- "wall-sit": "Wall Sit",
- "tricep-dips": "Tricep Dips",
- "butt-kicks": "Butt Kicks",
- "inchworms": "Inchworms",
- "curtsy-lunges": "Curtsy Lunges",
- "bear-crawls": "Bear Crawls",
- "squat-pulses": "Squat Pulses",
- "clean-and-press": "Clean & Press",
- "lateral-bounds": "Lateral Bounds",
- "slam-ball": "Slam Ball",
- "sprawls": "Sprawls",
- "jumping-lunges": "Jumping Lunges",
- "push-up-to-t": "Push-up to T",
- "speed-skaters": "Speed Skaters",
- "plank-shoulder-taps": "Plank Shoulder Taps",
- "side-lunges": "Side Lunges",
- "diamond-push-ups": "Diamond Push-ups",
- "glute-bridges": "Glute Bridges",
- "toe-touches": "Toe Touches",
- "crunches": "Crunches",
- "russian-twists": "Russian Twists",
- "leg-raises": "Leg Raises",
- "plank-hold": "Plank Hold",
- "v-ups": "V-Ups",
- "bicycle-crunches": "Bicycle Crunches",
- "dragon-flags": "Dragon Flags",
- "flutter-kicks": "Flutter Kicks",
- "dead-bug": "Dead Bug",
- "bird-dog": "Bird Dog",
- "side-plank-l": "Side Plank (L)",
- "side-plank-r": "Side Plank (R)",
- "woodchoppers": "Woodchoppers",
- "side-crunches": "Side Crunches",
- "windshield-wipers": "Windshield Wipers",
- "plank-hip-dips": "Plank Hip Dips",
- "hollow-body-hold": "Hollow Body Hold",
- "turkish-get-ups": "Turkish Get-ups",
- "ab-rollouts": "Ab Rollouts",
- "pallof-press": "Pallof Press",
- "pelvic-tilts": "Pelvic Tilts",
- "modified-crunches": "Modified Crunches",
- "supine-leg-march": "Supine Leg March",
- "cat-cow": "Cat-Cow",
- "plank-to-push-up": "Plank to Push-up",
- "reverse-crunches": "Reverse Crunches",
- "bear-hold": "Bear Hold",
- "l-sits": "L-Sits",
- "dragon-flag-negatives": "Dragon Flag Negatives",
- "hanging-knee-raises": "Hanging Knee Raises",
- "plank-walkouts": "Plank Walkouts",
- "forearm-plank": "Forearm Plank",
- "slow-bicycle": "Slow Bicycle",
- "glute-bridge-march": "Glute Bridge March",
- "prone-cobra": "Prone Cobra",
- "stir-the-pot": "Stir the Pot",
- "ab-wheel-rollout": "Ab Wheel Rollout",
- "hanging-leg-raises": "Hanging Leg Raises",
- "dumbbell-rows": "Dumbbell Rows",
- "shoulder-press": "Shoulder Press",
- "bicep-curls": "Bicep Curls",
- "tricep-extensions": "Tricep Extensions",
- "lateral-raises": "Lateral Raises",
- "front-raises": "Front Raises",
- "wide-push-ups": "Wide Push-ups",
- "decline-push-ups": "Decline Push-ups",
- "explosive-push-ups": "Explosive Push-ups",
- "arnold-press": "Arnold Press",
- "upright-rows": "Upright Rows",
- "face-pulls": "Face Pulls",
- "handstand-hold": "Handstand Hold",
- "chest-press": "Chest Press",
- "bent-over-rows": "Bent Over Rows",
- "chest-flyes": "Chest Flyes",
- "pull-ups": "Pull-ups",
- "tricep-dips-chair": "Tricep Dips (Chair)",
- "clean-and-jerk": "Clean & Jerk",
- "turkish-get-up": "Turkish Get-up",
- "overhead-press": "Overhead Press",
- "hammer-curls": "Hammer Curls",
- "overhead-tricep": "Overhead Tricep",
- "reverse-flyes": "Reverse Flyes",
- "concentration-curls": "Concentration Curls",
- "skull-crushers": "Skull Crushers",
- "zottman-curls": "Zottman Curls",
- "dips": "Dips",
- "barbell-press": "Barbell Press",
- "dumbbell-snatch": "Dumbbell Snatch",
- "plyo-push-ups": "Plyo Push-ups",
- "calf-raises": "Calf Raises",
- "jump-squats": "Jump Squats",
- "walking-lunges": "Walking Lunges",
- "step-ups": "Step-ups",
- "donkey-kicks": "Donkey Kicks",
- "fire-hydrants": "Fire Hydrants",
- "clamshells": "Clamshells",
- "pistol-squats": "Pistol Squats",
- "broad-jumps": "Broad Jumps",
- "goblet-squats": "Goblet Squats",
- "sumo-squats": "Sumo Squats",
- "pulse-squats": "Pulse Squats",
- "front-squats": "Front Squats",
- "romanian-deadlifts": "Romanian Deadlifts",
- "split-squats": "Split Squats",
- "power-cleans": "Power Cleans",
- "standing-kickbacks": "Standing Kickbacks",
- "side-leg-raises": "Side Leg Raises",
- "high-knees-sprint": "High Knees Sprint",
- "lateral-shuffles": "Lateral Shuffles",
- "butt-kick-sprint": "Butt Kick Sprint",
- "banded-squats": "Banded Squats",
- "hip-thrusts": "Hip Thrusts",
- "lateral-band-walks": "Lateral Band Walks",
- "step-back-lunges": "Step-back Lunges",
- "back-squats": "Back Squats",
- "leg-press-jumps": "Leg Press Jumps",
- "box-jump-overs": "Box Jump Overs",
- "grapevine": "Grapevine",
- "step-touch": "Step Touch",
- "cha-cha-slide": "Cha-Cha Slide",
- "jump-rope": "Jump Rope",
- "step-touches": "Step Touches",
- "boxing-punches": "Boxing Punches",
- "side-steps": "Side Steps",
- "burpee-tuck-jumps": "Burpee Tuck Jumps",
- "sprint-in-place": "Sprint in Place",
- "plyo-lunges": "Plyo Lunges",
- "sun-salutation": "Sun Salutation",
- "light-jog": "Light Jog",
- "arm-swings": "Arm Swings",
- "gentle-twists": "Gentle Twists",
- "double-unders": "Double Unders",
- "box-jump-burpees": "Box Jump Burpees",
- "sprint-intervals": "Sprint Intervals",
- "forward-fold": "Forward Fold",
- "pigeon-pose": "Pigeon Pose",
- "spinal-twist": "Spinal Twist",
- "butterfly-stretch": "Butterfly Stretch"
- },
- "equipment": {
- "no-equipment-required": "No equipment required",
- "yoga-mat-optional": "Yoga mat optional",
- "yoga-mat": "Yoga mat",
- "dumbbells-optional": "Dumbbells optional",
- "dumbbells": "Dumbbells",
- "kettlebell-recommended": "Kettlebell recommended",
- "kettlebell": "Kettlebell",
- "resistance-band-optional": "Resistance band optional",
- "resistance-band": "Resistance band",
- "medicine-ball": "Medicine ball",
- "light-dumbbells": "Light dumbbells",
- "pull-up-bar": "Pull-up bar",
- "full-gym-setup": "Full gym setup",
- "box-or-step": "Box or step",
- "chair-for-balance": "Chair for balance",
- "jump-rope-optional": "Jump rope optional",
- "jump-rope": "Jump rope",
- "stability-ball": "Stability ball",
- "barbell": "Barbell",
- "dumbbell-optional": "Dumbbell optional"
- },
- "collections": {
- "morning-energizer": {
- "title": "Morning Energizer",
- "description": "Start your day right"
- },
- "no-equipment": {
- "title": "No Equipment",
- "description": "Workout anywhere"
- },
- "7-day-burn": {
- "title": "7-Day Burn Challenge",
- "description": "Transform in one week"
- },
- "quick-intense": {
- "title": "Quick & Intense",
- "description": "Max effort in 4 minutes"
- },
- "core-focus": {
- "title": "Core Focus",
- "description": "Build a solid foundation"
- },
- "leg-day": {
- "title": "Leg Day",
- "description": "Never skip leg day"
- }
- },
- "programs": {
- "beginner-journey": {
- "title": "Beginner Journey",
- "description": "Your first steps into Tabata fitness"
- },
- "strength-builder": {
- "title": "Strength Builder",
- "description": "Progressive strength development"
- },
- "fat-burn-protocol": {
- "title": "Fat Burn Protocol",
- "description": "Maximum calorie burn program"
- },
- "upper-body": {
- "title": "Upper Body",
- "description": "Build strong shoulders, chest, back, and arms with physio-designed progressions"
- },
- "lower-body": {
- "title": "Lower Body",
- "description": "Develop powerful legs, glutes, and hips with joint-friendly movements"
- },
- "full-body": {
- "title": "Full Body",
- "description": "Complete total body transformation using only your bodyweight"
- }
- },
- "achievements": {
- "first-burn": {
- "title": "First Burn",
- "description": "Complete your first workout"
- },
- "week-warrior": {
- "title": "Week Warrior",
- "description": "7 day streak"
- },
- "century-club": {
- "title": "Century Club",
- "description": "Burn 100 calories total"
- },
- "iron-will": {
- "title": "Iron Will",
- "description": "Complete 10 workouts"
- },
- "tabata-master": {
- "title": "Tabata Master",
- "description": "Complete 50 workouts"
- },
- "marathon-burner": {
- "title": "Marathon Burner",
- "description": "Exercise for 100 minutes total"
- },
- "unstoppable": {
- "title": "Unstoppable",
- "description": "30 day streak"
- },
- "calorie-crusher": {
- "title": "Calorie Crusher",
- "description": "Burn 1000 calories total"
- }
- }
-}
diff --git a/src/shared/i18n/locales/en/notifications.json b/src/shared/i18n/locales/en/notifications.json
deleted file mode 100644
index 2f18bf2..0000000
--- a/src/shared/i18n/locales/en/notifications.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "dailyReminder": {
- "title": "Time for your Tabata!",
- "body": "4 minutes to transform your day."
- }
-}
diff --git a/src/shared/i18n/locales/en/screens.json b/src/shared/i18n/locales/en/screens.json
deleted file mode 100644
index 258a4d2..0000000
--- a/src/shared/i18n/locales/en/screens.json
+++ /dev/null
@@ -1,401 +0,0 @@
-{
- "tabs": {
- "home": "Home",
- "explore": "Explore",
- "programs": "Programs",
- "activity": "Activity",
- "progression": "Progress",
- "profile": "Profile"
- },
- "home": {
- "readyToCrush": "Ready to crush it today?",
- "featured": "FEATURED",
- "recent": "Recent",
- "popularThisWeek": "Popular This Week",
- "collections": "Collections",
- "chooseYourPath": "Choose Your Path",
- "continueYourJourney": "Continue Your Journey",
- "yourPrograms": "Your Programs",
- "programsSubtitle": "Designed by physiotherapists",
- "switchProgram": "Switch Program",
- "statsStreak": "Streak",
- "statsThisWeek": "This Week",
- "statsMinutes": "Minutes",
- "upperBody": "Upper Body",
- "lowerBody": "Lower Body",
- "fullBody": "Full Body",
- "programsByZone": "Programs by Body Zone",
- "filterAll": "All",
- "tabataCount": "{{count}} tabatas",
- "freeBadge": "FREE",
- "premiumBadge": "PREMIUM",
- "startProgram": "Start",
- "continueProgram": "Continue",
- "unlockPremium": "Unlock Premium",
- "tabataPrograms": "Physio Programs",
- "tabataProgramsSubtitle": "Rehabilitation and physiotherapy programs",
- "recommendedNext": "Continue your session",
- "mascotStreak": "{{count}}-day streak{{name}}! 🔥",
- "mascotReady": "Ready to train{{name}}?",
- "statsCompleted": "Programs",
- "zoneSubtitle": "3 levels · Beginner → Advanced",
- "zoneDescUpper": "Shoulders, chest, arms & core strength",
- "zoneDescLower": "Legs, glutes & hip mobility",
- "zoneDescFull": "Total body conditioning & cardio",
- "zoneLevels": "3 levels",
- "zonePrograms": "{{count}} programs"
- },
- "explore": {
- "title": "Explore",
- "collections": "Collections",
- "featured": "Featured",
- "allWorkouts": "All Workouts",
- "trainers": "Trainers",
- "noResults": "No workouts found",
- "tryAdjustingFilters": "Try adjusting your filters or search",
- "loading": "Loading...",
- "filterCategory": "Category",
- "filterLevel": "Level",
- "filterEquipment": "Equipment",
- "filterDuration": "Duration",
- "clearFilters": "Clear",
- "workoutsCount": "{{count}} workouts",
- "workouts": "Workouts",
- "equipmentOptions": {
- "none": "No Equipment",
- "band": "Resistance Band",
- "dumbbells": "Dumbbells",
- "mat": "Mat"
- },
- "allEquipment": "All Equipment",
- "searchPlaceholder": "Search workouts, trainers...",
- "recommendedForYou": "Recommended for You",
- "tryNewCategory": "Try something new",
- "startFirstWorkout": "Start your first workout to get personalized recommendations",
- "filters": "Filters",
- "activeFilters": "{{count}} active",
- "applyFilters": "Apply Filters",
- "resetFilters": "Reset",
- "errorTitle": "Couldn't load workouts",
- "errorRetry": "Tap to retry",
- "featuredCollection": "Featured Collection"
- },
- "activity": {
- "title": "Activity",
- "dayStreak": "day streak",
- "longest": "LONGEST",
- "workouts": "Workouts",
- "minutes": "Minutes",
- "calories": "Calories",
- "bestStreak": "Best Streak",
- "thisWeek": "This Week",
- "ofDays": "{{completed}} of 7 days",
- "recent": "Recent",
- "today": "Today",
- "yesterday": "Yesterday",
- "daysAgo": "{{count}}d ago",
- "achievements": "Achievements",
- "emptyTitle": "No Activity Yet",
- "emptySubtitle": "Complete your first workout and your stats will appear here.",
- "startFirstWorkout": "Start Your First Workout"
- },
- "profile": {
- "title": "Profile",
- "guest": "Guest",
- "memberSince": "Member since {{date}}",
- "sectionAccount": "ACCOUNT",
- "sectionWorkout": "WORKOUT",
- "sectionNotifications": "NOTIFICATIONS",
- "sectionAbout": "ABOUT",
- "sectionSubscription": "SUBSCRIPTION",
- "email": "Email",
- "plan": "Plan",
- "freePlan": "Free",
- "restorePurchases": "Restore Purchases",
- "hapticFeedback": "Haptic Feedback",
- "soundEffects": "Sound Effects",
- "voiceCoaching": "Voice Coaching",
- "dailyReminders": "Daily Reminders",
- "reminderTime": "Reminder Time",
- "reminderFooter": "Get a daily reminder to keep your streak going",
- "workoutSettingsFooter": "Customize your workout experience",
- "upgradeTitle": "Unlock TabataFit+",
- "upgradeDescription": "Get unlimited workouts, offline downloads, and more.",
- "learnMore": "Learn More",
- "version": "Version",
- "privacyPolicy": "Privacy Policy",
- "termsOfService": "Terms of Service",
- "signOut": "Sign Out",
- "statsWorkouts": "workouts",
- "statsStreak": "day streak",
- "statsCalories": "calories",
- "faq": "FAQ",
- "contactUs": "Contact Us",
- "rateApp": "Rate App",
- "sectionPremium": "Upgrade to Premium",
- "sectionPersonalization": "PERSONALIZATION",
- "personalization": "Personalization",
- "personalizationEnabled": "AI-powered recommendations active",
- "personalizationDisabled": "Enable for personalized workouts",
- "enablePersonalization": "Enable Personalization",
- "deleteData": "Delete My Data"
- },
- "sync": {
- "title": "Unlock Personalized Workouts",
- "benefits": {
- "recommendations": "AI-powered recommendations based on your progress",
- "adaptive": "Adaptive difficulty that grows with you",
- "sync": "Sync your progress across devices",
- "secure": "Your data is encrypted and secure"
- },
- "privacy": "We'll save your workout history to create your personalized program. You can delete this data anytime in Settings.",
- "primaryButton": "Personalize My Experience",
- "secondaryButton": "Continue with Generic Programs"
- },
- "dataDeletion": {
- "title": "Delete Your Data?",
- "description": "This will permanently delete your synced workout history and personalization data from our servers.",
- "note": "Your local workout history will be kept on this device. You'll continue with generic programs.",
- "deleteButton": "Delete My Data",
- "cancelButton": "Keep My Data"
- },
- "player": {
- "phases": {
- "prep": "GET READY",
- "work": "WORK",
- "rest": "REST",
- "complete": "COMPLETE",
- "warmup": "WARMUP",
- "stretch": "STRETCH",
- "trans": "TRANSITION"
- },
- "current": "Current",
- "next": "Next: ",
- "round": "Round",
- "burnBar": "Burn Bar",
- "communityAvg": "Community avg: {{calories}} cal",
- "workoutComplete": "Workout Complete!",
- "greatJob": "Great job!",
- "rounds": "Rounds",
- "calories": "Calories",
- "minutes": "Minutes",
- "mute": "Mute",
- "unmute": "Unmute",
- "quitTitle": "Quit Workout?",
- "quitMessage": "Your progress won't be saved. Are you sure?",
- "quitConfirm": "Quit",
- "quitCancel": "Keep Going",
- "warmupTitle": "Warm-up",
- "stretchTitle": "Stretch",
- "nextBlock": "Next: Block {{num}}",
- "sessionComplete": "Workout complete!",
- "greatWork": "Great work",
- "blocks": "Blocks"
- },
- "complete": {
- "title": "WORKOUT COMPLETE",
- "caloriesLabel": "CALORIES",
- "minutesLabel": "MINUTES",
- "completeLabel": "COMPLETE",
- "burnBar": "Burn Bar",
- "burnBarResult": "You beat {{percentile}}% of users!",
- "streakTitle": "{{count}} Day Streak!",
- "streakSubtitle": "Keep the momentum going!",
- "shareWorkout": "Share Your Workout",
- "shareText": "I just completed {{title}}! {{calories}} calories in {{duration}} minutes.",
- "recommendedNext": "Recommended Next",
- "backToHome": "Back to Home",
- "feedbackTitle": "How was it?",
- "feedbackHard": "Hard",
- "feedbackPerfect": "Perfect",
- "feedbackEasy": "Too Easy",
- "thisWeek": "This Week",
- "share": "Share",
- "shareTitle": "Session complete · {{minutes}} min",
- "streakDays_one": "{{count}} day in a row",
- "streakDays_other": "{{count}} days in a row",
- "streakRecord_one": "Record: {{count}} day",
- "streakRecord_other": "Record: {{count}} days"
- },
- "terms": {
- "title": "Terms of Service",
- "lastUpdated": "Last Updated: April 2026",
- "acceptance": {
- "title": "Acceptance of Terms",
- "content": "By downloading or using TabataFit, you agree to be bound by these Terms of Service."
- },
- "service": {
- "title": "Description of Service",
- "content": "TabataFit provides guided Tabata workout programs. The app is not a substitute for professional medical advice."
- },
- "subscriptions": {
- "title": "Subscriptions & Payments",
- "content": "Some features require a paid subscription managed through the App Store or Google Play. Prices may vary by region. Free trials convert to paid subscriptions unless cancelled before the trial ends."
- },
- "cancellation": {
- "title": "Cancellation",
- "content": "You can cancel your subscription at any time through your device settings. Cancellation takes effect at the end of the current billing period."
- },
- "liability": {
- "title": "Limitation of Liability",
- "content": "TabataFit is provided as-is. We are not liable for any injuries resulting from workouts. Always consult a healthcare professional before starting any exercise program."
- },
- "contact": {
- "title": "Contact",
- "content": "For questions about these terms, contact us at:"
- }
- },
- "paywall": {
- "title": "Stay on track.\nNo limits.",
- "features": {
- "unlimited": "Unlimited Workouts",
- "offline": "Offline Downloads",
- "stats": "Advanced Stats & Apple Watch",
- "noAds": "No Ads + Family Sharing"
- },
- "bestValue": "BEST VALUE",
- "yearlyPrice": "$49.99",
- "monthlyPrice": "$6.99",
- "savePercent": "Save 42%",
- "trialCta": "START FREE TRIAL (7 days)",
- "guarantees": "Cancel anytime · 30-day money-back guarantee",
- "restorePurchases": "Restore Purchases",
- "skipButton": "Continue without subscription",
- "termsLink": "Terms of Service",
- "privacyLink": "Privacy Policy"
- },
- "privacy": {
- "title": "Privacy Policy",
- "lastUpdated": "Last Updated: March 2026",
- "intro": {
- "title": "Introduction",
- "content": "TabataFit is committed to protecting your privacy. This policy explains how we collect, use, and safeguard your information when you use our fitness app."
- },
- "dataCollection": {
- "title": "Data We Collect",
- "content": "We collect only the information necessary to provide you with the best workout experience:",
- "items": {
- "workouts": "Workout history and preferences",
- "settings": "App settings and configurations",
- "device": "Device type and OS version for optimization"
- }
- },
- "usage": {
- "title": "How We Use Your Data",
- "content": "Your data is used solely to: personalize your workout experience, track your progress and achievements, sync your data across devices, and improve our app functionality."
- },
- "sharing": {
- "title": "Data Sharing",
- "content": "We do not sell or share your personal information with third parties. Your workout data remains private and secure on your device and in encrypted cloud storage."
- },
- "security": {
- "title": "Security",
- "content": "We implement industry-standard security measures to protect your data, including encryption and secure authentication."
- },
- "rights": {
- "title": "Your Rights",
- "content": "You have the right to access, modify, or delete your personal data at any time. You can export or delete your data from the app settings."
- },
- "contact": {
- "title": "Contact Us",
- "content": "If you have questions about this privacy policy, please contact us at:"
- }
- },
- "programs": {
- "title": "Programs",
- "weeks": "Weeks",
- "week": "Week",
- "workouts": "Workouts",
- "workout": "Workout",
- "minutes": "Minutes",
- "min": "min",
- "perWeek": "/week",
- "equipment": "Equipment",
- "optional": "(optional)",
- "bodyweightOnly": "Bodyweight only",
- "focusAreas": "Focus Areas",
- "exercises": "exercises",
- "of": "of",
- "complete": "complete",
- "completed": "Completed",
- "notStarted": "Not Started",
- "inProgress": "In Progress",
- "allWorkoutsComplete": "All Workouts Complete!",
- "status": {
- "notStarted": "Not Started",
- "inProgress": "In Progress",
- "complete": "Complete",
- "completed": "Completed"
- },
- "yourProgress": "Your Progress",
- "trainingPlan": "Training Plan",
- "current": "Current",
- "startProgram": "Start Program",
- "continue": "Continue",
- "continueTraining": "Continue Training",
- "restart": "Restart",
- "restartProgram": "Restart Program"
- },
- "workoutProgram": {
- "tabata": "Tabata",
- "exercise1": "Exercise 1",
- "exercise2": "Exercise 2",
- "rounds": "{{count}} rounds",
- "startProgram": "Start Program",
- "tabataLabel": "Tabata {{position}}",
- "beginner": "Beginner",
- "intermediate": "Intermediate",
- "advanced": "Advanced"
- },
- "zone": {
- "chooseYourFocus": "Choose your focus",
- "upperBody": {
- "label": "Upper Body",
- "description": "Arms, shoulders, chest, and back."
- },
- "lowerBody": {
- "label": "Lower Body",
- "description": "Legs, glutes, and core foundation."
- },
- "fullBody": {
- "label": "Full Body",
- "description": "Complete workouts, every muscle group."
- },
- "chooseLevel": "Choose your level",
- "emptyPrograms": "No programs at this level yet."
- },
- "settings": {
- "title": "Settings",
- "sectionProfile": "PROFILE",
- "name": "Name",
- "subscription": "Subscription",
- "premium": "Premium",
- "free": "Free",
- "upgradePremium": "Upgrade to Premium",
- "sectionPrefs": "PREFERENCES",
- "haptics": "Haptics",
- "soundEffects": "Sound Effects",
- "voiceCoaching": "Voice Coaching",
- "music": "Music",
- "sectionLegal": "LEGAL",
- "terms": "Terms of Service",
- "privacy": "Privacy Policy",
- "sectionData": "DATA",
- "resetProgress": "Reset Progress",
- "resetTitle": "Reset Progress?",
- "resetMessage": "This action is irreversible. Your streak, history, and completed programs will be erased.",
- "resetConfirm": "Reset",
- "version": "TabataGo · v1.0"
- },
- "program": {
- "notFound": "Program not found",
- "warmup": "Warm-up",
- "stretch": "Stretch",
- "exercise1": "Exercise 1",
- "exercise2": "Exercise 2",
- "tabataLabel": "Tabata {{num}}",
- "tabataSubtitle": "{{rounds}} rounds · {{work}}s / {{rest}}s",
- "startSession": "Start Session",
- "unlockPremium": "Unlock with Premium"
- }
-}
diff --git a/src/shared/i18n/locales/es/CLAUDE.md b/src/shared/i18n/locales/es/CLAUDE.md
deleted file mode 100644
index 99f65a6..0000000
--- a/src/shared/i18n/locales/es/CLAUDE.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-# Recent Activity
-
-
-
-### Feb 20, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5391 | 10:20 PM | 🟣 | RevenueCat subscription system fully integrated and tested | ~656 |
-| #5384 | 9:28 PM | 🟣 | Implemented RevenueCat subscription system | ~190 |
-| #5372 | 7:54 PM | 🟣 | Spanish Localization Added for Paywall Restore Purchases | ~138 |
-| #5371 | " | 🟣 | Spanish Localization Added for Profile Subscription Section | ~163 |
-
\ No newline at end of file
diff --git a/src/shared/i18n/locales/es/common.json b/src/shared/i18n/locales/es/common.json
deleted file mode 100644
index 71f0668..0000000
--- a/src/shared/i18n/locales/es/common.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "start": "EMPEZAR",
- "continue": "Continuar",
- "next": "Siguiente",
- "done": "Listo",
- "seeAll": "Ver todo",
- "back": "Atr\u00e1s",
- "share": "Compartir",
- "cancel": "Cancelar",
- "save": "Guardar",
- "loading": "Cargando...",
- "offline": "Sin conexi\u00f3n a internet",
-
- "levels": {
- "beginner": "Principiante",
- "intermediate": "Intermedio",
- "advanced": "Avanzado"
- },
-
- "categories": {
- "all": "Todos",
- "fullBody": "Cuerpo completo",
- "core": "Core",
- "upperBody": "Tren superior",
- "lowerBody": "Tren inferior",
- "cardio": "Cardio"
- },
-
- "days": {
- "sun": "Dom",
- "mon": "Lun",
- "tue": "Mar",
- "wed": "Mi\u00e9",
- "thu": "Jue",
- "fri": "Vie",
- "sat": "S\u00e1b"
- },
-
- "greetings": {
- "morning": "Buenos d\u00edas",
- "afternoon": "Buenas tardes",
- "evening": "Buenas noches"
- },
-
- "units": {
- "min": "min",
- "cal": "cal",
- "minUnit": "{{count}} min",
- "calUnit": "{{count}} cal",
- "sWork": "{{count}}s trabajo",
- "perYear": "por a\u00f1o",
- "perMonth": "por mes",
- "perWeek": "/semana"
- },
-
- "plurals": {
- "workout_one": "{{count}} entrenamiento",
- "workout_other": "{{count}} entrenamientos",
- "day_one": "{{count}} d\u00eda",
- "day_other": "{{count}} d\u00edas",
- "round_one": "{{count}} ronda",
- "round_other": "{{count}} rondas",
- "week_one": "{{count}} semana",
- "week_other": "{{count}} semanas"
- },
-
- "workoutMeta": "{{duration}} min \u00B7 {{level}} \u00B7 {{calories}} cal",
- "durationLevel": "{{duration}} min \u00B7 {{level}}",
- "calMin": "{{calories}} cal \u00B7 {{duration}} min",
-
- "musicVibes": {
- "electronic": "Electr\u00f3nica",
- "hipHop": "Hip-Hop",
- "pop": "Pop",
- "rock": "Rock",
- "chill": "Chill"
- }
-}
diff --git a/src/shared/i18n/locales/es/content.json b/src/shared/i18n/locales/es/content.json
deleted file mode 100644
index 2f11980..0000000
--- a/src/shared/i18n/locales/es/content.json
+++ /dev/null
@@ -1,322 +0,0 @@
-{
- "workouts": {
- "1": "Ignici\u00f3n total",
- "2": "Explosi\u00f3n corporal",
- "3": "Oleada de potencia",
- "4": "Despertar matutino",
- "5": "Constructor de resistencia",
- "6": "Quema r\u00e1pida",
- "7": "Flujo funcional",
- "8": "Potencia atl\u00e9tica",
- "9": "Sesi\u00f3n de sudor",
- "10": "Tonificaci\u00f3n total",
- "11": "Destructor de core",
- "12": "Abdominales extremos",
- "13": "Fundamentos de core",
- "14": "Infierno oblicuo",
- "15": "Resistencia de core",
- "16": "Core suave",
- "17": "Potencia de core",
- "18": "Core 360",
- "19": "Estabilidad de core",
- "20": "Marat\u00f3n de core",
- "21": "Ataque tren superior",
- "22": "Escultor de brazos",
- "23": "Dominio de flexiones",
- "24": "Hombros al l\u00edmite",
- "25": "Pecho y espalda",
- "26": "Tren superior sin equipo",
- "27": "Tabata de fuerza",
- "28": "Tonifica y define",
- "29": "Brazos de potencia",
- "30": "Resistencia tren superior",
- "31": "Quema de piernas",
- "32": "D\u00eda de piernas Tabata",
- "33": "Activador de gl\u00fateos",
- "34": "Piernas explosivas",
- "35": "Desaf\u00edo de sentadillas",
- "36": "Potencia inferior",
- "37": "Piernas sin impacto",
- "38": "Tabata de sprint",
- "39": "Piernas y gl\u00fateos",
- "40": "Marat\u00f3n de piernas",
- "41": "HIIT Extremo",
- "42": "Explosi\u00f3n cardio",
- "43": "Cardio bailable",
- "44": "Quema grasa expr\u00e9s",
- "45": "Cardio bajo impacto",
- "46": "Infierno cardio",
- "47": "Flujo al amanecer",
- "48": "Hora de potencia",
- "49": "Estiramiento profundo",
- "50": "Marat\u00f3n cardio"
- },
- "exercises": {
- "jumping-jacks": "Jumping Jacks",
- "squats": "Sentadillas",
- "push-ups": "Flexiones",
- "high-knees": "Rodillas arriba",
- "burpees": "Burpees",
- "lunges": "Zancadas",
- "push-up-to-row": "Flexi\u00f3n con remo",
- "mountain-climbers": "Escaladores",
- "thrusters": "Thrusters",
- "box-jumps": "Saltos al caj\u00f3n",
- "renegade-rows": "Remo renegado",
- "tuck-jumps": "Saltos con rodillas al pecho",
- "arm-circles": "C\u00edrculos de brazos",
- "bodyweight-squats": "Sentadillas con peso corporal",
- "knee-push-ups": "Flexiones de rodillas",
- "marching-in-place": "Marcha en el sitio",
- "devil-press": "Devil Press",
- "squat-jumps": "Sentadillas con salto",
- "push-up-burpees": "Burpees con flexi\u00f3n",
- "plank-jacks": "Plank Jacks",
- "star-jumps": "Saltos de estrella",
- "wall-sit": "Sentadilla en pared",
- "tricep-dips": "Fondos de tr\u00edceps",
- "butt-kicks": "Talones a gl\u00fateos",
- "inchworms": "Inchworms",
- "curtsy-lunges": "Zancadas cruzadas",
- "bear-crawls": "Caminata de oso",
- "squat-pulses": "Sentadillas con pulsos",
- "clean-and-press": "Cargada y press",
- "lateral-bounds": "Saltos laterales",
- "slam-ball": "Slam Ball",
- "sprawls": "Sprawls",
- "jumping-lunges": "Zancadas con salto",
- "push-up-to-t": "Flexi\u00f3n en T",
- "speed-skaters": "Patinadores",
- "plank-shoulder-taps": "Plancha con toque de hombros",
- "side-lunges": "Zancadas laterales",
- "diamond-push-ups": "Flexiones diamante",
- "glute-bridges": "Puente de gl\u00fateos",
- "toe-touches": "Toques de puntas",
- "crunches": "Abdominales",
- "russian-twists": "Giros rusos",
- "leg-raises": "Elevaciones de piernas",
- "plank-hold": "Plancha est\u00e1tica",
- "v-ups": "V-Ups",
- "bicycle-crunches": "Abdominales bicicleta",
- "dragon-flags": "Dragon Flags",
- "flutter-kicks": "Patadas de aleteo",
- "dead-bug": "Dead Bug",
- "bird-dog": "Bird Dog",
- "side-plank-l": "Plancha lateral (I)",
- "side-plank-r": "Plancha lateral (D)",
- "woodchoppers": "Le\u00f1adores",
- "side-crunches": "Abdominales laterales",
- "windshield-wipers": "Limpiaparabrisas",
- "plank-hip-dips": "Plancha con giro de cadera",
- "hollow-body-hold": "Hollow Body Hold",
- "turkish-get-ups": "Levantamiento turco",
- "ab-rollouts": "Ab Rollouts",
- "pallof-press": "Pallof Press",
- "pelvic-tilts": "Inclinaci\u00f3n p\u00e9lvica",
- "modified-crunches": "Abdominales modificados",
- "supine-leg-march": "Marcha supina de piernas",
- "cat-cow": "Gato-vaca",
- "plank-to-push-up": "Plancha a flexi\u00f3n",
- "reverse-crunches": "Abdominales inversos",
- "bear-hold": "Posici\u00f3n de oso",
- "l-sits": "L-Sits",
- "dragon-flag-negatives": "Dragon Flag negativos",
- "hanging-knee-raises": "Elevaci\u00f3n de rodillas colgado",
- "plank-walkouts": "Plancha caminada",
- "forearm-plank": "Plancha de antebrazos",
- "slow-bicycle": "Bicicleta lenta",
- "glute-bridge-march": "Puente de gl\u00fateos con marcha",
- "prone-cobra": "Cobra boca abajo",
- "stir-the-pot": "Stir the Pot",
- "ab-wheel-rollout": "Rueda abdominal",
- "hanging-leg-raises": "Elevaci\u00f3n de piernas colgado",
- "dumbbell-rows": "Remo con mancuerna",
- "shoulder-press": "Press de hombros",
- "bicep-curls": "Curl de b\u00edceps",
- "tricep-extensions": "Extensiones de tr\u00edceps",
- "lateral-raises": "Elevaciones laterales",
- "front-raises": "Elevaciones frontales",
- "wide-push-ups": "Flexiones abiertas",
- "decline-push-ups": "Flexiones declinadas",
- "explosive-push-ups": "Flexiones explosivas",
- "arnold-press": "Press Arnold",
- "upright-rows": "Remo al ment\u00f3n",
- "face-pulls": "Face Pulls",
- "handstand-hold": "Pino est\u00e1tico",
- "chest-press": "Press de pecho",
- "bent-over-rows": "Remo inclinado",
- "chest-flyes": "Aperturas de pecho",
- "pull-ups": "Dominadas",
- "tricep-dips-chair": "Fondos de tr\u00edceps (silla)",
- "clean-and-jerk": "Cargada y env\u00f3n",
- "turkish-get-up": "Levantamiento turco",
- "overhead-press": "Press por encima de la cabeza",
- "hammer-curls": "Curl martillo",
- "overhead-tricep": "Tr\u00edceps por encima",
- "reverse-flyes": "Aperturas inversas",
- "concentration-curls": "Curl concentrado",
- "skull-crushers": "Skull Crushers",
- "zottman-curls": "Curl Zottman",
- "dips": "Fondos",
- "barbell-press": "Press con barra",
- "dumbbell-snatch": "Arrancada con mancuerna",
- "plyo-push-ups": "Flexiones pliom\u00e9tricas",
- "calf-raises": "Elevaciones de gemelos",
- "jump-squats": "Sentadillas con salto",
- "walking-lunges": "Zancadas caminando",
- "step-ups": "Step-ups",
- "donkey-kicks": "Patadas de burro",
- "fire-hydrants": "Hidrantes",
- "clamshells": "Clamshells",
- "pistol-squats": "Sentadilla a una pierna",
- "broad-jumps": "Saltos de longitud",
- "goblet-squats": "Sentadilla goblet",
- "sumo-squats": "Sentadilla sumo",
- "pulse-squats": "Sentadillas con pulsos",
- "front-squats": "Sentadilla frontal",
- "romanian-deadlifts": "Peso muerto rumano",
- "split-squats": "Sentadillas b\u00falgaras",
- "power-cleans": "Cargadas de potencia",
- "standing-kickbacks": "Patadas traseras de pie",
- "side-leg-raises": "Elevaci\u00f3n lateral de pierna",
- "high-knees-sprint": "Sprint con rodillas arriba",
- "lateral-shuffles": "Desplazamientos laterales",
- "butt-kick-sprint": "Sprint con talones a gl\u00fateos",
- "banded-squats": "Sentadillas con banda",
- "hip-thrusts": "Empuje de cadera",
- "lateral-band-walks": "Caminata lateral con banda",
- "step-back-lunges": "Zancadas hacia atr\u00e1s",
- "back-squats": "Sentadilla trasera",
- "leg-press-jumps": "Saltos de prensa de pierna",
- "box-jump-overs": "Saltos por encima del caj\u00f3n",
- "grapevine": "Grapevine",
- "step-touch": "Step Touch",
- "cha-cha-slide": "Cha-Cha Slide",
- "jump-rope": "Saltar la cuerda",
- "step-touches": "Step Touches",
- "boxing-punches": "Golpes de boxeo",
- "side-steps": "Pasos laterales",
- "burpee-tuck-jumps": "Burpees con salto agrupado",
- "sprint-in-place": "Sprint en el sitio",
- "plyo-lunges": "Zancadas pliom\u00e9tricas",
- "sun-salutation": "Saludo al sol",
- "light-jog": "Trote suave",
- "arm-swings": "Balanceo de brazos",
- "gentle-twists": "Giros suaves",
- "double-unders": "Dobles de cuerda",
- "box-jump-burpees": "Burpees con salto al caj\u00f3n",
- "sprint-intervals": "Intervalos de sprint",
- "forward-fold": "Pliegue hacia adelante",
- "pigeon-pose": "Postura de paloma",
- "spinal-twist": "Torsi\u00f3n espinal",
- "butterfly-stretch": "Estiramiento mariposa"
- },
- "equipment": {
- "no-equipment-required": "Sin equipamiento necesario",
- "yoga-mat-optional": "Esterilla de yoga opcional",
- "yoga-mat": "Esterilla de yoga",
- "dumbbells-optional": "Mancuernas opcionales",
- "dumbbells": "Mancuernas",
- "kettlebell-recommended": "Kettlebell recomendada",
- "kettlebell": "Kettlebell",
- "resistance-band-optional": "Banda el\u00e1stica opcional",
- "resistance-band": "Banda el\u00e1stica",
- "medicine-ball": "Bal\u00f3n medicinal",
- "light-dumbbells": "Mancuernas ligeras",
- "pull-up-bar": "Barra de dominadas",
- "full-gym-setup": "Equipamiento completo de gimnasio",
- "box-or-step": "Caj\u00f3n o step",
- "chair-for-balance": "Silla para equilibrio",
- "jump-rope-optional": "Cuerda de saltar opcional",
- "jump-rope": "Cuerda de saltar",
- "stability-ball": "Bal\u00f3n de estabilidad",
- "barbell": "Barra",
- "dumbbell-optional": "Mancuerna opcional"
- },
- "collections": {
- "morning-energizer": {
- "title": "Energ\u00eda matutina",
- "description": "Empieza bien el d\u00eda"
- },
- "no-equipment": {
- "title": "Sin equipamiento",
- "description": "Entrena donde quieras"
- },
- "7-day-burn": {
- "title": "Desaf\u00edo 7 d\u00edas de quema",
- "description": "Transf\u00f3rmate en una semana"
- },
- "quick-intense": {
- "title": "R\u00e1pido e intenso",
- "description": "M\u00e1ximo esfuerzo en 4 minutos"
- },
- "core-focus": {
- "title": "Enfoque en core",
- "description": "Construye una base s\u00f3lida"
- },
- "leg-day": {
- "title": "D\u00eda de piernas",
- "description": "Nunca te saltes el d\u00eda de piernas"
- }
- },
- "programs": {
- "beginner-journey": {
- "title": "Camino del principiante",
- "description": "Tus primeros pasos en el fitness Tabata"
- },
- "strength-builder": {
- "title": "Constructor de fuerza",
- "description": "Desarrollo progresivo de fuerza"
- },
- "fat-burn-protocol": {
- "title": "Protocolo quema grasa",
- "description": "Programa de m\u00e1xima quema de calor\u00edas"
- },
- "upper-body": {
- "title": "Tren Superior",
- "description": "Hombros, pecho, espalda y brazos con progresiones dise\u00f1adas por fisioterapeutas"
- },
- "lower-body": {
- "title": "Tren Inferior",
- "description": "Piernas, gl\u00fateos y caderas potentes con movimientos suaves para las articulaciones"
- },
- "full-body": {
- "title": "Cuerpo Completo",
- "description": "Transformaci\u00f3n total del cuerpo usando solo tu peso corporal"
- }
- },
- "achievements": {
- "first-burn": {
- "title": "Primera quema",
- "description": "Completa tu primer entreno"
- },
- "week-warrior": {
- "title": "Guerrero semanal",
- "description": "Racha de 7 d\u00edas"
- },
- "century-club": {
- "title": "Club del centenario",
- "description": "Quema 100 calor\u00edas en total"
- },
- "iron-will": {
- "title": "Voluntad de hierro",
- "description": "Completa 10 entrenos"
- },
- "tabata-master": {
- "title": "Maestro Tabata",
- "description": "Completa 50 entrenos"
- },
- "marathon-burner": {
- "title": "Quemador marat\u00f3n",
- "description": "Ejerc\u00edtate 100 minutos en total"
- },
- "unstoppable": {
- "title": "Imparable",
- "description": "Racha de 30 d\u00edas"
- },
- "calorie-crusher": {
- "title": "Destructor de calor\u00edas",
- "description": "Quema 1000 calor\u00edas en total"
- }
- }
-}
diff --git a/src/shared/i18n/locales/es/notifications.json b/src/shared/i18n/locales/es/notifications.json
deleted file mode 100644
index 5192895..0000000
--- a/src/shared/i18n/locales/es/notifications.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "dailyReminder": {
- "title": "\u00a1Es hora de tu Tabata!",
- "body": "4 minutos para transformar tu d\u00eda."
- }
-}
diff --git a/src/shared/i18n/locales/es/screens.json b/src/shared/i18n/locales/es/screens.json
deleted file mode 100644
index 9b158f0..0000000
--- a/src/shared/i18n/locales/es/screens.json
+++ /dev/null
@@ -1,465 +0,0 @@
-{
- "tabs": {
- "home": "Inicio",
- "explore": "Explorar",
- "programs": "Programas",
- "activity": "Actividad",
- "progression": "Progresión",
- "profile": "Perfil"
- },
- "home": {
- "readyToCrush": "¿Listo para arrasar hoy?",
- "featured": "DESTACADO",
- "recent": "Recientes",
- "popularThisWeek": "Popular esta semana",
- "collections": "Colecciones",
- "chooseYourPath": "Elige tu camino",
- "continueYourJourney": "Continúa tu viaje",
- "yourPrograms": "Tus programas",
- "programsSubtitle": "Diseñados por fisioterapeutas",
- "switchProgram": "Cambiar programa",
- "statsStreak": "Racha",
- "statsThisWeek": "Esta semana",
- "statsMinutes": "Minutos",
- "upperBody": "Parte superior",
- "lowerBody": "Parte inferior",
- "fullBody": "Cuerpo completo",
- "programsByZone": "Programas por zona",
- "filterAll": "Todos",
- "tabataCount": "{{count}} tabatas",
- "freeBadge": "GRATIS",
- "premiumBadge": "PREMIUM",
- "startProgram": "Empezar",
- "continueProgram": "Continuar",
- "unlockPremium": "Desbloquear Premium",
- "tabataPrograms": "Programas de fisio",
- "tabataProgramsSubtitle": "Programas de rehabilitación y fisioterapia",
- "recommendedNext": "Continuar tu sesión",
- "mascotStreak": "¡Racha de {{count}} días{{name}}! 🔥",
- "mascotReady": "¿Listo para entrenar{{name}}?",
- "statsCompleted": "Programas",
- "zoneSubtitle": "3 niveles · Principiante → Avanzado",
- "zoneDescUpper": "Hombros, pecho, brazos y core",
- "zoneDescLower": "Piernas, glúteos y movilidad de cadera",
- "zoneDescFull": "Acondicionamiento total y cardio",
- "zoneLevels": "3 niveles",
- "zonePrograms": "{{count}} programas"
- },
- "explore": {
- "title": "Explorar",
- "collections": "Colecciones",
- "featured": "Destacados",
- "allWorkouts": "Todos los entrenos",
- "trainers": "Entrenadores",
- "noResults": "No se encontraron entrenos",
- "tryAdjustingFilters": "Intenta ajustar tus filtros o búsqueda",
- "loading": "Cargando...",
- "filterCategory": "Categoría",
- "filterLevel": "Nivel",
- "filterEquipment": "Equipo",
- "filterDuration": "Duración",
- "clearFilters": "Borrar",
- "workoutsCount": "{{count}} entrenos",
- "workouts": "Entrenos",
- "equipmentOptions": {
- "none": "Sin equipo",
- "band": "Banda elástica",
- "dumbbells": "Mancuernas",
- "mat": "Colchoneta"
- },
- "allEquipment": "Todo el equipo",
- "searchPlaceholder": "Buscar entrenos, entrenadores...",
- "recommendedForYou": "Recomendado para ti",
- "tryNewCategory": "Prueba algo nuevo",
- "startFirstWorkout": "Completa tu primer entreno para recomendaciones personalizadas",
- "filters": "Filtros",
- "activeFilters": "{{count}} activos",
- "applyFilters": "Aplicar",
- "resetFilters": "Restablecer",
- "errorTitle": "No se pudieron cargar los entrenos",
- "errorRetry": "Toca para reintentar",
- "featuredCollection": "Colección destacada"
- },
- "activity": {
- "title": "Actividad",
- "dayStreak": "días consecutivos",
- "longest": "MÁS LARGO",
- "workouts": "Entrenos",
- "minutes": "Minutos",
- "calories": "Calorías",
- "bestStreak": "Mejor racha",
- "thisWeek": "Esta semana",
- "ofDays": "{{completed}} de 7 días",
- "recent": "Recientes",
- "today": "Hoy",
- "yesterday": "Ayer",
- "daysAgo": "hace {{count}}d",
- "achievements": "Logros",
- "emptyTitle": "Sin actividad aún",
- "emptySubtitle": "Completa tu primer entreno y tus estadísticas aparecerán aquí.",
- "startFirstWorkout": "Comienza tu primer entreno"
- },
- "profile": {
- "title": "Perfil",
- "guest": "Invitado",
- "memberSince": "Miembro desde {{date}}",
- "sectionAccount": "CUENTA",
- "sectionWorkout": "ENTRENAMIENTO",
- "sectionNotifications": "NOTIFICACIONES",
- "sectionAbout": "ACERCA DE",
- "sectionSubscription": "SUSCRIPCIÓN",
- "email": "Correo electrónico",
- "plan": "Plan",
- "freePlan": "Gratis",
- "restorePurchases": "Restaurar compras",
- "hapticFeedback": "Retroalimentación háptica",
- "soundEffects": "Efectos de sonido",
- "voiceCoaching": "Coaching por voz",
- "dailyReminders": "Recordatorios diarios",
- "reminderTime": "Hora del recordatorio",
- "reminderFooter": "Recibe un recordatorio diario para mantener tu racha",
- "workoutSettingsFooter": "Personaliza tu experiencia de entrenamiento",
- "upgradeTitle": "Desbloquear TabataFit+",
- "upgradeDescription": "Obtén entrenos ilimitados, descargas sin conexión y más.",
- "learnMore": "Más información",
- "version": "Versión",
- "privacyPolicy": "Política de privacidad",
- "termsOfService": "Términos de servicio",
- "signOut": "Cerrar sesión",
- "statsWorkouts": "entrenos",
- "statsStreak": "días consecutivos",
- "statsCalories": "calorías",
- "faq": "FAQ",
- "contactUs": "Contactarnos",
- "rateApp": "Calificar app",
- "sectionPremium": "Actualizar a Premium",
- "sectionPersonalization": "PERSONALIZACIÓN",
- "personalization": "Personalización",
- "personalizationEnabled": "Recomendaciones IA activas",
- "personalizationDisabled": "Active para entrenos personalizados",
- "enablePersonalization": "Activar personalización",
- "deleteData": "Eliminar mis datos"
- },
- "sync": {
- "title": "Desbloquea entrenos personalizados",
- "benefits": {
- "recommendations": "Recomendaciones IA basadas en tu progreso",
- "adaptive": "Dificultad adaptativa que crece contigo",
- "sync": "Sincroniza tu progreso en todos tus dispositivos",
- "secure": "Tus datos están cifrados y seguros"
- },
- "privacy": "Guardaremos tu historial de entrenos para crear tu programa personalizado. Puedes eliminar estos datos en cualquier momento en Configuración.",
- "primaryButton": "Personalizar mi experiencia",
- "secondaryButton": "Continuar con programas genéricos"
- },
- "dataDeletion": {
- "title": "¿Eliminar tus datos?",
- "description": "Esto eliminará permanentemente tu historial de entrenos sincronizado y tus datos de personalización de nuestros servidores.",
- "note": "Tu historial de entrenos local se mantendrá en este dispositivo. Continuarás con programas genéricos.",
- "deleteButton": "Eliminar mis datos",
- "cancelButton": "Conservar mis datos"
- },
- "player": {
- "phases": {
- "prep": "PREPÁRATE",
- "work": "TRABAJO",
- "rest": "DESCANSO",
- "complete": "COMPLETO",
- "warmup": "CALENTAMIENTO",
- "stretch": "ESTIRAMIENTO",
- "trans": "TRANSICIÓN"
- },
- "current": "Actual",
- "next": "Siguiente: ",
- "round": "Ronda",
- "burnBar": "Barra de quema",
- "communityAvg": "Media comunidad: {{calories}} cal",
- "workoutComplete": "¡Entreno completo!",
- "greatJob": "¡Buen trabajo!",
- "rounds": "Rondas",
- "calories": "Calorías",
- "minutes": "Minutos",
- "mute": "Silenciar",
- "unmute": "Activar sonido",
- "quitTitle": "¿Salir del entreno?",
- "quitMessage": "Tu progreso se perderá.",
- "quitConfirm": "Salir",
- "quitCancel": "Cancelar",
- "warmupTitle": "Calentamiento",
- "stretchTitle": "Estiramiento",
- "nextBlock": "Siguiente: Bloque {{num}}",
- "sessionComplete": "¡Entrenamiento completo!",
- "greatWork": "Excelente trabajo",
- "blocks": "Bloques"
- },
- "complete": {
- "title": "ENTRENO COMPLETO",
- "caloriesLabel": "CALORÍAS",
- "minutesLabel": "MINUTOS",
- "completeLabel": "COMPLETO",
- "burnBar": "Barra de quema",
- "burnBarResult": "¡Superaste al {{percentile}}% de usuarios!",
- "streakTitle": "¡{{count}} días consecutivos!",
- "streakSubtitle": "¡Mantén el impulso!",
- "shareWorkout": "Comparte tu entreno",
- "shareText": "¡Acabo de completar {{title}}! 🔥 {{calories}} calorías en {{duration}} minutos.",
- "recommendedNext": "Recomendado a continuación",
- "backToHome": "Volver al inicio",
- "feedbackTitle": "¿Cómo fue el entreno?",
- "feedbackEasy": "Fácil",
- "feedbackPerfect": "Perfecto",
- "feedbackHard": "Difícil",
- "thisWeek": "Esta semana",
- "share": "Compartir",
- "shareTitle": "Sesión completada · {{minutes}} min",
- "streakDays_one": "{{count}} día seguido",
- "streakDays_other": "{{count}} días seguidos",
- "streakRecord_one": "Récord: {{count}} día",
- "streakRecord_other": "Récord: {{count}} días"
- },
- "collection": {
- "notFound": "Colección no encontrada",
- "minTotal": "{{count}} min en total"
- },
- "category": {
- "allLevels": "Todos los niveles"
- },
- "workout": {
- "notFound": "Entreno no encontrado",
- "whatYoullNeed": "Lo que necesitarás",
- "exercises": "Ejercicios ({{count}} rondas)",
- "repeatRounds": "Repetir × {{count}} rondas",
- "music": "Música",
- "musicMix": "Mix {{vibe}}",
- "curatedForWorkout": "Seleccionado para tu entreno",
- "startWorkout": "EMPEZAR ENTRENO",
- "unlockWithPremium": "DESBLOQUEAR CON TABATAFIT+"
- },
- "paywall": {
- "subtitle": "Desbloquea todas las funciones y alcanza tus metas más rápido",
- "features": {
- "music": "Música Premium",
- "workouts": "Entrenos Ilimitados",
- "stats": "Estadísticas Avanzadas",
- "calories": "Seguimiento de Calorías",
- "reminders": "Recordatorios Diarios",
- "ads": "Sin Anuncios"
- },
- "yearly": "Anual",
- "monthly": "Mensual",
- "perYear": "por año",
- "perMonth": "por mes",
- "save50": "AHORRA 50%",
- "equivalent": "Solo {{price}}/mes",
- "subscribe": "Suscribirse Ahora",
- "trialCta": "Empezar Prueba Gratis",
- "processing": "Procesando...",
- "restore": "Restaurar Compras",
- "terms": "El pago se cargará a tu Apple ID al confirmar. La suscripción se renueva automáticamente a menos que se cancele al menos 24 horas antes del final del período. Gestiona en Ajustes de la cuenta.",
- "termsLink": "Términos de servicio",
- "privacyLink": "Política de privacidad"
- },
- "onboarding": {
- "problem": {
- "title": "No tienes 1 hora\npara el gimnasio.",
- "subtitle1": "Nadie la tiene.",
- "subtitle2": "Pero quieres resultados. Tenemos la solución.",
- "cta": "Enséñame cómo"
- },
- "empathy": {
- "title": "¿Qué te frena?",
- "chooseUpTo": "Elige hasta 2",
- "noTime": "Sin tiempo",
- "lowMotivation": "Poca motivación",
- "noKnowledge": "No sé qué hacer",
- "noGym": "Sin acceso al gimnasio"
- },
- "solution": {
- "title": "4 minutos.\nVerdaderamente transformadores.",
- "tabataCalories": "85 kcal",
- "cardioCalories": "90 kcal",
- "tabata": "Tabata",
- "cardio": "Cardio",
- "tabataDuration": "4 min",
- "cardioDuration": "30 min",
- "vs": "VS",
- "citation": "\"El método Tabata aumenta la capacidad aeróbica y anaeróbica simultáneamente.\"",
- "citationAuthor": "— Dr. Izumi Tabata, 1996",
- "cta": "Estoy convencido/a"
- },
- "wow": {
- "title": "Tu app, en vista previa.",
- "subtitle": "Todo lo que necesitas para transformarte.",
- "card1Title": "El cronómetro perfecto",
- "card1Subtitle": "TRABAJO, DESCANSO, REPETIR — fases cronometradas con retroalimentación visual.",
- "card2Title": "50+ entrenos expertos",
- "card2Subtitle": "De 4 minutos intensos a 20 minutos de quema. Principiante a avanzado.",
- "card3Title": "Coaching inteligente",
- "card3Subtitle": "Indicaciones de voz y retroalimentación háptica para mantenerte en la zona.",
- "card4Title": "Sigue tu progreso",
- "card4Subtitle": "Rachas semanales, seguimiento de calorías y récords personales."
- },
- "personalization": {
- "title": "Configuremos tu\nprimera semana.",
- "yourName": "TU NOMBRE",
- "namePlaceholder": "Introduce tu nombre",
- "fitnessLevel": "NIVEL DE FORMA",
- "yourGoal": "TU OBJETIVO",
- "weeklyFrequency": "FRECUENCIA SEMANAL",
- "readyMessage": "Tu programa personalizado está listo.",
- "goals": {
- "weightLoss": "Pérdida de peso",
- "cardio": "Cardio",
- "strength": "Fuerza",
- "wellness": "Bienestar"
- },
- "frequencies": {
- "2x": "2x / semana",
- "3x": "3x / semana",
- "5x": "5x / semana"
- }
- },
- "paywall": {
- "title": "Mantén el impulso.\nSin límites.",
- "features": {
- "unlimited": "Entrenos ilimitados",
- "offline": "Descargas sin conexión",
- "stats": "Estadísticas avanzadas y Apple Watch",
- "noAds": "Sin anuncios + Compartir en familia"
- },
- "bestValue": "MEJOR OFERTA",
- "yearlyPrice": "$49.99",
- "monthlyPrice": "$6.99",
- "savePercent": "Ahorra 42%",
- "trialCta": "EMPEZAR PRUEBA GRATIS (7 días)",
- "guarantees": "Cancela cuando quieras · Garantía de devolución de 30 días",
- "restorePurchases": "Restaurar compras",
- "skipButton": "Continuar sin suscripción"
- }
- },
- "programs": {
- "title": "Programas",
- "weeks": "Semanas",
- "week": "Semana",
- "workouts": "Entrenamientos",
- "workout": "Entrenamiento",
- "minutes": "Minutos",
- "min": "min",
- "perWeek": "/semana",
- "equipment": "Equipo",
- "optional": "(opcional)",
- "bodyweightOnly": "Solo peso corporal",
- "focusAreas": "Áreas de enfoque",
- "exercises": "ejercicios",
- "of": "de",
- "complete": "completado",
- "completed": "Completado",
- "notStarted": "No iniciado",
- "inProgress": "En progreso",
- "allWorkoutsComplete": "¡Todos los entrenamientos completados!",
- "status": {
- "notStarted": "No iniciado",
- "inProgress": "En progreso",
- "complete": "Completado",
- "completed": "Completado"
- },
- "yourProgress": "Tu progreso",
- "trainingPlan": "Plan de entrenamiento",
- "current": "Actual",
- "startProgram": "Iniciar programa",
- "continue": "Continuar",
- "continueTraining": "Continuar entrenamiento",
- "restart": "Reiniciar",
- "restartProgram": "Reiniciar programa"
- },
- "workoutProgram": {
- "tabata": "Tabata",
- "exercise1": "Ejercicio 1",
- "exercise2": "Ejercicio 2",
- "rounds": "{{count}} rondas",
- "startProgram": "Iniciar programa",
- "tabataLabel": "Tabata {{position}}",
- "beginner": "Principiante",
- "intermediate": "Intermedio",
- "advanced": "Avanzado"
- },
- "terms": {
- "title": "Términos de servicio",
- "lastUpdated": "Última actualización: marzo 2026",
- "acceptance": {
- "title": "Aceptación",
- "content": "Al usar TabataFit, aceptas estos términos de servicio."
- },
- "service": {
- "title": "Descripción del servicio",
- "content": "TabataFit es una app de fitness para entrenamiento Tabata con temporizador, programas y seguimiento de progreso."
- },
- "subscription": {
- "title": "Suscripción",
- "content": "Las funciones premium requieren suscripción. El cobro se realiza a través de tu Apple ID. La renovación automática se puede desactivar en Ajustes de la cuenta."
- },
- "liability": {
- "title": "Limitación de responsabilidad",
- "content": "TabataFit no se hace responsable de lesiones. Consulta a un médico antes de comenzar cualquier programa de ejercicios."
- },
- "ip": {
- "title": "Propiedad intelectual",
- "content": "Todo el contenido está protegido por derechos de autor."
- },
- "contact": {
- "title": "Contacto",
- "content": "Para consultas: support@tabatafit.app"
- }
- },
- "zone": {
- "chooseYourFocus": "Elige tu enfoque",
- "upperBody": {
- "label": "Tren superior",
- "description": "Brazos, hombros, pecho y espalda."
- },
- "lowerBody": {
- "label": "Tren inferior",
- "description": "Piernas, glúteos y core."
- },
- "fullBody": {
- "label": "Cuerpo completo",
- "description": "Entrenamientos completos, cada grupo muscular."
- },
- "chooseLevel": "Elige tu nivel",
- "emptyPrograms": "Ningún programa en este nivel por ahora."
- },
- "settings": {
- "title": "Ajustes",
- "sectionProfile": "PERFIL",
- "name": "Nombre",
- "subscription": "Suscripción",
- "premium": "Premium",
- "free": "Gratuito",
- "upgradePremium": "Actualizar a Premium",
- "sectionPrefs": "PREFERENCIAS",
- "haptics": "Vibración",
- "soundEffects": "Efectos de sonido",
- "voiceCoaching": "Entrenador de voz",
- "music": "Música",
- "sectionLegal": "LEGAL",
- "terms": "Términos de servicio",
- "privacy": "Política de privacidad",
- "sectionData": "DATOS",
- "resetProgress": "Restablecer progreso",
- "resetTitle": "¿Restablecer progreso?",
- "resetMessage": "Esta acción es irreversible. Tu racha, historial y programas completados serán borrados.",
- "resetConfirm": "Restablecer",
- "version": "TabataGo · v1.0"
- },
- "program": {
- "notFound": "Programa no encontrado",
- "warmup": "Calentamiento",
- "stretch": "Estiramientos",
- "exercise1": "Ejercicio 1",
- "exercise2": "Ejercicio 2",
- "tabataLabel": "Tabata {{num}}",
- "tabataSubtitle": "{{rounds}} rondas · {{work}}s / {{rest}}s",
- "startSession": "Iniciar sesión",
- "unlockPremium": "Desbloquear con Premium"
- }
-}
diff --git a/src/shared/i18n/locales/fr/CLAUDE.md b/src/shared/i18n/locales/fr/CLAUDE.md
deleted file mode 100644
index adfdcb1..0000000
--- a/src/shared/i18n/locales/fr/CLAUDE.md
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# Recent Activity
-
-
-
-*No recent activity*
-
\ No newline at end of file
diff --git a/src/shared/i18n/locales/fr/common.json b/src/shared/i18n/locales/fr/common.json
deleted file mode 100644
index 1b25354..0000000
--- a/src/shared/i18n/locales/fr/common.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "start": "COMMENCER",
- "continue": "Continuer",
- "next": "Suivant",
- "done": "Termin\u00e9",
- "seeAll": "Tout voir",
- "back": "Retour",
- "share": "Partager",
- "cancel": "Annuler",
- "save": "Enregistrer",
- "loading": "Chargement...",
- "offline": "Pas de connexion internet",
-
- "levels": {
- "beginner": "D\u00e9butant",
- "intermediate": "Interm\u00e9diaire",
- "advanced": "Avanc\u00e9"
- },
-
- "categories": {
- "all": "Tout",
- "fullBody": "Full Body",
- "core": "Abdos",
- "upperBody": "Haut du corps",
- "lowerBody": "Bas du corps",
- "cardio": "Cardio"
- },
-
- "days": {
- "sun": "Dim",
- "mon": "Lun",
- "tue": "Mar",
- "wed": "Mer",
- "thu": "Jeu",
- "fri": "Ven",
- "sat": "Sam"
- },
-
- "greetings": {
- "morning": "Bonjour",
- "afternoon": "Bon apr\u00e8s-midi",
- "evening": "Bonsoir"
- },
-
- "units": {
- "min": "min",
- "cal": "cal",
- "minUnit": "{{count}} min",
- "calUnit": "{{count}} cal",
- "sWork": "{{count}}s effort",
- "perYear": "par an",
- "perMonth": "par mois",
- "perWeek": "/semaine"
- },
-
- "plurals": {
- "workout_one": "{{count}} entra\u00eenement",
- "workout_other": "{{count}} entra\u00eenements",
- "day_one": "{{count}} jour",
- "day_other": "{{count}} jours",
- "round_one": "{{count}} round",
- "round_other": "{{count}} rounds",
- "week_one": "{{count}} semaine",
- "week_other": "{{count}} semaines"
- },
-
- "workoutMeta": "{{duration}} min \u00B7 {{level}} \u00B7 {{calories}} cal",
- "durationLevel": "{{duration}} min \u00B7 {{level}}",
- "calMin": "{{calories}} cal \u00B7 {{duration}} min",
-
- "musicVibes": {
- "electronic": "\u00c9lectronique",
- "hipHop": "Hip-Hop",
- "pop": "Pop",
- "rock": "Rock",
- "chill": "Chill"
- }
-}
diff --git a/src/shared/i18n/locales/fr/content.json b/src/shared/i18n/locales/fr/content.json
deleted file mode 100644
index 949238e..0000000
--- a/src/shared/i18n/locales/fr/content.json
+++ /dev/null
@@ -1,322 +0,0 @@
-{
- "workouts": {
- "1": "Full Body Ignite",
- "2": "Total Body Blast",
- "3": "Power Surge",
- "4": "R\u00e9veil Matinal",
- "5": "Endurance Builder",
- "6": "Br\u00fblure Express",
- "7": "Functional Flow",
- "8": "Puissance Athl\u00e9tique",
- "9": "Session Sueur",
- "10": "Tonus Total",
- "11": "Core Crusher",
- "12": "Ab Shredder",
- "13": "Abdos Fondamentaux",
- "14": "Obliqu\u00e9s Inferno",
- "15": "Endurance Abdos",
- "16": "Abdos Doux",
- "17": "Core Power",
- "18": "Abdos 360",
- "19": "Stabilit\u00e9 Abdos",
- "20": "Abdos Marathon",
- "21": "Haut du Corps Blitz",
- "22": "Sculpteur de Bras",
- "23": "Ma\u00eetrise des Pompes",
- "24": "\u00c9paules Sculpt\u00e9es",
- "25": "Pectoraux & Dos",
- "26": "Haut du Corps Poids de Corps",
- "27": "Tabata Force",
- "28": "Tonifier & D\u00e9finir",
- "29": "Bras Puissants",
- "30": "Endurance Haut du Corps",
- "31": "Br\u00fblure Bas du Corps",
- "32": "Leg Day Tabata",
- "33": "Activation Fessiers",
- "34": "Jambes Explosives",
- "35": "D\u00e9fi Squats",
- "36": "Puissance Bas du Corps",
- "37": "Jambes Sans Impact",
- "38": "Sprint Tabata",
- "39": "Jambes & Fessiers",
- "40": "Marathon Jambes",
- "41": "HIIT Extr\u00eame",
- "42": "Cardio Blast",
- "43": "Cardio Danse",
- "44": "Br\u00fbleur de Graisses Express",
- "45": "Cardio Faible Impact",
- "46": "Cardio Inferno",
- "47": "Flow Matinal",
- "48": "Power Hour",
- "49": "\u00c9tirements Profonds",
- "50": "Cardio Marathon"
- },
- "exercises": {
- "jumping-jacks": "Jumping Jacks",
- "squats": "Squats",
- "push-ups": "Pompes",
- "high-knees": "Mont\u00e9es de genoux",
- "burpees": "Burpees",
- "lunges": "Fentes",
- "push-up-to-row": "Pompe-Tirage",
- "mountain-climbers": "Mountain Climbers",
- "thrusters": "Thrusters",
- "box-jumps": "Box Jumps",
- "renegade-rows": "Renegade Rows",
- "tuck-jumps": "Sauts group\u00e9s",
- "arm-circles": "Cercles de bras",
- "bodyweight-squats": "Squats poids de corps",
- "knee-push-ups": "Pompes sur les genoux",
- "marching-in-place": "Marche sur place",
- "devil-press": "Devil Press",
- "squat-jumps": "Squats saut\u00e9s",
- "push-up-burpees": "Pompes Burpees",
- "plank-jacks": "Plank Jacks",
- "star-jumps": "Sauts en \u00e9toile",
- "wall-sit": "Chaise",
- "tricep-dips": "Dips triceps",
- "butt-kicks": "Talons-fesses",
- "inchworms": "Inchworms",
- "curtsy-lunges": "Fentes crois\u00e9es",
- "bear-crawls": "Marche de l'ours",
- "squat-pulses": "Squats pulses",
- "clean-and-press": "Clean & Press",
- "lateral-bounds": "Bonds lat\u00e9raux",
- "slam-ball": "Slam Ball",
- "sprawls": "Sprawls",
- "jumping-lunges": "Fentes saut\u00e9es",
- "push-up-to-t": "Pompe en T",
- "speed-skaters": "Speed Skaters",
- "plank-shoulder-taps": "Planche touches d'\u00e9paules",
- "side-lunges": "Fentes lat\u00e9rales",
- "diamond-push-ups": "Pompes diamant",
- "glute-bridges": "Ponts fessiers",
- "toe-touches": "Touch\u00e9s d'orteils",
- "crunches": "Crunchs",
- "russian-twists": "Rotations russes",
- "leg-raises": "\u00c9l\u00e9vations de jambes",
- "plank-hold": "Gainage",
- "v-ups": "V-Ups",
- "bicycle-crunches": "Crunchs bicyclette",
- "dragon-flags": "Dragon Flags",
- "flutter-kicks": "Battements de jambes",
- "dead-bug": "Dead Bug",
- "bird-dog": "Bird Dog",
- "side-plank-l": "Planche lat\u00e9rale (G)",
- "side-plank-r": "Planche lat\u00e9rale (D)",
- "woodchoppers": "B\u00fbcherons",
- "side-crunches": "Crunchs lat\u00e9raux",
- "windshield-wipers": "Essuie-glaces",
- "plank-hip-dips": "Planche rotations hanches",
- "hollow-body-hold": "Hollow Body Hold",
- "turkish-get-ups": "Relev\u00e9s turcs",
- "ab-rollouts": "Ab Rollouts",
- "pallof-press": "Pallof Press",
- "pelvic-tilts": "Bascules du bassin",
- "modified-crunches": "Crunchs modifi\u00e9s",
- "supine-leg-march": "Marche de jambes allong\u00e9",
- "cat-cow": "Chat-Vache",
- "plank-to-push-up": "Planche vers pompe",
- "reverse-crunches": "Crunchs invers\u00e9s",
- "bear-hold": "Position de l'ours",
- "l-sits": "L-Sits",
- "dragon-flag-negatives": "Dragon Flag n\u00e9gatifs",
- "hanging-knee-raises": "Mont\u00e9es de genoux suspendues",
- "plank-walkouts": "Planche marche",
- "forearm-plank": "Gainage avant-bras",
- "slow-bicycle": "Bicyclette lente",
- "glute-bridge-march": "Pont fessier march\u00e9",
- "prone-cobra": "Cobra ventral",
- "stir-the-pot": "Stir the Pot",
- "ab-wheel-rollout": "Roue abdominale",
- "hanging-leg-raises": "\u00c9l\u00e9vations de jambes suspendues",
- "dumbbell-rows": "Tirages halt\u00e8res",
- "shoulder-press": "D\u00e9velopp\u00e9 \u00e9paules",
- "bicep-curls": "Curls biceps",
- "tricep-extensions": "Extensions triceps",
- "lateral-raises": "\u00c9l\u00e9vations lat\u00e9rales",
- "front-raises": "\u00c9l\u00e9vations frontales",
- "wide-push-ups": "Pompes larges",
- "decline-push-ups": "Pompes d\u00e9clin\u00e9es",
- "explosive-push-ups": "Pompes explosives",
- "arnold-press": "Arnold Press",
- "upright-rows": "Tirages verticaux",
- "face-pulls": "Face Pulls",
- "handstand-hold": "Appui tendu renvers\u00e9",
- "chest-press": "D\u00e9velopp\u00e9 couch\u00e9",
- "bent-over-rows": "Tirages pench\u00e9s",
- "chest-flyes": "\u00c9cart\u00e9s pectoraux",
- "pull-ups": "Tractions",
- "tricep-dips-chair": "Dips triceps (chaise)",
- "clean-and-jerk": "Clean & Jerk",
- "turkish-get-up": "Relev\u00e9 turc",
- "overhead-press": "D\u00e9velopp\u00e9 militaire",
- "hammer-curls": "Curls marteau",
- "overhead-tricep": "Triceps au-dessus de la t\u00eate",
- "reverse-flyes": "\u00c9cart\u00e9s invers\u00e9s",
- "concentration-curls": "Curls concentration",
- "skull-crushers": "Skull Crushers",
- "zottman-curls": "Curls Zottman",
- "dips": "Dips",
- "barbell-press": "D\u00e9velopp\u00e9 barre",
- "dumbbell-snatch": "Arrach\u00e9 halt\u00e8re",
- "plyo-push-ups": "Pompes pliom\u00e9triques",
- "calf-raises": "Mollets debout",
- "jump-squats": "Squats saut\u00e9s",
- "walking-lunges": "Fentes march\u00e9es",
- "step-ups": "Step-ups",
- "donkey-kicks": "Donkey Kicks",
- "fire-hydrants": "Fire Hydrants",
- "clamshells": "Clamshells",
- "pistol-squats": "Pistol Squats",
- "broad-jumps": "Sauts en longueur",
- "goblet-squats": "Goblet Squats",
- "sumo-squats": "Squats sumo",
- "pulse-squats": "Squats pulses",
- "front-squats": "Squats avant",
- "romanian-deadlifts": "Soulevé de terre roumain",
- "split-squats": "Squats bulgares",
- "power-cleans": "Power Cleans",
- "standing-kickbacks": "Kickbacks debout",
- "side-leg-raises": "\u00c9l\u00e9vations lat\u00e9rales de jambes",
- "high-knees-sprint": "Sprint mont\u00e9es de genoux",
- "lateral-shuffles": "Pas chass\u00e9s",
- "butt-kick-sprint": "Sprint talons-fesses",
- "banded-squats": "Squats avec \u00e9lastique",
- "hip-thrusts": "Hip Thrusts",
- "lateral-band-walks": "Marche lat\u00e9rale \u00e9lastique",
- "step-back-lunges": "Fentes arri\u00e8re",
- "back-squats": "Squats arri\u00e8re",
- "leg-press-jumps": "Sauts presse \u00e0 jambes",
- "box-jump-overs": "Box Jump Overs",
- "grapevine": "Pas crois\u00e9s",
- "step-touch": "Step Touch",
- "cha-cha-slide": "Cha-Cha Slide",
- "jump-rope": "Corde \u00e0 sauter",
- "step-touches": "Step Touches",
- "boxing-punches": "Coups de poing boxe",
- "side-steps": "Pas lat\u00e9raux",
- "burpee-tuck-jumps": "Burpees sauts group\u00e9s",
- "sprint-in-place": "Sprint sur place",
- "plyo-lunges": "Fentes pliom\u00e9triques",
- "sun-salutation": "Salutation au soleil",
- "light-jog": "Jogging l\u00e9ger",
- "arm-swings": "Balancements de bras",
- "gentle-twists": "Rotations douces",
- "double-unders": "Double Unders",
- "box-jump-burpees": "Box Jump Burpees",
- "sprint-intervals": "Intervalles de sprint",
- "forward-fold": "Flexion avant",
- "pigeon-pose": "Posture du pigeon",
- "spinal-twist": "Torsion vertébrale",
- "butterfly-stretch": "\u00c9tirement papillon"
- },
- "equipment": {
- "no-equipment-required": "Aucun \u00e9quipement requis",
- "yoga-mat-optional": "Tapis de yoga optionnel",
- "yoga-mat": "Tapis de yoga",
- "dumbbells-optional": "Halt\u00e8res optionnels",
- "dumbbells": "Halt\u00e8res",
- "kettlebell-recommended": "Kettlebell recommand\u00e9",
- "kettlebell": "Kettlebell",
- "resistance-band-optional": "\u00c9lastique optionnel",
- "resistance-band": "\u00c9lastique de r\u00e9sistance",
- "medicine-ball": "M\u00e9decine-ball",
- "light-dumbbells": "Halt\u00e8res l\u00e9gers",
- "pull-up-bar": "Barre de traction",
- "full-gym-setup": "\u00c9quipement complet de salle",
- "box-or-step": "Box ou step",
- "chair-for-balance": "Chaise pour l'\u00e9quilibre",
- "jump-rope-optional": "Corde \u00e0 sauter optionnelle",
- "jump-rope": "Corde \u00e0 sauter",
- "stability-ball": "Ballon de stabilit\u00e9",
- "barbell": "Barre d'halt\u00e8res",
- "dumbbell-optional": "Halt\u00e8re optionnel"
- },
- "collections": {
- "morning-energizer": {
- "title": "\u00c9nergie Matinale",
- "description": "Bien d\u00e9marrer la journ\u00e9e"
- },
- "no-equipment": {
- "title": "Sans \u00c9quipement",
- "description": "Entra\u00eenez-vous partout"
- },
- "7-day-burn": {
- "title": "D\u00e9fi Br\u00fblure 7 Jours",
- "description": "Transformez-vous en une semaine"
- },
- "quick-intense": {
- "title": "Rapide & Intense",
- "description": "Effort max en 4 minutes"
- },
- "core-focus": {
- "title": "Focus Abdos",
- "description": "Construisez une base solide"
- },
- "leg-day": {
- "title": "Jour des Jambes",
- "description": "Ne sautez jamais le jour des jambes"
- }
- },
- "programs": {
- "beginner-journey": {
- "title": "Parcours D\u00e9butant",
- "description": "Vos premiers pas dans le fitness Tabata"
- },
- "strength-builder": {
- "title": "Construction de Force",
- "description": "D\u00e9veloppement progressif de la force"
- },
- "fat-burn-protocol": {
- "title": "Protocole Br\u00fble-Graisses",
- "description": "Programme de br\u00fblure maximale de calories"
- },
- "upper-body": {
- "title": "Haut du Corps",
- "description": "\u00c9paules, poitrine, dos et bras avec des progressions con\u00e7ues par des kin\u00e9s"
- },
- "lower-body": {
- "title": "Bas du Corps",
- "description": "Jambes, fessiers et hanches puissants avec des mouvements doux pour les articulations"
- },
- "full-body": {
- "title": "Corps Complet",
- "description": "Transformation compl\u00e8te du corps avec seulement votre poids de corps"
- }
- },
- "achievements": {
- "first-burn": {
- "title": "Premi\u00e8re Flamme",
- "description": "Terminez votre premier entra\u00eenement"
- },
- "week-warrior": {
- "title": "Guerrier de la Semaine",
- "description": "S\u00e9rie de 7 jours"
- },
- "century-club": {
- "title": "Club des 100",
- "description": "Br\u00fblez 100 calories au total"
- },
- "iron-will": {
- "title": "Volont\u00e9 de Fer",
- "description": "Terminez 10 entra\u00eenements"
- },
- "tabata-master": {
- "title": "Ma\u00eetre Tabata",
- "description": "Terminez 50 entra\u00eenements"
- },
- "marathon-burner": {
- "title": "Marathonien du Br\u00fblage",
- "description": "Exercez-vous pendant 100 minutes au total"
- },
- "unstoppable": {
- "title": "Inarr\u00eatable",
- "description": "S\u00e9rie de 30 jours"
- },
- "calorie-crusher": {
- "title": "Broyeur de Calories",
- "description": "Br\u00fblez 1000 calories au total"
- }
- }
-}
diff --git a/src/shared/i18n/locales/fr/notifications.json b/src/shared/i18n/locales/fr/notifications.json
deleted file mode 100644
index e66ac10..0000000
--- a/src/shared/i18n/locales/fr/notifications.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "dailyReminder": {
- "title": "C'est l'heure de votre Tabata !",
- "body": "4 minutes pour transformer votre journ\u00e9e."
- }
-}
diff --git a/src/shared/i18n/locales/fr/screens.json b/src/shared/i18n/locales/fr/screens.json
deleted file mode 100644
index b50e05b..0000000
--- a/src/shared/i18n/locales/fr/screens.json
+++ /dev/null
@@ -1,502 +0,0 @@
-{
- "tabs": {
- "home": "Accueil",
- "explore": "Explorer",
- "programs": "Programmes",
- "activity": "Activité",
- "progression": "Progression",
- "profile": "Profil"
- },
- "home": {
- "readyToCrush": "Prêt à tout casser aujourd'hui ?",
- "featured": "À LA UNE",
- "recent": "Récents",
- "popularThisWeek": "Populaires cette semaine",
- "collections": "Collections",
- "chooseYourPath": "Choisissez votre parcours",
- "continueYourJourney": "Continuez votre parcours",
- "yourPrograms": "Vos programmes",
- "programsSubtitle": "Conçus par des kinésithérapeutes",
- "switchProgram": "Changer de programme",
- "statsStreak": "Série",
- "statsThisWeek": "Cette semaine",
- "statsMinutes": "Minutes",
- "upperBody": "Haut du corps",
- "lowerBody": "Bas du corps",
- "fullBody": "Corps entier",
- "programsByZone": "Programmes par zone",
- "filterAll": "Tous",
- "tabataCount": "{{count}} tabatas",
- "freeBadge": "GRATUIT",
- "premiumBadge": "PREMIUM",
- "startProgram": "Démarrer",
- "continueProgram": "Continuer",
- "unlockPremium": "Débloquer Premium",
- "tabataPrograms": "Programmes Tabata",
- "tabataProgramsSubtitle": "Programmes de rééducation et physiothérapie",
- "recommendedNext": "Continuer votre séance",
- "mascotStreak": "Streak de {{count}} jours{{name}} ! 🔥",
- "mascotReady": "Prêt à bouger{{name}} ?",
- "statsCompleted": "Programmes",
- "zoneSubtitle": "3 niveaux · Débutant → Avancé",
- "zoneDescUpper": "Épaules, poitrine, bras & gainage",
- "zoneDescLower": "Jambes, fessiers & mobilité des hanches",
- "zoneDescFull": "Renforcement complet & cardio",
- "zoneLevels": "3 niveaux",
- "zonePrograms": "{{count}} programmes"
- },
- "explore": {
- "title": "Explorer",
- "collections": "Collections",
- "featured": "En vedette",
- "allWorkouts": "Tous les exercices",
- "trainers": "Entraîneurs",
- "noResults": "Aucun exercice trouvé",
- "tryAdjustingFilters": "Essayez d'ajuster vos filtres ou votre recherche",
- "loading": "Chargement...",
- "filterCategory": "Catégorie",
- "filterLevel": "Niveau",
- "filterEquipment": "Équipement",
- "filterDuration": "Durée",
- "clearFilters": "Effacer",
- "workoutsCount": "{{count}} exercices",
- "workouts": "Exercices",
- "equipmentOptions": {
- "none": "Sans équipement",
- "band": "Bande élastique",
- "dumbbells": "Haltères",
- "mat": "Tapis"
- },
- "allEquipment": "Tout l'équipement",
- "searchPlaceholder": "Rechercher exercices, entraîneurs...",
- "recommendedForYou": "Recommandé pour vous",
- "tryNewCategory": "Essayez quelque chose de nouveau",
- "startFirstWorkout": "Complétez votre premier exercice pour des recommandations personnalisées",
- "filters": "Filtres",
- "activeFilters": "{{count}} actifs",
- "applyFilters": "Appliquer",
- "resetFilters": "Réinitialiser",
- "errorTitle": "Impossible de charger les exercices",
- "errorRetry": "Appuyez pour réessayer",
- "featuredCollection": "Collection en vedette"
- },
- "activity": {
- "title": "Activité",
- "dayStreak": "jours consécutifs",
- "longest": "RECORD",
- "workouts": "Entraînements",
- "minutes": "Minutes",
- "calories": "Calories",
- "bestStreak": "Meilleure série",
- "thisWeek": "Cette semaine",
- "ofDays": "{{completed}} sur 7 jours",
- "recent": "Récents",
- "today": "Aujourd'hui",
- "yesterday": "Hier",
- "daysAgo": "il y a {{count}}j",
- "achievements": "Succès",
- "emptyTitle": "Aucune activité",
- "emptySubtitle": "Terminez votre premier entraînement et vos statistiques apparaîtront ici.",
- "startFirstWorkout": "Commencez votre premier entraînement"
- },
- "profile": {
- "title": "Profil",
- "guest": "Invité",
- "memberSince": "Membre depuis {{date}}",
- "sectionAccount": "COMPTE",
- "sectionWorkout": "ENTRAÎNEMENT",
- "sectionNotifications": "NOTIFICATIONS",
- "sectionAbout": "À PROPOS",
- "sectionSubscription": "ABONNEMENT",
- "email": "E-mail",
- "plan": "Formule",
- "freePlan": "Gratuit",
- "restorePurchases": "Restaurer les achats",
- "hapticFeedback": "Retour haptique",
- "soundEffects": "Effets sonores",
- "voiceCoaching": "Coaching vocal",
- "dailyReminders": "Rappels quotidiens",
- "reminderTime": "Heure du rappel",
- "reminderFooter": "Recevez un rappel quotidien pour maintenir votre série",
- "workoutSettingsFooter": "Personnalisez votre expérience d'entraînement",
- "upgradeTitle": "Débloquer TabataFit+",
- "upgradeDescription": "Accédez à des entraînements illimités, téléchargements hors ligne et plus.",
- "learnMore": "En savoir plus",
- "version": "Version",
- "privacyPolicy": "Politique de confidentialité",
- "termsOfService": "Conditions d'utilisation",
- "signOut": "Se déconnecter",
- "statsWorkouts": "entraînements",
- "statsStreak": "jours consécutifs",
- "statsCalories": "calories",
- "faq": "FAQ",
- "contactUs": "Nous contacter",
- "rateApp": "Noter l'app",
- "sectionPremium": "Passer à Premium",
- "sectionPersonalization": "PERSONNALISATION",
- "personalization": "Personnalisation",
- "personalizationEnabled": "Recommandations IA actives",
- "personalizationDisabled": "Activez pour des entraînements personnalisés",
- "enablePersonalization": "Activer la personnalisation",
- "deleteData": "Supprimer mes données"
- },
- "sync": {
- "title": "Débloquez les entraînements personnalisés",
- "benefits": {
- "recommendations": "Recommandations IA basées sur vos progrès",
- "adaptive": "Difficulté adaptative qui évolue avec vous",
- "sync": "Synchronisez vos progrès sur tous vos appareils",
- "secure": "Vos données sont chiffrées et sécurisées"
- },
- "privacy": "Nous sauvegarderons votre historique d'entraînement pour créer votre programme personnalisé. Vous pouvez supprimer ces données à tout moment dans les Paramètres.",
- "primaryButton": "Personnaliser mon expérience",
- "secondaryButton": "Continuer avec les programmes génériques"
- },
- "dataDeletion": {
- "title": "Supprimer vos données ?",
- "description": "Cela supprimera définitivement votre historique d'entraînement synchronisé et vos données de personnalisation de nos serveurs.",
- "note": "Votre historique d'entraînement local sera conservé sur cet appareil. Vous continuerez avec les programmes génériques.",
- "deleteButton": "Supprimer mes données",
- "cancelButton": "Conserver mes données"
- },
- "player": {
- "phases": {
- "prep": "PRÉPAREZ-VOUS",
- "work": "EFFORT",
- "rest": "REPOS",
- "complete": "TERMINÉ",
- "warmup": "ÉCHAUFFEMENT",
- "stretch": "ÉTIREMENT",
- "trans": "TRANSITION"
- },
- "current": "En cours",
- "next": "Suivant : ",
- "round": "Round",
- "burnBar": "Burn Bar",
- "communityAvg": "Moyenne communauté : {{calories}} cal",
- "workoutComplete": "Entraînement terminé !",
- "greatJob": "Bien joué !",
- "rounds": "Rounds",
- "calories": "Calories",
- "minutes": "Minutes",
- "mute": "Muet",
- "unmute": "Son",
- "quitTitle": "Quitter la séance ?",
- "quitMessage": "Votre progression ne sera pas sauvegardée. Êtes-vous sûr(e) ?",
- "quitConfirm": "Quitter",
- "quitCancel": "Continuer",
- "warmupTitle": "Échauffement",
- "stretchTitle": "Étirement",
- "nextBlock": "Prochain : Bloc {{num}}",
- "sessionComplete": "Séance terminée !",
- "greatWork": "Excellent travail",
- "blocks": "Blocs"
- },
- "complete": {
- "title": "ENTRAÎNEMENT TERMINÉ",
- "caloriesLabel": "CALORIES",
- "minutesLabel": "MINUTES",
- "completeLabel": "TERMINÉ",
- "burnBar": "Burn Bar",
- "burnBarResult": "Vous avez battu {{percentile}}% des utilisateurs !",
- "streakTitle": "{{count}} jours consécutifs !",
- "streakSubtitle": "Continuez sur cette lancée !",
- "shareWorkout": "Partagez votre entraînement",
- "shareText": "Je viens de terminer {{title}} ! 🔥 {{calories}} calories en {{duration}} minutes.",
- "recommendedNext": "Recommandé ensuite",
- "backToHome": "Retour à l'accueil",
- "feedbackTitle": "Comment c'était ?",
- "feedbackHard": "Dur",
- "feedbackPerfect": "Parfait",
- "feedbackEasy": "Trop facile",
- "thisWeek": "Cette semaine",
- "share": "Partager",
- "shareTitle": "Séance terminée · {{minutes}} min",
- "streakDays_one": "{{count}} jour d'affilée",
- "streakDays_other": "{{count}} jours d'affilée",
- "streakRecord_one": "Record : {{count}} jour",
- "streakRecord_other": "Record : {{count}} jours"
- },
- "terms": {
- "title": "Conditions Générales d'Utilisation",
- "lastUpdated": "Dernière mise à jour : Avril 2026",
- "acceptance": {
- "title": "Acceptation des conditions",
- "content": "En téléchargeant ou en utilisant TabataFit, vous acceptez d'être lié par ces Conditions Générales d'Utilisation."
- },
- "service": {
- "title": "Description du service",
- "content": "TabataFit propose des programmes d'entraînement Tabata guidés. L'application ne remplace pas un avis médical professionnel."
- },
- "subscriptions": {
- "title": "Abonnements et paiements",
- "content": "Certaines fonctionnalités nécessitent un abonnement payant géré via l'App Store ou Google Play. Les prix peuvent varier selon la région. Les essais gratuits se convertissent en abonnements payants sauf annulation avant la fin de l'essai."
- },
- "cancellation": {
- "title": "Annulation",
- "content": "Vous pouvez annuler votre abonnement à tout moment via les paramètres de votre appareil. L'annulation prend effet à la fin de la période de facturation en cours."
- },
- "liability": {
- "title": "Limitation de responsabilité",
- "content": "TabataFit est fourni tel quel. Nous ne sommes pas responsables des blessures résultant des entraînements. Consultez toujours un professionnel de santé avant de commencer un programme d'exercice."
- },
- "contact": {
- "title": "Contact",
- "content": "Pour toute question sur ces conditions, contactez-nous à :"
- }
- },
- "collection": {
- "notFound": "Collection introuvable",
- "minTotal": "{{count}} min au total"
- },
- "category": {
- "allLevels": "Tous les niveaux"
- },
- "workout": {
- "notFound": "Entraînement introuvable",
- "whatYoullNeed": "Ce qu'il vous faut",
- "exercises": "Exercices ({{count}} rounds)",
- "repeatRounds": "Répéter × {{count}} rounds",
- "music": "Musique",
- "musicMix": "Mix {{vibe}}",
- "curatedForWorkout": "Sélectionné pour votre entraînement",
- "startWorkout": "COMMENCER L'ENTRAÎNEMENT",
- "unlockWithPremium": "DÉBLOQUER AVEC TABATAFIT+"
- },
- "paywall": {
- "subtitle": "Débloquez toutes les fonctionnalités et atteignez vos objectifs plus vite",
- "features": {
- "music": "Musique Premium",
- "workouts": "Entraînements illimités",
- "stats": "Statistiques avancées",
- "calories": "Suivi des calories",
- "reminders": "Rappels quotidiens",
- "ads": "Sans publicités"
- },
- "yearly": "Annuel",
- "monthly": "Mensuel",
- "perYear": "par an",
- "perMonth": "par mois",
- "save50": "ÉCONOMISEZ 50%",
- "equivalent": "Seulement {{price}}/mois",
- "subscribe": "S'abonner maintenant",
- "trialCta": "Commencer l'essai gratuit",
- "processing": "Traitement...",
- "restore": "Restaurer les achats",
- "terms": "Le paiement sera débité sur votre identifiant Apple à la confirmation. L'abonnement se renouvelle automatiquement sauf annulation au moins 24h avant la fin de la période. Gérez dans les réglages du compte."
- },
- "onboarding": {
- "problem": {
- "title": "Vous n'avez pas 1 heure\npour la salle.",
- "subtitle1": "Personne n'a le temps.",
- "subtitle2": "Pourtant vous voulez des résultats. On a la solution.",
- "cta": "Montrez-moi"
- },
- "empathy": {
- "title": "Qu'est-ce qui vous freine ?",
- "chooseUpTo": "Choisissez jusqu'à 2",
- "noTime": "Pas le temps",
- "lowMotivation": "Manque de motivation",
- "noKnowledge": "Je ne sais pas quoi faire",
- "noGym": "Pas d'accès à une salle"
- },
- "solution": {
- "title": "4 minutes.\nVéritablement transformateur.",
- "tabataCalories": "85 kcal",
- "cardioCalories": "90 kcal",
- "tabata": "Tabata",
- "cardio": "Cardio",
- "tabataDuration": "4 min",
- "cardioDuration": "30 min",
- "vs": "VS",
- "citation": "« La méthode Tabata augmente simultanément les capacités aérobie et anaérobie. »",
- "citationAuthor": "— Dr. Izumi Tabata, 1996",
- "cta": "Je suis convaincu"
- },
- "wow": {
- "title": "Votre app, en avant-première.",
- "subtitle": "Tout ce qu'il vous faut pour vous transformer.",
- "card1Title": "Le chrono parfait",
- "card1Subtitle": "EFFORT, REPOS, RÉPÉTEZ — des phases minutées avec un retour visuel.",
- "card2Title": "50+ entraînements experts",
- "card2Subtitle": "De 4 minutes intenses à 20 minutes de brûlage. Débutant à avancé.",
- "card3Title": "Coaching intelligent",
- "card3Subtitle": "Indications vocales et retour haptique pour rester dans la zone.",
- "card4Title": "Suivez vos progrès",
- "card4Subtitle": "Séries hebdomadaires, suivi des calories et records personnels."
- },
- "personalization": {
- "title": "Préparons votre\npremière semaine.",
- "yourName": "VOTRE NOM",
- "namePlaceholder": "Entrez votre nom",
- "fitnessLevel": "NIVEAU DE FORME",
- "yourGoal": "VOTRE OBJECTIF",
- "weeklyFrequency": "FRÉQUENCE HEBDOMADAIRE",
- "readyMessage": "Votre programme personnalisé est prêt.",
- "goals": {
- "weightLoss": "Perte de poids",
- "cardio": "Cardio",
- "strength": "Force",
- "wellness": "Bien-être"
- },
- "frequencies": {
- "2x": "2x / semaine",
- "3x": "3x / semaine",
- "5x": "5x / semaine"
- }
- },
- "paywall": {
- "title": "Restez motivé.\nSans limites.",
- "features": {
- "unlimited": "Entraînements illimités",
- "offline": "Téléchargements hors ligne",
- "stats": "Statistiques avancées et Apple Watch",
- "noAds": "Sans publicités + Partage familial"
- },
- "bestValue": "MEILLEURE OFFRE",
- "yearlyPrice": "49,99 $",
- "monthlyPrice": "6,99 $",
- "savePercent": "Économisez 42%",
- "trialCta": "ESSAI GRATUIT (7 jours)",
- "guarantees": "Annulez à tout moment · Garantie satisfait ou remboursé 30 jours",
- "restorePurchases": "Restaurer les achats",
- "skipButton": "Continuer sans abonnement",
- "termsLink": "Conditions d'utilisation",
- "privacyLink": "Politique de confidentialité"
- },
- "privacy": {
- "title": "Politique de Confidentialité",
- "lastUpdated": "Dernière mise à jour : Mars 2026",
- "intro": {
- "title": "Introduction",
- "content": "TabataFit s'engage à protéger votre vie privée. Cette politique explique comment nous collectons, utilisons et protégeons vos informations lorsque vous utilisez notre application de fitness."
- },
- "dataCollection": {
- "title": "Données Collectées",
- "content": "Nous collectons uniquement les informations nécessaires pour vous offrir la meilleure expérience d'entraînement :",
- "items": {
- "workouts": "Historique et préférences d'entraînement",
- "settings": "Paramètres et configurations de l'app",
- "device": "Type d'appareil et version iOS pour l'optimisation"
- }
- },
- "usage": {
- "title": "Utilisation des Données",
- "content": "Vos données sont utilisées uniquement pour : personnaliser votre expérience, suivre vos progrès, synchroniser vos données sur vos appareils, et améliorer notre application."
- },
- "sharing": {
- "title": "Partage des Données",
- "content": "Nous ne vendons ni ne partageons vos informations personnelles avec des tiers. Vos données d'entraînement restent privées et sécurisées."
- },
- "security": {
- "title": "Sécurité",
- "content": "Nous mettons en œuvre des mesures de sécurité conformes aux standards de l'industrie pour protéger vos données, incluant le chiffrement et l'authentification sécurisée."
- },
- "rights": {
- "title": "Vos Droits",
- "content": "Vous avez le droit d'accéder, modifier ou supprimer vos données personnelles à tout moment. Vous pouvez exporter ou supprimer vos données depuis les paramètres de l'app."
- },
- "contact": {
- "title": "Nous Contacter",
- "content": "Si vous avez des questions sur cette politique de confidentialité, contactez-nous à :"
- }
- }
- },
- "programs": {
- "title": "Programmes",
- "weeks": "Semaines",
- "week": "Semaine",
- "workouts": "Entraînements",
- "workout": "Entraînement",
- "minutes": "Minutes",
- "min": "min",
- "perWeek": "/semaine",
- "equipment": "Équipement",
- "optional": "(optionnel)",
- "bodyweightOnly": "Poids du corps uniquement",
- "focusAreas": "Zones ciblées",
- "exercises": "exercices",
- "of": "sur",
- "complete": "terminé",
- "completed": "Terminé",
- "notStarted": "Non commencé",
- "inProgress": "En cours",
- "allWorkoutsComplete": "Tous les entraînements terminés !",
- "status": {
- "notStarted": "Non commencé",
- "inProgress": "En cours",
- "complete": "Terminé",
- "completed": "Terminé"
- },
- "yourProgress": "Votre progression",
- "trainingPlan": "Plan d'entraînement",
- "current": "Actuel",
- "startProgram": "Commencer le programme",
- "continue": "Continuer",
- "continueTraining": "Continuer l'entraînement",
- "restart": "Recommencer",
- "restartProgram": "Recommencer le programme"
- },
- "workoutProgram": {
- "tabata": "Tabata",
- "exercise1": "Exercice 1",
- "exercise2": "Exercice 2",
- "rounds": "{{count}} rounds",
- "startProgram": "Démarrer le programme",
- "tabataLabel": "Tabata {{position}}",
- "beginner": "Débutant",
- "intermediate": "Intermédiaire",
- "advanced": "Avancé"
- },
- "zone": {
- "chooseYourFocus": "Choisis ta zone",
- "upperBody": {
- "label": "Haut du corps",
- "description": "Bras, épaules, pectoraux et dos."
- },
- "lowerBody": {
- "label": "Bas du corps",
- "description": "Jambes, fessiers et gainage."
- },
- "fullBody": {
- "label": "Corps complet",
- "description": "Séances complètes, tous les groupes musculaires."
- },
- "chooseLevel": "Choisis ton niveau",
- "emptyPrograms": "Aucun programme à ce niveau pour le moment."
- },
- "settings": {
- "title": "Réglages",
- "sectionProfile": "PROFIL",
- "name": "Nom",
- "subscription": "Abonnement",
- "premium": "Premium",
- "free": "Gratuit",
- "upgradePremium": "Passer à Premium",
- "sectionPrefs": "PRÉFÉRENCES",
- "haptics": "Vibrations",
- "soundEffects": "Effets sonores",
- "voiceCoaching": "Coach vocal",
- "music": "Musique",
- "sectionLegal": "LÉGAL",
- "terms": "Conditions d'utilisation",
- "privacy": "Confidentialité",
- "sectionData": "DONNÉES",
- "resetProgress": "Réinitialiser la progression",
- "resetTitle": "Réinitialiser la progression ?",
- "resetMessage": "Cette action est irréversible. Ton streak, ton historique et tes programmes complétés seront effacés.",
- "resetConfirm": "Réinitialiser",
- "version": "TabataGo · v1.0"
- },
- "program": {
- "notFound": "Programme introuvable",
- "warmup": "Échauffement",
- "stretch": "Étirements",
- "exercise1": "Exercice 1",
- "exercise2": "Exercice 2",
- "tabataLabel": "Tabata {{num}}",
- "tabataSubtitle": "{{rounds}} rounds · {{work}}s / {{rest}}s",
- "startSession": "Commencer la séance",
- "unlockPremium": "Débloquer avec Premium"
- }
-}
diff --git a/src/shared/i18n/types.ts b/src/shared/i18n/types.ts
deleted file mode 100644
index e11fe94..0000000
--- a/src/shared/i18n/types.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * TabataFit i18n Type Augmentation
- * Provides typed t() keys via module augmentation
- */
-
-import 'react-i18next'
-
-import type enCommon from './locales/en/common.json'
-import type enScreens from './locales/en/screens.json'
-import type enContent from './locales/en/content.json'
-import type enNotifications from './locales/en/notifications.json'
-
-declare module 'react-i18next' {
- interface CustomTypeOptions {
- defaultNS: 'common'
- resources: {
- common: typeof enCommon
- screens: typeof enScreens
- content: typeof enContent
- notifications: typeof enNotifications
- }
- }
-}
diff --git a/src/shared/services/CLAUDE.md b/src/shared/services/CLAUDE.md
deleted file mode 100644
index de17f4c..0000000
--- a/src/shared/services/CLAUDE.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 17, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6384 | 10:35 AM | 🔄 | Enhanced workout program access control with explicit boolean checks | ~352 |
-
\ No newline at end of file
diff --git a/src/shared/services/access.ts b/src/shared/services/access.ts
deleted file mode 100644
index e102aad..0000000
--- a/src/shared/services/access.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Tabata Access Control Service
- * Manages free tier vs premium access
- *
- * Workout Programs: Free status from database is_free flag
- * Tabata: Free = entire Débutant program, Premium = everything else
- * Legacy: 3 free workouts kept for backward compatibility
- */
-
-import type { TabataProgramId } from '../types/program'
-import type { WorkoutProgram } from '../types/workoutProgram'
-
-// ─── Tabata Program Access ───────────────────────────────────────
-
-/** Program available without a subscription */
-export const FREE_PROGRAM_ID: TabataProgramId = 'debutant'
-
-/** Session ID prefix → program mapping */
-const SESSION_PREFIX_MAP: Record = {
- 'deb-': 'debutant',
- 'int-': 'intermediaire',
- 'avc-': 'avance',
- 'bur-': 'bureau',
-}
-
-/**
- * Check if a program is part of the free tier
- */
-export function isFreeProgram(programId: TabataProgramId): boolean {
- return programId === FREE_PROGRAM_ID
-}
-
-/**
- * Check if user can access a tabata program
- */
-export function canAccessProgram(programId: TabataProgramId, isPremium: boolean): boolean {
- if (isPremium) return true
- return isFreeProgram(programId)
-}
-
-/**
- * Check if user can access a specific tabata session
- * Extracts program from session ID prefix (e.g., 'deb-w1-s1' → 'debutant')
- */
-export function canAccessSession(sessionId: string, isPremium: boolean): boolean {
- if (isPremium) return true
- for (const [prefix, programId] of Object.entries(SESSION_PREFIX_MAP)) {
- if (sessionId.startsWith(prefix)) {
- return isFreeProgram(programId)
- }
- }
- // Unknown prefix — deny by default
- return false
-}
-
-/**
- * Get the program ID from a session ID
- */
-export function getSessionProgramId(sessionId: string): TabataProgramId | undefined {
- for (const [prefix, programId] of Object.entries(SESSION_PREFIX_MAP)) {
- if (sessionId.startsWith(prefix)) return programId
- }
- return undefined
-}
-
-// ─── Workout Program Access ─────────────────────────────────────
-
-/**
- * Check if user can access a workout program
- * Free status is determined by the is_free flag in the database.
- * Falls back to level-based rules if isFree is undefined.
- */
-export function canAccessWorkoutProgram(
- program: WorkoutProgram,
- isPremium: boolean,
-): boolean {
- if (isPremium) return true
- // Handle boolean, string ("true"), and truthy values from Supabase/cache
- return program.isFree === true
-}
-
-// ─── Legacy Workout Access (kept for backward compatibility) ──
-
-/** Workout IDs available without a subscription (legacy) */
-export const FREE_WORKOUT_IDS: readonly string[] = [
- '1', // Full Body Ignite — Beginner, 4 min (full-body)
- '11', // Core Crusher — Intermediate, 4 min (core)
- '43', // Dance Cardio — Beginner, 4 min (cardio)
-] as const
-
-/** Number of free workouts (for display in paywall copy) */
-export const FREE_WORKOUT_COUNT = FREE_WORKOUT_IDS.length
-
-/**
- * Check if a specific workout is part of the free tier (legacy)
- */
-export function isFreeWorkout(workoutId: string): boolean {
- return FREE_WORKOUT_IDS.includes(workoutId)
-}
-
-/**
- * Check if user can access a workout (legacy + tabata)
- * Premium users can access everything; free users get free tier only
- */
-export function canAccessWorkout(workoutId: string, isPremium: boolean): boolean {
- if (isPremium) return true
- // Check tabata session access
- if (canAccessSession(workoutId, false)) return true
- // Check legacy workout access
- return isFreeWorkout(workoutId)
-}
diff --git a/src/shared/services/analytics.ts b/src/shared/services/analytics.ts
deleted file mode 100644
index 907a699..0000000
--- a/src/shared/services/analytics.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-/**
- * TabataFit PostHog Analytics Service
- * Initialize and configure PostHog for user analytics and session replay
- *
- * Follows the same pattern as purchases.ts:
- * - initializeAnalytics() called once at app startup
- * - track() helper for type-safe event tracking
- * - identifyUser() for user identification
- * - Session replay enabled for onboarding funnel analysis
- */
-
-import { logger } from '../utils/logger'
-import PostHog, { PostHogOptions } from 'posthog-react-native'
-
-type EventProperties = Record
-
-// PostHog configuration
-const POSTHOG_API_KEY = 'phc_9MuXpbtF6LfPycCAzI4xEPhggwwZyGiy9htW0jJ0LTi'
-const POSTHOG_HOST = 'https://eu.i.posthog.com'
-
-// Singleton client instance
-let posthogClient: PostHog | null = null
-
-/**
- * Initialize PostHog SDK with session replay
- * Call this once at app startup (after store hydration)
- */
-export async function initializeAnalytics(): Promise {
- if (posthogClient) {
- logger.log('[Analytics] Already initialized')
- return posthogClient
- }
-
- // Skip initialization if no real API key is configured
- if (POSTHOG_API_KEY.startsWith('__')) {
- if (__DEV__) {
- logger.log('[Analytics] No API key configured — events will be logged to console only')
- }
- return null
- }
-
- try {
- const config: PostHogOptions = {
- host: POSTHOG_HOST,
- enableSessionReplay: true,
- sessionReplayConfig: {
- maskAllTextInputs: true,
- captureNetworkTelemetry: true,
- },
- flushAt: 10,
- }
-
- posthogClient = new PostHog(POSTHOG_API_KEY, config)
-
- logger.log('[Analytics] PostHog initialized with session replay')
- return posthogClient
- } catch (error) {
- logger.error('[Analytics] Failed to initialize PostHog:', error)
- return null
- }
-}
-
-/**
- * Get the PostHog client instance (for PostHogProvider)
- */
-export function getPostHogClient(): PostHog | null {
- return posthogClient
-}
-
-/**
- * Track an analytics event
- * Safe to call even if PostHog is not initialized — logs to console in dev
- */
-export function track(event: string, properties?: EventProperties): void {
- if (__DEV__) {
- logger.log(`[Analytics] ${event}`, properties ?? '')
- }
- posthogClient?.capture(event, properties)
-}
-
-/**
- * Track a screen view
- */
-export function trackScreen(screenName: string, properties?: EventProperties): void {
- track('$screen', { $screen_name: screenName, ...properties })
-}
-
-/**
- * Identify a user with traits
- * Call after onboarding completion to link session replays to user
- */
-export function identifyUser(
- userId: string,
- traits?: EventProperties,
-): void {
- if (__DEV__) {
- logger.log('[Analytics] identify', userId, traits ?? '')
- }
- posthogClient?.identify(userId, traits)
-}
-
-/**
- * Set user properties (without identifying)
- */
-export function setUserProperties(properties: EventProperties): void {
- if (__DEV__) {
- logger.log('[Analytics] set user properties', properties)
- }
- posthogClient?.setPersonProperties(properties)
-}
-
-/**
- * Start a new session replay recording
- * Useful for key moments like onboarding start
- */
-export function startSessionRecording(): void {
- if (__DEV__) {
- logger.log('[Analytics] start session recording')
- }
- posthogClient?.startSessionRecording()
-}
-
-/**
- * Stop session replay recording
- */
-export function stopSessionRecording(): void {
- if (__DEV__) {
- logger.log('[Analytics] stop session recording')
- }
- posthogClient?.stopSessionRecording()
-}
-
-/**
- * Get the current session replay status
- * Note: Session replay URL is available via PostHog dashboard
- */
-export async function isSessionReplayActive(): Promise {
- return posthogClient?.isSessionReplayActive() ?? false
-}
diff --git a/src/shared/services/music.ts b/src/shared/services/music.ts
deleted file mode 100644
index e031c27..0000000
--- a/src/shared/services/music.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { supabase, isSupabaseConfigured } from '../supabase'
-import { logger } from '../utils/logger'
-import type { MusicVibe } from '../types'
-
-export interface MusicTrack {
- id: string
- title: string
- artist: string
- duration: number
- url: string
- vibe: MusicVibe
-}
-
-// Public-domain / royalty-free test audio from Apple HLS examples & samples
-// Replace with Supabase-hosted tracks in production
-const TEST_AUDIO_BASE = 'https://www2.cs.uic.edu/~i101/SoundFiles'
-
-const MOCK_TRACKS: Record = {
- electronic: [
- { id: '1', title: 'Energy Pulse', artist: 'Neon Dreams', duration: 240, url: `${TEST_AUDIO_BASE}/StarWars60.wav`, vibe: 'electronic' },
- { id: '2', title: 'Cyber Sprint', artist: 'Digital Flux', duration: 180, url: `${TEST_AUDIO_BASE}/tapioca.wav`, vibe: 'electronic' },
- { id: '3', title: 'High Voltage', artist: 'Circuit Breakers', duration: 200, url: `${TEST_AUDIO_BASE}/preamble10.wav`, vibe: 'electronic' },
- ],
- 'hip-hop': [
- { id: '4', title: 'Street Heat', artist: 'Urban Flow', duration: 210, url: `${TEST_AUDIO_BASE}/StarWars60.wav`, vibe: 'hip-hop' },
- { id: '5', title: 'Rhythm Power', artist: 'Beat Masters', duration: 195, url: `${TEST_AUDIO_BASE}/tapioca.wav`, vibe: 'hip-hop' },
- { id: '6', title: 'Flow State', artist: 'MC Dynamic', duration: 220, url: `${TEST_AUDIO_BASE}/preamble10.wav`, vibe: 'hip-hop' },
- ],
- pop: [
- { id: '7', title: 'Summer Energy', artist: 'The Popstars', duration: 185, url: `${TEST_AUDIO_BASE}/StarWars60.wav`, vibe: 'pop' },
- { id: '8', title: 'Upbeat Vibes', artist: 'Chart Toppers', duration: 200, url: `${TEST_AUDIO_BASE}/tapioca.wav`, vibe: 'pop' },
- { id: '9', title: 'Feel Good', artist: 'Radio Hits', duration: 175, url: `${TEST_AUDIO_BASE}/preamble10.wav`, vibe: 'pop' },
- ],
- rock: [
- { id: '10', title: 'Power Chord', artist: 'The Amplifiers', duration: 230, url: `${TEST_AUDIO_BASE}/StarWars60.wav`, vibe: 'rock' },
- { id: '11', title: 'High Gain', artist: 'Distortion', duration: 205, url: `${TEST_AUDIO_BASE}/tapioca.wav`, vibe: 'rock' },
- { id: '12', title: 'Adrenaline', artist: 'Thunderstruck', duration: 215, url: `${TEST_AUDIO_BASE}/preamble10.wav`, vibe: 'rock' },
- ],
- chill: [
- { id: '13', title: 'Smooth Flow', artist: 'Lo-Fi Beats', duration: 250, url: `${TEST_AUDIO_BASE}/StarWars60.wav`, vibe: 'chill' },
- { id: '14', title: 'Zen Mode', artist: 'Calm Collective', duration: 240, url: `${TEST_AUDIO_BASE}/tapioca.wav`, vibe: 'chill' },
- { id: '15', title: 'Deep Breath', artist: 'Mindful Tones', duration: 260, url: `${TEST_AUDIO_BASE}/preamble10.wav`, vibe: 'chill' },
- ],
-}
-
-/**
- * Maps download_items genres to workout MusicVibe values.
- * Multiple genres map to the same vibe so we get more tracks per vibe.
- */
-const VIBE_TO_GENRES: Record = {
- electronic: ['edm', 'house', 'drum-and-bass', 'dubstep'],
- 'hip-hop': ['hip-hop', 'r-and-b'],
- pop: ['pop', 'latin'],
- rock: ['rock', 'metal', 'country'],
- chill: ['ambient'],
-}
-
-const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL || 'http://localhost:54321'
-
-class MusicService {
- private cache: Map = new Map()
-
- async loadTracksForVibe(vibe: MusicVibe): Promise {
- if (this.cache.has(vibe)) {
- return this.cache.get(vibe)!
- }
-
- if (!isSupabaseConfigured()) {
- logger.log(`[Music] Using mock tracks for vibe: ${vibe}`)
- return MOCK_TRACKS[vibe] || MOCK_TRACKS.electronic
- }
-
- try {
- const genres = VIBE_TO_GENRES[vibe] || []
-
- type DownloadItemRow = {
- id: string | null
- video_id: string | null
- title: string | null
- duration_seconds: number | null
- public_url: string | null
- storage_path: string | null
- genre: string | null
- }
-
- const { data: items, error } = await supabase
- .from('download_items')
- .select('id, video_id, title, duration_seconds, public_url, storage_path, genre')
- .eq('status', 'completed')
- .in('genre', genres)
- .limit(50) as { data: DownloadItemRow[] | null; error: Error | null }
-
- if (error) {
- logger.error('[Music] Error loading tracks:', error)
- return MOCK_TRACKS[vibe] || MOCK_TRACKS.electronic
- }
-
- if (!items || items.length === 0) {
- logger.log(`[Music] No tracks found for vibe: ${vibe}, using mock data`)
- return MOCK_TRACKS[vibe] || MOCK_TRACKS.electronic
- }
-
- const tracks: MusicTrack[] = items
- .filter(item => item.public_url || item.storage_path)
- .map((item, index) => {
- const url = item.public_url
- || `${SUPABASE_URL}/storage/v1/object/public/workout-audio/${item.storage_path}`
-
- const titleStr = item.title || 'Unknown Track'
- const [artist, trackTitle] = titleStr.includes(' - ')
- ? titleStr.split(' - ').map((s: string) => s.trim())
- : ['YouTube Music', titleStr]
-
- return {
- id: item.id || `${vibe}-${index}`,
- title: trackTitle || titleStr,
- artist: artist || 'Unknown Artist',
- duration: item.duration_seconds ?? 180,
- url,
- vibe,
- }
- })
-
- if (tracks.length === 0) {
- logger.log(`[Music] No valid tracks for vibe: ${vibe}, using mock data`)
- return MOCK_TRACKS[vibe] || MOCK_TRACKS.electronic
- }
-
- this.cache.set(vibe, tracks)
- logger.log(`[Music] Loaded ${tracks.length} tracks for vibe: ${vibe}`)
-
- return tracks
- } catch (error) {
- logger.error('[Music] Error loading tracks:', error)
- return MOCK_TRACKS[vibe] || MOCK_TRACKS.electronic
- }
- }
-
- clearCache(vibe?: MusicVibe): void {
- if (vibe) {
- this.cache.delete(vibe)
- } else {
- this.cache.clear()
- }
- }
-
- getRandomTrack(tracks: MusicTrack[]): MusicTrack | null {
- if (tracks.length === 0) return null
- const randomIndex = Math.floor(Math.random() * tracks.length)
- return tracks[randomIndex]
- }
-
- /**
- * Returns N distinct random tracks for a given vibe.
- * Used to pick one track per tabata block (3 blocks = 3 tracks).
- * Falls back to repeating tracks if fewer than N are available.
- */
- async getRandomTracksForStyle(vibe: MusicVibe, count: number = 3): Promise {
- const pool = await this.loadTracksForVibe(vibe)
- if (pool.length === 0) return []
- if (pool.length <= count) {
- // Not enough distinct tracks — repeat with shuffle
- const shuffled = [...pool].sort(() => Math.random() - 0.5)
- const picked: MusicTrack[] = []
- for (let i = 0; i < count; i++) picked.push(shuffled[i % shuffled.length])
- return picked
- }
- // Fisher-Yates partial shuffle
- const arr = [...pool]
- for (let i = arr.length - 1; i > arr.length - 1 - count; i--) {
- const j = Math.floor(Math.random() * (i + 1))
- ;[arr[i], arr[j]] = [arr[j], arr[i]]
- }
- return arr.slice(-count)
- }
-
- getNextTrack(tracks: MusicTrack[], currentTrackId: string, shuffle: boolean = false): MusicTrack | null {
- if (tracks.length === 0) return null
- if (tracks.length === 1) return tracks[0]
-
- if (shuffle) {
- return this.getRandomTrack(tracks.filter(t => t.id !== currentTrackId))
- }
-
- const currentIndex = tracks.findIndex(t => t.id === currentTrackId)
- const nextIndex = (currentIndex + 1) % tracks.length
- return tracks[nextIndex]
- }
-}
-
-export const musicService = new MusicService()
diff --git a/src/shared/services/purchases.ts b/src/shared/services/purchases.ts
deleted file mode 100644
index 2a9a174..0000000
--- a/src/shared/services/purchases.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * TabataFit RevenueCat Service
- * Initialize and configure RevenueCat for Apple subscriptions
- *
- * Sandbox testing:
- * - The test_ API key enables RevenueCat sandbox mode
- * - StoreKit Configuration (TabataFit.storekit) enables simulator purchases
- * - Transactions are free sandbox completions — no real charges
- */
-
-import { logger } from '../utils/logger'
-import Purchases, { LOG_LEVEL } from 'react-native-purchases'
-
-// RevenueCat configuration
-// test_ prefix = sandbox mode (free transactions on simulator + device sandbox accounts)
-// Set EXPO_PUBLIC_REVENUECAT_API_KEY in .env for production
-export const REVENUECAT_API_KEY =
- process.env.EXPO_PUBLIC_REVENUECAT_API_KEY || 'test_oIJbIHWISJaUZdgxRMHlwizBHvM'
-
-// Entitlement ID configured in RevenueCat dashboard
-export const ENTITLEMENT_ID = '1000 Corp Pro'
-
-// Track initialization state
-let isInitialized = false
-
-/**
- * Initialize RevenueCat SDK
- * Call this once at app startup (after store hydration)
- */
-export async function initializePurchases(): Promise {
- if (isInitialized) {
- logger.log('[Purchases] Already initialized')
- return
- }
-
- try {
- // setLogLevel MUST be called before configure()
- if (__DEV__) {
- Purchases.setLogLevel(LOG_LEVEL.VERBOSE)
- }
-
- // Configure RevenueCat with API key
- await Purchases.configure({ apiKey: REVENUECAT_API_KEY })
-
- isInitialized = true
- logger.log('[Purchases] RevenueCat initialized successfully')
- logger.log('[Purchases] Sandbox mode:', __DEV__ ? 'enabled' : 'disabled')
- } catch (error) {
- logger.error('[Purchases] Failed to initialize RevenueCat:', error)
- throw error
- }
-}
-
-/**
- * Check if RevenueCat has been initialized
- */
-export function isPurchasesInitialized(): boolean {
- return isInitialized
-}
diff --git a/src/shared/services/sync.ts b/src/shared/services/sync.ts
deleted file mode 100644
index 622063f..0000000
--- a/src/shared/services/sync.ts
+++ /dev/null
@@ -1,220 +0,0 @@
-/**
- * Sync Service for Anonymous Auth & Data Sync
- * Handles opt-in personalization for premium users
- */
-
-import { logger } from '../utils/logger'
-
-import { supabase } from '@/src/shared/supabase'
-import type {
- UserProfileData,
- WorkoutSessionData,
- SyncState,
- EnableSyncResult,
- SyncWorkoutResult,
- DeleteDataResult,
-} from '@/src/shared/types'
-
-/**
- * Enable sync by creating anonymous user and syncing all data
- * Called when user accepts sync after first workout
- */
-export async function enableSync(
- profileData: UserProfileData,
- workoutHistory: WorkoutSessionData[]
-): Promise {
- try {
- // 1. Create anonymous user
- const { data: authData, error: authError } =
- await supabase.auth.signInAnonymously()
- if (authError) throw authError
- if (!authData.user) throw new Error('Failed to create anonymous user')
-
- const userId = authData.user.id
-
- // 2. Create user profile in Supabase
- const { error: profileError } = await (supabase as any)
- .from('user_profiles')
- .insert({
- id: userId,
- name: profileData.name,
- fitness_level: profileData.fitnessLevel,
- goal: profileData.goal,
- weekly_frequency: profileData.weeklyFrequency,
- barriers: profileData.barriers,
- is_anonymous: true,
- sync_enabled: true,
- subscription_plan: 'premium-yearly',
- onboarding_completed_at: profileData.onboardingCompletedAt,
- })
- if (profileError) throw profileError
-
- // 3. Sync all workout history retroactively
- if (workoutHistory.length > 0) {
- const { error: historyError } = await (supabase as any)
- .from('workout_sessions')
- .insert(
- workoutHistory.map((session) => ({
- user_id: userId,
- workout_id: session.workoutId,
- completed_at: session.completedAt,
- duration_seconds: session.durationSeconds,
- calories_burned: session.caloriesBurned,
- feeling_rating: session.feelingRating,
- }))
- )
- if (historyError) throw historyError
- }
-
- // 4. Initialize default preferences
- const { error: prefError } = await (supabase as any)
- .from('user_preferences')
- .insert({
- user_id: userId,
- preferred_categories: [],
- preferred_trainers: [],
- preferred_durations: [],
- difficulty_preference: 'matched',
- })
- if (prefError) throw prefError
-
- return { success: true, userId }
- } catch (error) {
- logger.error('Failed to enable sync:', error)
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error',
- }
- }
-}
-
-/**
- * Sync a single workout session (called after each workout completion)
- */
-export async function syncWorkoutSession(
- session: WorkoutSessionData
-): Promise {
- try {
- const {
- data: { user },
- } = await supabase.auth.getUser()
- if (!user) return { success: false, error: 'No authenticated user' }
-
- const { error } = await (supabase.from('workout_sessions') as any).insert({
- user_id: user.id,
- workout_id: session.workoutId,
- completed_at: session.completedAt,
- duration_seconds: session.durationSeconds,
- calories_burned: session.caloriesBurned,
- feeling_rating: session.feelingRating,
- })
-
- if (error) throw error
- return { success: true }
- } catch (error) {
- logger.error('Failed to sync workout:', error)
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error',
- }
- }
-}
-
-/**
- * Delete all synced data from Supabase (keep local)
- * User becomes "unsynced" - can opt-in again later
- */
-export async function deleteSyncedData(): Promise {
- try {
- const {
- data: { user },
- } = await supabase.auth.getUser()
- if (!user) return { success: false, error: 'No authenticated user' }
-
- // Note: Deleting the user will cascade delete all related data
- // due to ON DELETE CASCADE in the schema
- // However, supabase.auth.admin.deleteUser requires admin privileges
- // Instead, we'll delete from our tables manually
-
- // Delete user's preferences
- const { error: prefError } = await supabase
- .from('user_preferences')
- .delete()
- .eq('user_id', user.id)
- if (prefError) throw prefError
-
- // Delete user's workout sessions
- const { error: sessionsError } = await supabase
- .from('workout_sessions')
- .delete()
- .eq('user_id', user.id)
- if (sessionsError) throw sessionsError
-
- // Delete user's profile
- const { error: profileError } = await supabase
- .from('user_profiles')
- .delete()
- .eq('id', user.id)
- if (profileError) throw profileError
-
- // Sign out locally
- await supabase.auth.signOut()
-
- return { success: true }
- } catch (error) {
- logger.error('Failed to delete synced data:', error)
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error',
- }
- }
-}
-
-/**
- * Get current sync state
- */
-export async function getSyncState(): Promise {
- const { data: session } = await supabase.auth.getSession()
-
- if (!session.session?.user) {
- return {
- status: 'never-synced',
- userId: null,
- lastSyncAt: null,
- pendingWorkouts: 0,
- }
- }
-
- const userId = session.session.user.id
-
- // Get latest workout session to determine last sync
- const { data: latestSession } = await supabase
- .from('workout_sessions')
- .select('created_at')
- .eq('user_id', userId)
- .order('created_at', { ascending: false })
- .limit(1)
-
- return {
- status: 'synced',
- userId: userId,
- lastSyncAt: (latestSession as any)?.[0]?.created_at || null,
- pendingWorkouts: 0,
- }
-}
-
-/**
- * Check if user has synced data (for determining if we can show personalized features)
- */
-export async function hasSyncedData(): Promise {
- const { data: session } = await supabase.auth.getSession()
- return !!session.session?.user
-}
-
-/**
- * Check if user is currently authenticated with Supabase
- */
-export async function isAuthenticated(): Promise {
- const { data: session } = await supabase.auth.getSession()
- return !!session.session?.user
-}
diff --git a/src/shared/stores/CLAUDE.md b/src/shared/stores/CLAUDE.md
deleted file mode 100644
index fdbbf35..0000000
--- a/src/shared/stores/CLAUDE.md
+++ /dev/null
@@ -1,27 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 10, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6034 | 10:10 AM | 🔵 | Kine Program Store Re-Read for Progress Tracking Reference | ~376 |
-| #6026 | 10:08 AM | 🔵 | Kine Program Store with Progress Tracking and Sequential Unlocking | ~566 |
-
-### Apr 13, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6175 | 10:04 PM | 🟣 | Completed Explore Tab Removal | ~196 |
-| #6172 | 10:03 PM | ✅ | Removed Explore Filter Store Export | ~123 |
-| #6171 | " | 🔵 | Read Stores Barrel Export File | ~127 |
-| #6161 | 10:02 PM | 🔵 | Discovered Explore Filter Store | ~135 |
-
-### Apr 17, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6383 | 10:35 AM | 🔵 | User Store Subscription State Management | ~172 |
-
\ No newline at end of file
diff --git a/src/shared/stores/index.ts b/src/shared/stores/index.ts
deleted file mode 100644
index 0dde417..0000000
--- a/src/shared/stores/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * TabataFit Stores
- */
-
-export { useUserStore } from './userStore'
-export { usePlayerStore } from './playerStore'
-export { useWorkoutProgramStore } from './workoutProgramStore'
-export { useProgressStore } from './progressStore'
diff --git a/src/shared/stores/playerStore.ts b/src/shared/stores/playerStore.ts
deleted file mode 100644
index c81dfe2..0000000
--- a/src/shared/stores/playerStore.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * TabataFit Player Store
- * Current workout state — ephemeral (not persisted)
- */
-
-import { create } from 'zustand'
-import type { Workout } from '../types'
-
-type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
-
-interface PlayerState {
- workout: Workout | null
- phase: TimerPhase
- timeRemaining: number
- currentRound: number
- isPaused: boolean
- isRunning: boolean
- calories: number
- startedAt: number | null
-
- // Actions
- loadWorkout: (workout: Workout) => void
- setPhase: (phase: TimerPhase) => void
- setTimeRemaining: (time: number) => void
- setCurrentRound: (round: number) => void
- setPaused: (paused: boolean) => void
- setRunning: (running: boolean) => void
- addCalories: (amount: number) => void
- reset: () => void
-}
-
-export const usePlayerStore = create((set) => ({
- workout: null,
- phase: 'PREP',
- timeRemaining: 10,
- currentRound: 1,
- isPaused: false,
- isRunning: false,
- calories: 0,
- startedAt: null,
-
- loadWorkout: (workout) =>
- set({
- workout,
- phase: 'PREP',
- timeRemaining: workout.prepTime,
- currentRound: 1,
- isPaused: false,
- isRunning: false,
- calories: 0,
- startedAt: null,
- }),
-
- setPhase: (phase) => set({ phase }),
- setTimeRemaining: (time) => set({ timeRemaining: time }),
- setCurrentRound: (round) => set({ currentRound: round }),
- setPaused: (paused) => set({ isPaused: paused }),
- setRunning: (running) =>
- set((state) => ({
- isRunning: running,
- startedAt: running && !state.startedAt ? Date.now() : state.startedAt,
- })),
- addCalories: (amount) => set((state) => ({ calories: state.calories + amount })),
- reset: () =>
- set({
- workout: null,
- phase: 'PREP',
- timeRemaining: 10,
- currentRound: 1,
- isPaused: false,
- isRunning: false,
- calories: 0,
- startedAt: null,
- }),
-}))
diff --git a/src/shared/stores/progressStore.ts b/src/shared/stores/progressStore.ts
deleted file mode 100644
index aa2ace6..0000000
--- a/src/shared/stores/progressStore.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-/**
- * Progress Store — Simplified
- *
- * Unique source de vérité pour le progrès utilisateur :
- * - Streak (current + longest, basé sur jours uniques)
- * - History (capped à 30 dernières séances, pour perf mobile)
- * - Completed programs (set d'IDs)
- * - Weekly count (séances cette semaine)
- *
- * Remplace activityStore + workoutProgramStore.
- */
-
-import { create } from 'zustand'
-import { persist, createJSONStorage } from 'zustand/middleware'
-import AsyncStorage from '@react-native-async-storage/async-storage'
-import { useUserStore } from './userStore'
-import { syncWorkoutSession } from '@/src/shared/services/sync'
-
-// ─── Types ──────────────────────────────────────────────────────
-
-export interface ProgramSession {
- /** Program ID (e.g. "wp-upper-beginner-01") */
- programId: string
- /** Epoch ms */
- completedAt: number
- /** Total duration in seconds */
- durationSeconds: number
- /** Body zone: "upper" | "lower" | "full" */
- bodyZone: string
- /** Level: "Beginner" | "Intermediate" | "Advanced" */
- level: string
-}
-
-interface ProgressState {
- history: ProgramSession[]
- completedProgramIds: string[]
- streak: { current: number; longest: number }
-
- // Actions
- completeProgram: (session: ProgramSession) => Promise
- resetProgress: () => void
-
- // Getters
- isProgramCompleted: (programId: string) => boolean
- getCompletedCount: () => number
- getWeeklyCount: () => number
- getStreak: () => { current: number; longest: number }
-}
-
-// ─── Helpers ────────────────────────────────────────────────────
-
-const HISTORY_CAP = 30
-const MS_PER_DAY = 86_400_000
-
-function toDateKey(timestamp: number): string {
- return new Date(timestamp).toISOString().split('T')[0]
-}
-
-function computeStreak(history: ProgramSession[]): { current: number; longest: number } {
- if (history.length === 0) return { current: 0, longest: 0 }
-
- const uniqueDays = Array.from(new Set(history.map((s) => toDateKey(s.completedAt)))).sort().reverse()
- const today = toDateKey(Date.now())
- const yesterday = toDateKey(Date.now() - MS_PER_DAY)
- const isActive = uniqueDays[0] === today || uniqueDays[0] === yesterday
-
- const longest = computeLongest(uniqueDays)
- if (!isActive) return { current: 0, longest }
-
- let current = 1
- for (let i = 0; i < uniqueDays.length - 1; i++) {
- const diff = new Date(uniqueDays[i]).getTime() - new Date(uniqueDays[i + 1]).getTime()
- if (diff <= MS_PER_DAY) current++
- else break
- }
- return { current, longest: Math.max(current, longest) }
-}
-
-function computeLongest(sortedDays: string[]): number {
- if (sortedDays.length === 0) return 0
- let longest = 1
- let run = 1
- for (let i = 0; i < sortedDays.length - 1; i++) {
- const diff = new Date(sortedDays[i]).getTime() - new Date(sortedDays[i + 1]).getTime()
- if (diff <= MS_PER_DAY) {
- run++
- longest = Math.max(longest, run)
- } else {
- run = 1
- }
- }
- return longest
-}
-
-function countThisWeek(history: ProgramSession[]): number {
- const now = new Date()
- const startOfWeek = new Date(now)
- startOfWeek.setDate(now.getDate() - now.getDay())
- startOfWeek.setHours(0, 0, 0, 0)
- const cutoff = startOfWeek.getTime()
- return history.filter((s) => s.completedAt >= cutoff).length
-}
-
-// ─── Store ──────────────────────────────────────────────────────
-
-export const useProgressStore = create()(
- persist(
- (set, get) => ({
- history: [],
- completedProgramIds: [],
- streak: { current: 0, longest: 0 },
-
- completeProgram: async (session) => {
- const existing = get()
- const newHistory = [session, ...existing.history].slice(0, HISTORY_CAP)
- const newCompleted = existing.completedProgramIds.includes(session.programId)
- ? existing.completedProgramIds
- : [...existing.completedProgramIds, session.programId]
- const newStreak = computeStreak(newHistory)
-
- set({
- history: newHistory,
- completedProgramIds: newCompleted,
- streak: newStreak,
- })
-
- // Sync to Supabase if premium + synced
- const userStore = useUserStore.getState()
- const isPremium = userStore.profile.subscription !== 'free'
- if (userStore.profile.syncStatus === 'synced') {
- await syncWorkoutSession({
- workoutId: session.programId,
- completedAt: new Date(session.completedAt).toISOString(),
- durationSeconds: session.durationSeconds,
- caloriesBurned: 0,
- feelingRating: undefined,
- })
- } else if (userStore.profile.syncStatus === 'never-synced' && isPremium) {
- userStore.setPromptPending()
- }
- },
-
- resetProgress: () => {
- set({ history: [], completedProgramIds: [], streak: { current: 0, longest: 0 } })
- },
-
- isProgramCompleted: (programId) => get().completedProgramIds.includes(programId),
- getCompletedCount: () => get().completedProgramIds.length,
- getWeeklyCount: () => countThisWeek(get().history),
- getStreak: () => get().streak,
- }),
- {
- name: 'tabatago-progress',
- storage: createJSONStorage(() => AsyncStorage),
- version: 1,
- },
- ),
-)
diff --git a/src/shared/stores/userStore.ts b/src/shared/stores/userStore.ts
deleted file mode 100644
index 44050d1..0000000
--- a/src/shared/stores/userStore.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * TabataFit User Store
- * Profile, settings, subscription — persisted via AsyncStorage
- * Added: Sync status for anonymous auth opt-in
- */
-
-import { create } from 'zustand'
-import { persist, createJSONStorage } from 'zustand/middleware'
-import AsyncStorage from '@react-native-async-storage/async-storage'
-import { getLocales } from 'expo-localization'
-import type {
- UserProfile,
- UserSettings,
- SubscriptionPlan,
- FitnessLevel,
- FitnessGoal,
- WeeklyFrequency,
- SyncStatus,
-} from '../types'
-
-interface OnboardingData {
- name: string
- fitnessLevel: FitnessLevel
- goal: FitnessGoal
- weeklyFrequency: WeeklyFrequency
- barriers: string[]
-}
-
-interface UserState {
- profile: UserProfile
- settings: UserSettings
- savedWorkouts: string[]
- // Actions
- updateProfile: (updates: Partial) => void
- updateSettings: (updates: Partial) => void
- setSubscription: (plan: SubscriptionPlan) => void
- completeOnboarding: (data: OnboardingData) => void
- toggleSavedWorkout: (workoutId: string) => void
- // NEW: Sync-related actions
- setSyncStatus: (status: SyncStatus, userId?: string | null) => void
- setPromptPending: () => void
-}
-
-export const useUserStore = create()(
- persist(
- (set) => ({
- profile: {
- name: '',
- email: '',
- joinDate: new Date().toLocaleDateString(
- getLocales()[0]?.languageTag ?? 'en-US',
- { month: 'long', year: 'numeric' }
- ),
- subscription: 'free',
- onboardingCompleted: false,
- fitnessLevel: 'beginner',
- goal: 'cardio',
- weeklyFrequency: 3,
- barriers: [],
- savedWorkouts: [],
- // NEW: Sync fields
- syncStatus: 'never-synced',
- supabaseUserId: null,
- },
- settings: {
- haptics: true,
- soundEffects: true,
- voiceCoaching: true,
- musicEnabled: true,
- musicVolume: 0.5,
- reminders: false,
- reminderTime: '09:00',
- hasPromptedReview: false,
- },
-
- savedWorkouts: [],
-
- updateProfile: (updates) =>
- set((state) => ({
- profile: { ...state.profile, ...updates },
- })),
-
- updateSettings: (updates) =>
- set((state) => ({
- settings: { ...state.settings, ...updates },
- })),
-
- setSubscription: (plan) =>
- set((state) => ({
- profile: { ...state.profile, subscription: plan },
- })),
-
- completeOnboarding: (data) =>
- set((state) => ({
- profile: {
- ...state.profile,
- name: data.name,
- fitnessLevel: data.fitnessLevel,
- goal: data.goal,
- weeklyFrequency: data.weeklyFrequency,
- barriers: data.barriers,
- onboardingCompleted: true,
- },
- })),
-
- toggleSavedWorkout: (workoutId) =>
- set((state) => ({
- savedWorkouts: state.savedWorkouts.includes(workoutId)
- ? state.savedWorkouts.filter((id) => id !== workoutId)
- : [...state.savedWorkouts, workoutId],
- })),
-
- // NEW: Sync status management
- setSyncStatus: (status, userId = null) =>
- set((state) => ({
- profile: {
- ...state.profile,
- syncStatus: status,
- supabaseUserId: userId,
- },
- })),
-
- // NEW: Mark that we should show sync prompt after first workout
- setPromptPending: () =>
- set((state) => ({
- profile: {
- ...state.profile,
- syncStatus: 'prompt-pending',
- },
- })),
- }),
- {
- name: 'tabatafit-user',
- storage: createJSONStorage(() => AsyncStorage),
- }
- )
-)
diff --git a/src/shared/stores/workoutProgramStore.ts b/src/shared/stores/workoutProgramStore.ts
deleted file mode 100644
index 2d4e398..0000000
--- a/src/shared/stores/workoutProgramStore.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/**
- * Workout Program Store
- * Tracks completion of body-zone workout programs
- */
-
-import { create } from 'zustand'
-import { persist } from 'zustand/middleware'
-import AsyncStorage from '@react-native-async-storage/async-storage'
-import type { WorkoutProgram } from '../types/workoutProgram'
-
-// ─── Types ──────────────────────────────────────────────────────
-
-interface ProgramCompletion {
- completedAt: string
- tabatasCompleted: number[]
-}
-
-interface WorkoutProgramState {
- // Completion tracking
- completions: Record
-
- // Actions
- completeProgram: (programId: string, tabataPosition?: number) => void
- resetProgram: (programId: string) => void
-
- // Getters
- isProgramCompleted: (programId: string) => boolean
- getCompletedCount: () => number
- getRecommendedNext: (programs: WorkoutProgram[]) => WorkoutProgram | null
- getTabatasCompleted: (programId: string) => number[]
-}
-
-// ─── Store ──────────────────────────────────────────────────────
-
-export const useWorkoutProgramStore = create()(
- persist(
- (set, get) => ({
- completions: {},
-
- completeProgram: (programId, tabataPosition) => {
- set(state => {
- const existing = state.completions[programId]
- if (tabataPosition !== undefined) {
- // Mark specific tabata as completed
- const tabatasCompleted = existing
- ? [...existing.tabatasCompleted.filter(t => t !== tabataPosition), tabataPosition]
- : [tabataPosition]
- const isDone = tabatasCompleted.length >= 3
- return {
- completions: {
- ...state.completions,
- [programId]: {
- completedAt: isDone ? new Date().toISOString() : existing?.completedAt ?? '',
- tabatasCompleted,
- },
- },
- }
- }
- // Mark entire program as completed
- return {
- completions: {
- ...state.completions,
- [programId]: {
- completedAt: new Date().toISOString(),
- tabatasCompleted: [1, 2, 3],
- },
- },
- }
- })
- },
-
- resetProgram: (programId) => {
- set(state => {
- const { [programId]: _, ...rest } = state.completions
- return { completions: rest }
- })
- },
-
- isProgramCompleted: (programId) => {
- const completion = get().completions[programId]
- return !!completion && completion.tabatasCompleted.length >= 3
- },
-
- getCompletedCount: () => {
- return Object.values(get().completions).filter(
- c => c.tabatasCompleted.length >= 3,
- ).length
- },
-
- getRecommendedNext: (programs) => {
- const { completions } = get()
- // Find first incomplete program, prioritizing Beginner → Intermediate → Advanced
- const levelOrder = { Beginner: 0, Intermediate: 1, Advanced: 2 }
- const sorted = [...programs].sort(
- (a, b) => levelOrder[a.level] - levelOrder[b.level],
- )
- return (
- sorted.find(p => {
- const c = completions[p.id]
- return !c || c.tabatasCompleted.length < 3
- }) ?? null
- )
- },
-
- getTabatasCompleted: (programId) => {
- return get().completions[programId]?.tabatasCompleted ?? []
- },
- }),
- {
- name: 'tabatafit-workout-program-storage',
- storage: {
- getItem: async (name) => {
- const value = await AsyncStorage.getItem(name)
- return value ? JSON.parse(value) : null
- },
- setItem: async (name, value) => {
- await AsyncStorage.setItem(name, JSON.stringify(value))
- },
- removeItem: async (name) => {
- await AsyncStorage.removeItem(name)
- },
- },
- },
- ),
-)
diff --git a/src/shared/supabase/CLAUDE.md b/src/shared/supabase/CLAUDE.md
deleted file mode 100644
index bfbb7a3..0000000
--- a/src/shared/supabase/CLAUDE.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 13, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #6267 | 10:51 PM | 🟣 | Database types added to mobile client for YouTube music integration | ~407 |
-
\ No newline at end of file
diff --git a/src/shared/supabase/client.ts b/src/shared/supabase/client.ts
deleted file mode 100644
index cadbf12..0000000
--- a/src/shared/supabase/client.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * TabataFit Supabase Client
- * Initialize Supabase client with environment variables
- */
-
-import { logger } from '../utils/logger'
-
-import { createClient } from '@supabase/supabase-js'
-import type { Database } from './database.types'
-
-const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL
-const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY
-
-if (!supabaseUrl || !supabaseAnonKey) {
- logger.warn(
- 'Supabase credentials not found. Using mock data. ' +
- 'Please set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY in your .env file'
- )
-}
-
-// Create Supabase client with type safety
-export const supabase = createClient(
- supabaseUrl ?? 'http://localhost:54321',
- supabaseAnonKey ?? 'mock-key',
- {
- auth: {
- persistSession: true,
- autoRefreshToken: true,
- detectSessionInUrl: false,
- },
- }
-)
-
-// Check if Supabase is properly configured
-export const isSupabaseConfigured = (): boolean => {
- return !!supabaseUrl && !!supabaseAnonKey &&
- supabaseUrl !== 'http://localhost:54321' &&
- supabaseAnonKey !== 'mock-key'
-}
diff --git a/src/shared/supabase/database.types.ts b/src/shared/supabase/database.types.ts
deleted file mode 100644
index fabe489..0000000
--- a/src/shared/supabase/database.types.ts
+++ /dev/null
@@ -1,514 +0,0 @@
-/**
- * TabataFit Supabase Database Types
- * Generated types for the Supabase schema
- */
-
-export type Json =
- | string
- | number
- | boolean
- | null
- | { [key: string]: Json | undefined }
- | Json[]
-
-export interface Database {
- public: {
- Tables: {
- workouts: {
- Row: {
- id: string
- title: string
- trainer_id: string
- category: 'full-body' | 'core' | 'upper-body' | 'lower-body' | 'cardio'
- level: 'Beginner' | 'Intermediate' | 'Advanced'
- duration: number
- calories: number
- rounds: number
- prep_time: number
- work_time: number
- rest_time: number
- equipment: string[]
- music_vibe: 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
- exercises: {
- name: string
- duration: number
- }[]
- thumbnail_url: string | null
- video_url: string | null
- is_featured: boolean
- created_at: string
- updated_at: string
- }
- Insert: {
- id?: string
- title: string
- trainer_id: string
- category: 'full-body' | 'core' | 'upper-body' | 'lower-body' | 'cardio'
- level: 'Beginner' | 'Intermediate' | 'Advanced'
- duration: number
- calories: number
- rounds: number
- prep_time: number
- work_time: number
- rest_time: number
- equipment?: string[]
- music_vibe: 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
- exercises: {
- name: string
- duration: number
- }[]
- thumbnail_url?: string | null
- video_url?: string | null
- is_featured?: boolean
- created_at?: string
- updated_at?: string
- }
- Update: {
- id?: string
- title?: string
- trainer_id?: string
- category?: 'full-body' | 'core' | 'upper-body' | 'lower-body' | 'cardio'
- level?: 'Beginner' | 'Intermediate' | 'Advanced'
- duration?: number
- calories?: number
- rounds?: number
- prep_time?: number
- work_time?: number
- rest_time?: number
- equipment?: string[]
- music_vibe?: 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
- exercises?: {
- name: string
- duration: number
- }[]
- thumbnail_url?: string | null
- video_url?: string | null
- is_featured?: boolean
- updated_at?: string
- }
- }
- user_profiles: {
- Row: {
- id: string
- name: string
- fitness_level: string
- goal: string
- weekly_frequency: number
- barriers: string[]
- is_anonymous: boolean
- sync_enabled: boolean
- subscription_plan: string
- onboarding_completed_at: string
- created_at: string
- updated_at: string
- }
- Insert: {
- id: string
- name: string
- fitness_level: string
- goal: string
- weekly_frequency: number
- barriers?: string[]
- is_anonymous?: boolean
- sync_enabled?: boolean
- subscription_plan?: string
- onboarding_completed_at: string
- created_at?: string
- updated_at?: string
- }
- Update: {
- id?: string
- name?: string
- fitness_level?: string
- goal?: string
- weekly_frequency?: number
- barriers?: string[]
- is_anonymous?: boolean
- sync_enabled?: boolean
- subscription_plan?: string
- onboarding_completed_at?: string
- updated_at?: string
- }
- }
- user_preferences: {
- Row: {
- user_id: string
- preferred_categories: string[]
- preferred_trainers: string[]
- preferred_durations: string[]
- difficulty_preference: string
- created_at: string
- updated_at: string
- }
- Insert: {
- user_id: string
- preferred_categories?: string[]
- preferred_trainers?: string[]
- preferred_durations?: string[]
- difficulty_preference?: string
- created_at?: string
- updated_at?: string
- }
- Update: {
- preferred_categories?: string[]
- preferred_trainers?: string[]
- preferred_durations?: string[]
- difficulty_preference?: string
- updated_at?: string
- }
- }
- workout_sessions: {
- Row: {
- id: string
- user_id: string
- workout_id: string
- completed_at: string
- duration_seconds: number
- calories_burned: number
- feeling_rating: number | null
- created_at: string
- }
- Insert: {
- id?: string
- user_id: string
- workout_id: string
- completed_at: string
- duration_seconds: number
- calories_burned: number
- feeling_rating?: number | null
- created_at?: string
- }
- Update: {
- workout_id?: string
- completed_at?: string
- duration_seconds?: number
- calories_burned?: number
- feeling_rating?: number | null
- }
- }
- trainers: {
- Row: {
- id: string
- name: string
- specialty: string
- color: string
- avatar_url: string | null
- workout_count: number
- created_at: string
- updated_at: string
- }
- Insert: {
- id?: string
- name: string
- specialty: string
- color: string
- avatar_url?: string | null
- workout_count?: number
- created_at?: string
- updated_at?: string
- }
- Update: {
- id?: string
- name?: string
- specialty?: string
- color?: string
- avatar_url?: string | null
- workout_count?: number
- updated_at?: string
- }
- }
- collections: {
- Row: {
- id: string
- title: string
- description: string
- icon: string
- gradient: string[] | null
- created_at: string
- updated_at: string
- }
- Insert: {
- id?: string
- title: string
- description: string
- icon: string
- gradient?: string[] | null
- created_at?: string
- updated_at?: string
- }
- Update: {
- id?: string
- title?: string
- description?: string
- icon?: string
- gradient?: string[] | null
- updated_at?: string
- }
- }
- collection_workouts: {
- Row: {
- id: string
- collection_id: string
- workout_id: string
- sort_order: number
- created_at: string
- }
- Insert: {
- id?: string
- collection_id: string
- workout_id: string
- sort_order?: number
- created_at?: string
- }
- Update: {
- id?: string
- collection_id?: string
- workout_id?: string
- sort_order?: number
- }
- }
- workout_programs: {
- Row: {
- id: string
- title: string
- description: string | null
- body_zone: 'upper-body' | 'lower-body' | 'full-body'
- level: 'Beginner' | 'Intermediate' | 'Advanced'
- is_free: boolean
- music_vibe: 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
- estimated_duration: number
- estimated_calories: number
- icon: string | null
- accent_color: string | null
- sort_order: number
- created_at: string
- updated_at: string
- }
- Insert: {
- id?: string
- title: string
- description?: string | null
- body_zone: 'upper-body' | 'lower-body' | 'full-body'
- level: 'Beginner' | 'Intermediate' | 'Advanced'
- is_free?: boolean
- music_vibe?: 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
- estimated_duration?: number
- estimated_calories: number
- icon?: string | null
- accent_color?: string | null
- sort_order?: number
- created_at?: string
- updated_at?: string
- }
- Update: {
- id?: string
- title?: string
- description?: string | null
- body_zone?: 'upper-body' | 'lower-body' | 'full-body'
- level?: 'Beginner' | 'Intermediate' | 'Advanced'
- is_free?: boolean
- music_vibe?: 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
- estimated_duration?: number
- estimated_calories?: number
- icon?: string | null
- accent_color?: string | null
- sort_order?: number
- updated_at?: string
- }
- }
- program_tabatas: {
- Row: {
- id: string
- program_id: string
- position: number
- exercise_1_name: string
- exercise_1_name_en: string | null
- exercise_1_tip: string | null
- exercise_1_tip_en: string | null
- exercise_1_modification: string | null
- exercise_1_modification_en: string | null
- exercise_1_progression: string | null
- exercise_1_progression_en: string | null
- exercise_2_name: string
- exercise_2_name_en: string | null
- exercise_2_tip: string | null
- exercise_2_tip_en: string | null
- exercise_2_modification: string | null
- exercise_2_modification_en: string | null
- exercise_2_progression: string | null
- exercise_2_progression_en: string | null
- rounds: number
- work_time: number
- rest_time: number
- created_at: string
- }
- Insert: {
- id?: string
- program_id: string
- position: number
- exercise_1_name: string
- exercise_1_name_en?: string | null
- exercise_1_tip?: string | null
- exercise_1_tip_en?: string | null
- exercise_1_modification?: string | null
- exercise_1_modification_en?: string | null
- exercise_1_progression?: string | null
- exercise_1_progression_en?: string | null
- exercise_2_name: string
- exercise_2_name_en?: string | null
- exercise_2_tip?: string | null
- exercise_2_tip_en?: string | null
- exercise_2_modification?: string | null
- exercise_2_modification_en?: string | null
- exercise_2_progression?: string | null
- exercise_2_progression_en?: string | null
- rounds?: number
- work_time?: number
- rest_time?: number
- created_at?: string
- }
- Update: {
- id?: string
- program_id?: string
- position?: number
- exercise_1_name?: string
- exercise_1_name_en?: string | null
- exercise_1_tip?: string | null
- exercise_1_tip_en?: string | null
- exercise_1_modification?: string | null
- exercise_1_modification_en?: string | null
- exercise_1_progression?: string | null
- exercise_1_progression_en?: string | null
- exercise_2_name?: string
- exercise_2_name_en?: string | null
- exercise_2_tip?: string | null
- exercise_2_tip_en?: string | null
- exercise_2_modification?: string | null
- exercise_2_modification_en?: string | null
- exercise_2_progression?: string | null
- exercise_2_progression_en?: string | null
- rounds?: number
- work_time?: number
- rest_time?: number
- }
- }
- achievements: {
- Row: {
- id: string
- title: string
- description: string
- icon: string
- requirement: number
- type: 'workouts' | 'streak' | 'minutes' | 'calories'
- created_at: string
- updated_at: string
- }
- Insert: {
- id?: string
- title: string
- description: string
- icon: string
- requirement: number
- type: 'workouts' | 'streak' | 'minutes' | 'calories'
- created_at?: string
- updated_at?: string
- }
- Update: {
- id?: string
- title?: string
- description?: string
- icon?: string
- requirement?: number
- type?: 'workouts' | 'streak' | 'minutes' | 'calories'
- updated_at?: string
- }
- }
- admin_users: {
- Row: {
- id: string
- email: string
- role: 'admin' | 'editor'
- created_at: string
- last_login: string | null
- }
- Insert: {
- id?: string
- email: string
- role?: 'admin' | 'editor'
- created_at?: string
- last_login?: string | null
- }
- Update: {
- id?: string
- email?: string
- role?: 'admin' | 'editor'
- last_login?: string | null
- }
- }
- download_jobs: {
- Row: {
- id: string
- playlist_url: string
- playlist_title: string | null
- status: 'pending' | 'processing' | 'completed' | 'failed'
- total_items: number
- completed_items: number
- failed_items: number
- created_by: string
- created_at: string
- updated_at: string
- }
- Insert: {
- id?: string
- playlist_url: string
- playlist_title?: string | null
- status?: 'pending' | 'processing' | 'completed' | 'failed'
- total_items?: number
- completed_items?: number
- failed_items?: number
- created_by: string
- }
- Update: Partial>
- }
- download_items: {
- Row: {
- id: string
- job_id: string
- video_id: string
- title: string | null
- duration_seconds: number | null
- thumbnail_url: string | null
- status: 'pending' | 'downloading' | 'completed' | 'failed'
- storage_path: string | null
- public_url: string | null
- error_message: string | null
- genre: 'edm' | 'hip-hop' | 'pop' | 'rock' | 'latin' | 'house' | 'drum-and-bass' | 'dubstep' | 'r-and-b' | 'country' | 'metal' | 'ambient' | null
- created_at: string
- }
- Insert: {
- id?: string
- job_id: string
- video_id: string
- title?: string | null
- duration_seconds?: number | null
- thumbnail_url?: string | null
- status?: 'pending' | 'downloading' | 'completed' | 'failed'
- storage_path?: string | null
- public_url?: string | null
- error_message?: string | null
- genre?: 'edm' | 'hip-hop' | 'pop' | 'rock' | 'latin' | 'house' | 'drum-and-bass' | 'dubstep' | 'r-and-b' | 'country' | 'metal' | 'ambient' | null
- }
- Update: Partial>
- }
- }
- Views: {
- [_ in never]: never
- }
- Functions: {
- [_ in never]: never
- }
- Enums: {
- [_ in never]: never
- }
- }
-}
diff --git a/src/shared/supabase/index.ts b/src/shared/supabase/index.ts
deleted file mode 100644
index 07c137d..0000000
--- a/src/shared/supabase/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export type { Database } from './database.types'
-export { supabase, isSupabaseConfigured } from './client'
diff --git a/src/shared/theme/ThemeContext.tsx b/src/shared/theme/ThemeContext.tsx
deleted file mode 100644
index dac1603..0000000
--- a/src/shared/theme/ThemeContext.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Theme context + provider
- * Dark mode only — Dark Medical design system
- */
-
-import { createContext, useContext } from 'react'
-import { darkColors } from './colors.dark'
-import type { ThemeColors } from './types'
-
-const ThemeContext = createContext(darkColors)
-
-export function ThemeProvider({ children }: { children: React.ReactNode }) {
- return (
- {children}
- )
-}
-
-export function useThemeColors(): ThemeColors {
- return useContext(ThemeContext)
-}
diff --git a/src/shared/theme/colors.dark.ts b/src/shared/theme/colors.dark.ts
deleted file mode 100644
index 46e78a8..0000000
--- a/src/shared/theme/colors.dark.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Dark Premium theme palette
- * Refined navy backgrounds, green actions, native iOS feel
- */
-
-import type { ThemeColors } from './types'
-import { NAVY, TEXT as TEXT_COLORS, BORDER_COLORS, GREEN, ORANGE } from '../constants/colors'
-
-export const darkColors: ThemeColors = {
- bg: {
- base: NAVY[900],
- surface: NAVY[800],
- elevated: NAVY[700],
- overlay1: 'rgba(150,164,190,0.05)',
- overlay2: 'rgba(150,164,190,0.08)',
- overlay3: 'rgba(150,164,190,0.12)',
- scrim: 'rgba(0,0,0,0.6)',
- },
- text: {
- primary: TEXT_COLORS.PRIMARY,
- secondary: TEXT_COLORS.SECONDARY,
- tertiary: TEXT_COLORS.TERTIARY,
- muted: TEXT_COLORS.MUTED,
- hint: TEXT_COLORS.HINT,
- disabled: TEXT_COLORS.DISABLED,
- },
- surface: {
- default: {
- backgroundColor: NAVY[800],
- borderColor: BORDER_COLORS.DIM,
- borderWidth: 1,
- },
- accent: {
- backgroundColor: GREEN.DIM,
- borderColor: GREEN.BORDER,
- borderWidth: 1.5,
- },
- tip: {
- backgroundColor: ORANGE.DIM,
- borderColor: ORANGE[500],
- borderWidth: 1,
- },
- },
- border: {
- dim: BORDER_COLORS.DIM,
- hover: BORDER_COLORS.HOVER,
- brand: BORDER_COLORS.BRAND,
- separator: BORDER_COLORS.SEPARATOR,
- },
- gradients: {
- videoOverlay: ['transparent', 'rgba(0,0,0,0.8)'],
- videoTop: ['rgba(0,0,0,0.5)', 'transparent'],
- },
- colorScheme: 'dark',
- statusBarStyle: 'light',
-}
diff --git a/src/shared/theme/index.ts b/src/shared/theme/index.ts
deleted file mode 100644
index c1ebf31..0000000
--- a/src/shared/theme/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export { ThemeProvider, useThemeColors } from './ThemeContext'
-export { darkColors } from './colors.dark'
-export type { ThemeColors } from './types'
-// Re-export invariant colors
-export { BRAND, PHASE, PHASE_COLORS, GRADIENTS } from '../constants/colors'
diff --git a/src/shared/theme/types.ts b/src/shared/theme/types.ts
deleted file mode 100644
index fc8ba55..0000000
--- a/src/shared/theme/types.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Theme type definitions
- * Dark Premium — refined navy, native iOS feel
- */
-
-export interface SurfaceStyle {
- backgroundColor: string
- borderColor: string
- borderWidth: number
-}
-
-export interface ThemeColors {
- bg: {
- base: string
- surface: string
- elevated: string
- overlay1: string
- overlay2: string
- overlay3: string
- scrim: string
- }
- text: {
- primary: string
- secondary: string
- tertiary: string
- muted: string
- hint: string
- disabled: string
- }
- surface: {
- default: SurfaceStyle
- accent: SurfaceStyle
- tip: SurfaceStyle
- }
- border: {
- dim: string
- hover: string
- brand: string
- separator: string
- }
- gradients: {
- videoOverlay: readonly string[]
- videoTop: readonly string[]
- }
- colorScheme: 'dark'
- statusBarStyle: 'light'
-}
diff --git a/src/shared/types/CLAUDE.md b/src/shared/types/CLAUDE.md
deleted file mode 100644
index 94b7dde..0000000
--- a/src/shared/types/CLAUDE.md
+++ /dev/null
@@ -1,12 +0,0 @@
-
-# Recent Activity
-
-
-
-### Apr 9, 2026
-
-| ID | Time | T | Title | Read |
-|----|------|---|-------|------|
-| #5980 | 9:54 AM | 🟣 | Bureau Tabata Kine program implemented with 13 office-friendly sessions | ~541 |
-| #5979 | 9:53 AM | 🟣 | Bureau program encoded into Tabata Kine system | ~432 |
-
\ No newline at end of file
diff --git a/src/shared/types/activity.ts b/src/shared/types/activity.ts
deleted file mode 100644
index 1187ee5..0000000
--- a/src/shared/types/activity.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * TabataFit Activity Types
- */
-
-export interface WorkoutResult {
- id: string
- workoutId: string
- completedAt: number
- calories: number
- durationMinutes: number
- rounds: number
- /** 0..1 */
- completionRate: number
-}
-
-export interface DayActivity {
- date: string
- completed: boolean
- workoutCount: number
-}
-
-export interface WeeklyStats {
- days: DayActivity[]
- totalWorkouts: number
- totalMinutes: number
- totalCalories: number
-}
-
-export interface Streak {
- current: number
- longest: number
-}
diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts
deleted file mode 100644
index 4ee6cdd..0000000
--- a/src/shared/types/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * TabataFit Shared Types
- */
-
-export * from './workout'
-export * from './trainer'
-export * from './user'
-export * from './activity'
-export * from './sync'
-export * from './program'
diff --git a/src/shared/types/program.ts b/src/shared/types/program.ts
deleted file mode 100644
index 61809e0..0000000
--- a/src/shared/types/program.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-import type { MusicVibe } from './workout'
-
-/**
- * Tabata Program Types
- * Physiotherapist-designed 4-program progressive system
- *
- * Model: Programme → Semaine → Séance → Bloc → Exercice
- * Each block alternates between odd/even exercises on tabata rounds.
- */
-
-// ─── Tabata Types ─────────────────────────────────────────────
-
-export type TabataProgramId = 'debutant' | 'intermediaire' | 'avance' | 'bureau'
-
-export type ProgramTier = 'free' | 'premium'
-
-/** A single exercise with tabata metadata */
-export interface TabataExercise {
- name: string
- nameEn: string
- /** Physiotherapist tip (the 📋 text from the guide) */
- conseil: string
- conseilEn: string
- /** Easier alternative */
- modification?: string
- modificationEn?: string
- /** Harder alternative */
- progression?: string
- progressionEn?: string
-}
-
-/** A tabata block alternates two exercises on odd/even rounds */
-export interface TabataBlock {
- id: string
- /** Rounds 1, 3, 5, 7 */
- oddExercise: TabataExercise
- /** Rounds 2, 4, 6, 8 */
- evenExercise: TabataExercise
- /** Standard tabata: 8 rounds per block */
- rounds: number
- /** Work interval in seconds (typically 20) */
- workTime: number
- /** Rest interval in seconds (typically 10) */
- restTime: number
-}
-
-/** A timed movement for warmup or cooldown */
-export interface TimedMovement {
- name: string
- nameEn: string
- /** Duration in seconds */
- duration: number
-}
-
-export interface WarmupPhase {
- movements: TimedMovement[]
- totalDuration: number
-}
-
-export interface CooldownPhase {
- movements: TimedMovement[]
- totalDuration: number
-}
-
-/** A session = warmup + blocks + cooldown */
-export interface TabataSession {
- id: string // e.g. 'deb-w1-s1'
- week: number
- /** Order within the week (1-based) */
- order: number
- title: string
- titleEn: string
- description: string
- descriptionEn: string
- focus: string[]
- focusEn: string[]
- warmup: WarmupPhase
- blocks: TabataBlock[]
- cooldown: CooldownPhase
- equipment: string[]
- /** Sum of all block rounds */
- totalRounds: number
- /** Estimated total duration in minutes including warmup/cooldown */
- totalDuration: number
- /** Estimated calories */
- calories: number
- /** Music vibe for this session (from workout program) */
- musicVibe?: MusicVibe
-}
-
-export interface TabataWeek {
- weekNumber: number
- title: string
- titleEn: string
- description: string
- descriptionEn: string
- focus: string
- focusEn: string
- /** Whether this is a deload week (reduced volume) */
- isDeload: boolean
- sessions: TabataSession[]
-}
-
-export interface TabataProgram {
- id: TabataProgramId
- title: string
- titleEn: string
- description: string
- descriptionEn: string
- tier: ProgramTier
- /** Accent color for UI */
- accentColor: string
- /** Icon name for display */
- icon: string
- durationWeeks: number
- /** Sessions per week — may vary by week */
- sessionsPerWeek: number
- totalSessions: number
- equipment: {
- required: string[]
- optional: string[]
- }
- focusAreas: string[]
- focusAreasEn: string[]
- /** Program rules/principles displayed to user */
- principles: string[]
- principlesEn: string[]
- /** Criteria to pass before moving to next program */
- completionCriteria: string[]
- completionCriteriaEn: string[]
- /** Recommended next program */
- nextProgramId?: TabataProgramId
- weeks: TabataWeek[]
-}
-
-/** Tabata-specific achievements */
-export interface TabataAchievement {
- id: string
- title: string
- titleEn: string
- description: string
- descriptionEn: string
- icon: string
- requirement: number
- type: 'sessions' | 'streak' | 'weeks' | 'programs' | 'calories'
-}
-
-/** Progress tracking for tabata programs */
-export interface TabataProgramProgress {
- programId: TabataProgramId
- currentWeek: number
- currentSessionIndex: number
- completedSessionIds: string[]
- isProgramCompleted: boolean
- startDate?: string
- lastSessionDate?: string
-}
-
-// ─── Timer Phases ─────────────────────────────────────────────
-
-export type TabataTimerPhase =
- | 'WARMUP'
- | 'WORK'
- | 'REST'
- | 'INTER_BLOCK_REST'
- | 'COOLDOWN'
- | 'COMPLETE'
-
-// ─── Legacy Types (kept for backward compatibility) ───────────
-
-export type ProgramId = 'upper-body' | 'lower-body' | 'full-body'
-
-export type WeekNumber = 1 | 2 | 3 | 4
-
-export type ProgramWorkout = {
- id: string
- week: WeekNumber
- order: number
- title: string
- description: string
- duration: 4
- exercises: ProgramExercise[]
- equipment: string[]
- focus: string[]
- tips: string[]
-}
-
-export type ProgramExercise = {
- name: string
- duration: 20
- reps?: string
- modification?: string
- progression?: string
-}
-
-export interface Week {
- weekNumber: WeekNumber
- title: string
- description: string
- focus: string
- workouts: ProgramWorkout[]
-}
-
-export interface Program {
- id: ProgramId
- title: string
- description: string
- durationWeeks: 4
- workoutsPerWeek: 5
- totalWorkouts: 20
- equipment: {
- required: string[]
- optional: string[]
- }
- focusAreas: string[]
- weeks: Week[]
-}
-
-export interface ProgramProgress {
- programId: ProgramId
- currentWeek: WeekNumber
- currentWorkoutIndex: number
- completedWorkoutIds: string[]
- isProgramCompleted: boolean
- startDate?: string
- lastWorkoutDate?: string
-}
-
-export interface AssessmentExercise {
- name: string
- duration: 20
- purpose: string
-}
-
-export interface Assessment {
- id: 'initial-assessment'
- title: string
- description: string
- duration: 4
- exercises: AssessmentExercise[]
- tips: string[]
-}
-
-export interface AssessmentResult {
- completedAt: string
- exercisesCompleted: string[]
- notes?: string
- recommendedProgram?: ProgramId
-}
-
-export type UserProgramState = {
- selectedProgram: ProgramId | null
- programsProgress: Record
- assessment: {
- isCompleted: boolean
- result: AssessmentResult | null
- }
-}
diff --git a/src/shared/types/sync.ts b/src/shared/types/sync.ts
deleted file mode 100644
index 18c12f4..0000000
--- a/src/shared/types/sync.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Sync Types for Supabase Integration
- * Handles opt-in personalization for premium users
- */
-
-export type SyncStatus =
- | 'never-synced' // Never been asked or opted in
- | 'prompt-pending' // Waiting to show prompt (after first workout)
- | 'synced' // Currently syncing
- | 'unsynced' // Previously synced, now disabled
-
-export interface UserProfileData {
- name: string
- fitnessLevel: string
- goal: string
- weeklyFrequency: number
- barriers: string[]
- onboardingCompletedAt: string
-}
-
-export interface WorkoutSessionData {
- workoutId: string
- completedAt: string
- durationSeconds: number
- caloriesBurned: number
- feelingRating?: number
-}
-
-export interface UserPreferencesData {
- preferredCategories: string[]
- preferredTrainers: string[]
- preferredDurations: number[]
- difficultyPreference: 'easier' | 'matched' | 'harder'
-}
-
-export interface SyncState {
- status: SyncStatus
- userId: string | null
- lastSyncAt: string | null
- pendingWorkouts: number // Local workouts waiting to sync
-}
-
-export interface EnableSyncResult {
- success: boolean
- userId?: string
- error?: string
-}
-
-export interface SyncWorkoutResult {
- success: boolean
- error?: string
-}
-
-export interface DeleteDataResult {
- success: boolean
- error?: string
-}
diff --git a/src/shared/types/trainer.ts b/src/shared/types/trainer.ts
deleted file mode 100644
index b274c15..0000000
--- a/src/shared/types/trainer.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * TabataFit Trainer Types
- */
-
-export interface Trainer {
- id: string
- name: string
- gender?: 'male' | 'female'
- specialty: string
- color: string
- avatarUrl?: string
- workoutCount: number
-}
diff --git a/src/shared/types/user.ts b/src/shared/types/user.ts
deleted file mode 100644
index 51df21f..0000000
--- a/src/shared/types/user.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * TabataFit User Types
- */
-
-import type { SyncStatus } from './sync'
-
-export type SubscriptionPlan = 'free' | 'premium-monthly' | 'premium-yearly'
-
-export interface UserSettings {
- haptics: boolean
- soundEffects: boolean
- voiceCoaching: boolean
- musicEnabled: boolean
- musicVolume: number
- reminders: boolean
- reminderTime: string
- hasPromptedReview: boolean
-}
-
-export type FitnessLevel = 'beginner' | 'intermediate' | 'advanced'
-export type FitnessGoal = 'weight-loss' | 'cardio' | 'strength' | 'wellness'
-export type WeeklyFrequency = 2 | 3 | 5
-
-export interface UserProfile {
- name: string
- email: string
- joinDate: string
- subscription: SubscriptionPlan
- onboardingCompleted: boolean
- fitnessLevel: FitnessLevel
- goal: FitnessGoal
- weeklyFrequency: WeeklyFrequency
- barriers: string[]
- savedWorkouts: string[]
- syncStatus: SyncStatus
- supabaseUserId: string | null
-}
-
-export interface Achievement {
- id: string
- title: string
- description: string
- icon: string
- requirement: number
- type: 'workouts' | 'streak' | 'minutes' | 'calories'
-}
diff --git a/src/shared/types/workout.ts b/src/shared/types/workout.ts
deleted file mode 100644
index 603fcf8..0000000
--- a/src/shared/types/workout.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * TabataFit Workout Types
- */
-
-export type WorkoutCategory = 'full-body' | 'core' | 'upper-body' | 'lower-body' | 'cardio'
-
-export type WorkoutLevel = 'Beginner' | 'Intermediate' | 'Advanced'
-
-export type WorkoutDuration = 4 | 8 | 12 | 20
-
-export type MusicVibe = 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
-
-export interface Exercise {
- name: string
- /** Work duration in seconds */
- duration: number
-}
-
-export interface Workout {
- id: string
- title: string
- trainerId: string
- category: WorkoutCategory
- level: WorkoutLevel
- /** Duration in minutes */
- duration: WorkoutDuration
- /** Estimated calories burned */
- calories: number
- exercises: Exercise[]
- /** Total rounds (work+rest cycles) */
- rounds: number
- /** Prep time in seconds */
- prepTime: number
- /** Work interval in seconds */
- workTime: number
- /** Rest interval in seconds */
- restTime: number
- equipment: string[]
- musicVibe: MusicVibe
- thumbnailUrl?: string
- videoUrl?: string
- isFeatured?: boolean
-}
-
-export interface Collection {
- id: string
- title: string
- description: string
- icon: string
- workoutIds: string[]
- gradient?: [string, string]
-}
-
-// Note: Old Program interface removed - replaced by new Program types in program.ts
diff --git a/src/shared/types/workoutProgram.ts b/src/shared/types/workoutProgram.ts
deleted file mode 100644
index 740a850..0000000
--- a/src/shared/types/workoutProgram.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import type { MusicVibe } from './workout'
-
-/**
- * Workout Program Types
- * Body Zone + Difficulty model
- * Program = Warmup → 3 Tabatas → Stretch
- * Tabata = 2 exercises × 8 rounds (20s work / 10s rest)
- */
-
-// ─── Enums ──────────────────────────────────────────────────────
-
-export type BodyZone = 'upper-body' | 'lower-body' | 'full-body'
-
-export type ProgramLevel = 'Beginner' | 'Intermediate' | 'Advanced'
-
-// ─── Exercise ───────────────────────────────────────────────────
-
-export interface ProgramExercise {
- name: string
- nameEn: string
- tip?: string
- tipEn?: string
- modification?: string
- modificationEn?: string
- progression?: string
- progressionEn?: string
- videoUrl?: string | null
-}
-
-// ─── Timed Exercise (warmup / stretch) ──────────────────────────
-
-export interface TimedExercise {
- name: string
- nameEn: string
- duration: number // seconds
- videoUrl: string | null
- tip?: string
- tipEn?: string
-}
-
-export interface WarmupBlock {
- exercises: TimedExercise[]
- totalDuration: number // seconds (sum of exercises.duration)
-}
-
-export interface StretchBlock {
- exercises: TimedExercise[]
- totalDuration: number
-}
-
-// ─── Tabata ─────────────────────────────────────────────────────
-
-export interface WorkoutTabata {
- id: string
- position: 1 | 2 | 3
- exercise1: ProgramExercise
- exercise2: ProgramExercise
- rounds: number
- workTime: number
- restTime: number
-}
-
-// ─── Program ────────────────────────────────────────────────────
-
-export interface WorkoutProgram {
- id: string
- title: string
- description: string | null
- bodyZone: BodyZone
- level: ProgramLevel
- isFree: boolean
- musicVibe: MusicVibe
- estimatedDuration: number
- estimatedCalories: number
- icon: string | null
- accentColor: string | null
- sortOrder: number
- warmup: WarmupBlock
- tabatas: WorkoutTabata[]
- stretch: StretchBlock
- createdAt: string
- updatedAt: string
-}
-
-// ─── Supabase Row Types ─────────────────────────────────────────
-
-export interface WorkoutProgramRow {
- id: string
- title: string
- description: string | null
- body_zone: BodyZone
- level: ProgramLevel
- is_free: boolean
- music_vibe: MusicVibe
- estimated_duration: number
- estimated_calories: number
- icon: string | null
- accent_color: string | null
- sort_order: number
- created_at: string
- updated_at: string
-}
-
-export interface WorkoutTabataRow {
- id: string
- program_id: string
- position: 1 | 2 | 3
- exercise_1_name: string
- exercise_1_name_en: string | null
- exercise_1_tip: string | null
- exercise_1_tip_en: string | null
- exercise_1_modification: string | null
- exercise_1_modification_en: string | null
- exercise_1_progression: string | null
- exercise_1_progression_en: string | null
- exercise_1_video_url: string | null
- exercise_2_name: string
- exercise_2_name_en: string | null
- exercise_2_tip: string | null
- exercise_2_tip_en: string | null
- exercise_2_modification: string | null
- exercise_2_modification_en: string | null
- exercise_2_progression: string | null
- exercise_2_progression_en: string | null
- exercise_2_video_url: string | null
- rounds: number
- work_time: number
- rest_time: number
- created_at: string
-}
-
-export interface WorkoutTimedExerciseRow {
- id: string
- program_id: string
- position: number
- name: string
- name_en: string | null
- tip: string | null
- tip_en: string | null
- duration: number
- video_url: string | null
- created_at: string
-}
-
-// ─── Display Metadata ───────────────────────────────────────────
-
-export const BODY_ZONE_META: Record = {
- 'upper-body': {
- label: 'Haut du corps',
- labelEn: 'Upper Body',
- icon: 'figure.strengthtraining.traditional',
- color: '#4A90D9',
- descKey: 'screens:home.zoneDescUpper',
- },
- 'lower-body': {
- label: 'Bas du corps',
- labelEn: 'Lower Body',
- icon: 'figure.run',
- color: '#9B59B6',
- descKey: 'screens:home.zoneDescLower',
- },
- 'full-body': {
- label: 'Corps entier',
- labelEn: 'Full Body',
- icon: 'figure.cooldown',
- color: '#00C896',
- descKey: 'screens:home.zoneDescFull',
- },
-}
-
-export const LEVEL_META: Record = {
- Beginner: { label: 'Débutant', labelEn: 'Beginner', color: '#30D158' },
- Intermediate: { label: 'Intermédiaire', labelEn: 'Intermediate', color: '#FF9500' },
- Advanced: { label: 'Avancé', labelEn: 'Advanced', color: '#FF453A' },
-}
diff --git a/src/shared/utils/CLAUDE.md b/src/shared/utils/CLAUDE.md
deleted file mode 100644
index adfdcb1..0000000
--- a/src/shared/utils/CLAUDE.md
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# Recent Activity
-
-
-
-*No recent activity*
-
\ No newline at end of file
diff --git a/src/shared/utils/color.ts b/src/shared/utils/color.ts
deleted file mode 100644
index 8c96cdf..0000000
--- a/src/shared/utils/color.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Color utility functions
- */
-
-/**
- * Converts a hex color + opacity (0-1) to an rgba string.
- * Handles 3-digit and 6-digit hex colors.
- */
-export function withOpacity(hex: string, opacity: number): string {
- const clean = hex.replace('#', '')
- const full = clean.length === 3
- ? clean.split('').map(c => c + c).join('')
- : clean
- const r = parseInt(full.substring(0, 2), 16)
- const g = parseInt(full.substring(2, 4), 16)
- const b = parseInt(full.substring(4, 6), 16)
- return `rgba(${r},${g},${b},${opacity})`
-}
diff --git a/src/shared/utils/logger.ts b/src/shared/utils/logger.ts
deleted file mode 100644
index 3e44666..0000000
--- a/src/shared/utils/logger.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Logger utility with __DEV__ guard
- * In production builds, all log/warn/debug calls are no-ops.
- * Errors are always logged (they indicate real problems).
- */
-
-/* eslint-disable no-console */
-
-function noop(..._args: unknown[]): void {}
-
-export const logger = {
- log: __DEV__ ? console.log.bind(console) : noop,
- warn: __DEV__ ? console.warn.bind(console) : noop,
- error: console.error.bind(console),
- debug: __DEV__ ? console.debug.bind(console) : noop,
- info: __DEV__ ? console.info.bind(console) : noop,
-}
diff --git a/storekit/CLAUDE.md b/storekit/CLAUDE.md
deleted file mode 100644
index adfdcb1..0000000
--- a/storekit/CLAUDE.md
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# Recent Activity
-
-
-
-*No recent activity*
-
\ No newline at end of file
diff --git a/storekit/TabataFit.storekit b/storekit/TabataFit.storekit
deleted file mode 100644
index f6e8e9b..0000000
--- a/storekit/TabataFit.storekit
+++ /dev/null
@@ -1,126 +0,0 @@
-{
- "identifier" : "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
- "type" : "Default",
- "version" : {
- "major" : 4,
- "minor" : 0
- },
- "settings" : {
- "_applicationInternalID" : "",
- "_developerTeamID" : "",
- "_failTransactionsEnabled" : false,
- "_lastSynchronizedDate" : null,
- "_locale" : "en_US",
- "_storefront" : "USA",
- "_storeKitErrors" : [
- {
- "current" : null,
- "enabled" : false,
- "name" : "Load Products"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "Purchase"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "Verification"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "App Store Sync"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "Subscription Status"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "App Transaction"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "Manage Subscriptions Sheet"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "Refund Request Sheet"
- },
- {
- "current" : null,
- "enabled" : false,
- "name" : "Offer Code Redeem Sheet"
- }
- ]
- },
- "subscriptionGroups" : [
- {
- "id" : "F1A2B3C4-D5E6-7890-ABCD-111111111111",
- "localizations" : [],
- "name" : "TabataFit Premium",
- "subscriptions" : [
- {
- "adHocOffers" : [],
- "codeOffers" : [],
- "displayPrice" : "6.99",
- "familyShareable" : true,
- "groupNumber" : 1,
- "internalID" : "A1111111-1111-1111-1111-111111111111",
- "introductoryOffer" : {
- "displayPrice" : "0",
- "internalID" : "B1111111-1111-1111-1111-111111111111",
- "numberOfPeriods" : 1,
- "paymentMode" : "free",
- "subscriptionPeriod" : "P1W"
- },
- "localizations" : [
- {
- "description" : "Monthly access to all TabataFit workouts and features",
- "displayName" : "TabataFit+ Monthly",
- "locale" : "en_US"
- }
- ],
- "productID" : "tabatafit_premium_monthly",
- "recurringSubscriptionPeriod" : "P1M",
- "referenceName" : "Monthly Premium",
- "subscriptionGroupID" : "F1A2B3C4-D5E6-7890-ABCD-111111111111",
- "type" : "RecurringSubscription"
- },
- {
- "adHocOffers" : [],
- "codeOffers" : [],
- "displayPrice" : "49.99",
- "familyShareable" : true,
- "groupNumber" : 1,
- "internalID" : "A2222222-2222-2222-2222-222222222222",
- "introductoryOffer" : {
- "displayPrice" : "0",
- "internalID" : "B2222222-2222-2222-2222-222222222222",
- "numberOfPeriods" : 1,
- "paymentMode" : "free",
- "subscriptionPeriod" : "P1W"
- },
- "localizations" : [
- {
- "description" : "Annual access to all TabataFit workouts — save 40%",
- "displayName" : "TabataFit+ Annual",
- "locale" : "en_US"
- }
- ],
- "productID" : "tabatafit_premium_annual",
- "recurringSubscriptionPeriod" : "P1Y",
- "referenceName" : "Annual Premium",
- "subscriptionGroupID" : "F1A2B3C4-D5E6-7890-ABCD-111111111111",
- "type" : "RecurringSubscription"
- }
- ]
- }
- ]
-}
diff --git a/tabatago-swift/Config/Secrets.xcconfig b/tabatago-swift/Config/Secrets.xcconfig
new file mode 100644
index 0000000..cb1e7bd
--- /dev/null
+++ b/tabatago-swift/Config/Secrets.xcconfig
@@ -0,0 +1,4 @@
+SUPABASE_URL = https:/$()/supabase.1000co.fr
+SUPABASE_ANON_KEY = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzcyMjMzMjAwLCJleHAiOjE5Mjk5OTk2MDB9.SlYN046eGvUSObW0tFQHcMRUqFvtMqBLfFRlZliSx_w
+REVENUECAT_API_KEY = test_oIJbIHWISJaUZdgxRMHlwizBHvM
+POSTHOG_API_KEY =
diff --git a/tabatago-swift/Config/Secrets.xcconfig.example b/tabatago-swift/Config/Secrets.xcconfig.example
new file mode 100644
index 0000000..d213014
--- /dev/null
+++ b/tabatago-swift/Config/Secrets.xcconfig.example
@@ -0,0 +1,8 @@
+// Secrets.xcconfig.example
+// Copy this file to Secrets.xcconfig and fill in your values.
+// Secrets.xcconfig is gitignored — never commit the real file.
+
+SUPABASE_URL = https://your-project.supabase.co
+SUPABASE_ANON_KEY = your-anon-key-here
+REVENUECAT_API_KEY = your-revenuecat-key-here
+POSTHOG_API_KEY = your-posthog-key-here
diff --git a/tabatago-swift/TabataGo.xcodeproj/project.pbxproj b/tabatago-swift/TabataGo.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..4a32f6d
--- /dev/null
+++ b/tabatago-swift/TabataGo.xcodeproj/project.pbxproj
@@ -0,0 +1,1159 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 09285D4F326731E9A27827B2 /* MusicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C9BB1EEE2291A9A23B5F3C /* MusicService.swift */; };
+ 14578A06877E3D67A49650A9 /* AudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAABCD29F20A3646B6A4036 /* AudioService.swift */; };
+ 14EC768D950BC071AFBEFDF2 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12715936CAA6BD90A7FBE9D7 /* MainTabView.swift */; };
+ 18E74EE69364472DA7F0D9EC /* TabataGoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D558DAFE1AD94786AA674A4 /* TabataGoUITests.swift */; };
+ 192F8CFFE1888005ABF339E8 /* WorkoutSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBBFC7FC6A52DE9908EE4A6 /* WorkoutSession.swift */; };
+ 1955D0D74D9B09D10705104C /* WorkoutProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E64CFB210A549AC85F878D /* WorkoutProgram.swift */; };
+ 20FD0BC9A6E01E8EA182E030 /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 2A66A0F120927A9EBC548828 /* Supabase */; };
+ 29DA1C9905E244CDC316D5AA /* CompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A6F4FB39AE902BCED1C2D5 /* CompletionView.swift */; };
+ 2D5CE02211FB67CD2CFDAA11 /* SupabaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDF0ED76A8476BF1F80EF8C /* SupabaseService.swift */; };
+ 367B00BF0E8537F9BA15530F /* MusicTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6156C6E0E1A543DAC87A90 /* MusicTrack.swift */; };
+ 3A1A6EA59BD9CDEFBF22763F /* ProgramsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16C8FAFDECBFA2D9CF66505B /* ProgramsTab.swift */; };
+ 3E2E78027B1973F72E05D8D2 /* PlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8206D4F904F61E5685DE369E /* PlayerViewModel.swift */; };
+ 3F0F63E163BBA968C4CEFF81 /* TabataGoWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 484865AEFA8CCD26C4AE7F73 /* TabataGoWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 3FAAAAC1576A7861AB833E39 /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5DFE227FDB6400C8D7A4A4 /* ProfileTab.swift */; };
+ 4371D8DD5F2638905606513A /* WatchActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8CA90001C65B27E3B7BE34 /* WatchActivityView.swift */; };
+ 53FDC12EFCD8159045C105C0 /* HealthKitService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0499FCA348FF1A127C8E4FAE /* HealthKitService.swift */; };
+ 556620C10FA0BC85E1BDE529 /* WatchIdleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495E38AB3B412E296F8C3649 /* WatchIdleView.swift */; };
+ 59B482DEBAA43EE5F24B883D /* HomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB04FA5E81BD1E52DEFB3AC2 /* HomeTab.swift */; };
+ 5A25DA9A1B21F5EED15BA370 /* ProgramDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFF744890571DE314540E16 /* ProgramDetailView.swift */; };
+ 5A402D7E31059AB7107B625C /* MusicPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5505FBD6E001AE3AFD413ADA /* MusicPlayerViewModel.swift */; };
+ 5CE2F2210BEF17AC304F2AC2 /* HealthSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DE8A4DAD846A879B8ED379 /* HealthSnapshot.swift */; };
+ 60503F963221C7FCF719C493 /* ActivityTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84123E854DE0BF3E0D4F0912 /* ActivityTab.swift */; };
+ 6060D95D485E4188EAABDDED /* WatchRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AEC37E6361DC4C7AE326139 /* WatchRootView.swift */; };
+ 61BD8C313424F89F13FDE92E /* PurchaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E005D62F3B3B80A2A53C2C /* PurchaseViewModel.swift */; };
+ 66E87ABBDC5C3B36B3E932FB /* WatchConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802638FA5E5FDB5B278123AC /* WatchConnectivityManager.swift */; };
+ 70C2DAC704F628494A59EF56 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5AA44513F793EA7FEBA00 /* Theme.swift */; };
+ 70DEC8E97218C774A46F7CEA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CDFE1E10182972315386F9D7 /* Assets.xcassets */; };
+ 725EBACF4CF7BC23D2C476AA /* TabataGoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09EB765FCE6A3EE95E86EB3 /* TabataGoApp.swift */; };
+ 7B4E626E8A28525094C19B8D /* PurchaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84A0F7F17713D5D0A679122 /* PurchaseService.swift */; };
+ 80214BDEB93076416728E9BD /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = FE5B048B90231B158C0027EA /* PostHog */; };
+ 8045D997CFE2447CBED7BF71 /* TabataGoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C5359B9E5850BE09484D2C /* TabataGoTests.swift */; };
+ 850C700B060F46134C2D4569 /* WatchPlayerEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973741405B4155D15137B3C4 /* WatchPlayerEngine.swift */; };
+ 86EF518650FBD42FF912DB58 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5F5D3568A736B7A326874677 /* Localizable.xcstrings */; };
+ 8C3B87A3ACCDE45862C33913 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE731C42C570A89F2C6F613 /* Strings.swift */; };
+ 90728D374B15A38DD9A75E5B /* WatchPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC464C4D17B88E57FB5477C /* WatchPlayerView.swift */; };
+ 93457B73C62C4BEA4329BD4C /* TabataGoWatchWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 255972F9906563A0921C47C0 /* TabataGoWatchWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 9633A730F910E47C28A288AC /* WatchConnectivityTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8425C668A3901B0F12DBFCD /* WatchConnectivityTypes.swift */; };
+ 9F9695303EEC1516B1845417 /* TabataGoWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9006191EE89D06E6558786E3 /* TabataGoWatchApp.swift */; };
+ AA17AD2E25DF408ECE100F99 /* PreviewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7482C05380DE017FF582C28B /* PreviewData.swift */; };
+ B4CFD4E752EF66F6535AD173 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815C7C1CC22063B7E27F2F9B /* SettingsView.swift */; };
+ B5591230E8B61A2B18F5DD87 /* WatchConnectivityTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8425C668A3901B0F12DBFCD /* WatchConnectivityTypes.swift */; };
+ B60D023230A8BA995A812FC3 /* PhoneConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25FD149C9626FEC155E8C72E /* PhoneConnectivityManager.swift */; };
+ C2CB48B999939D3550A50936 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 185636F13439162E23F874D3 /* Environment.swift */; };
+ CA45F50F953886A372F22AA4 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = C18748562E7BDB56A11C0FB3 /* RevenueCat */; };
+ CCCCEFD2D61ED1D7DDB9040C /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C8A95404A7E4E05A1A7C34 /* AnalyticsService.swift */; };
+ CDFA9A56DB6DA111B728FF48 /* BodyZoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514301FBE811B1260720D151 /* BodyZoneView.swift */; };
+ D03E8BFA9CC4400EC8718884 /* TabataGoSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DEACB2D18F636B35BB2C48 /* TabataGoSchema.swift */; };
+ D422758C736D40CB0E9C4063 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B4E4F3DA1047ACD980F581 /* NowPlayingView.swift */; };
+ D65673484CBB4DDA03C23225 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8242C26A4F51BE7AA779840 /* RootView.swift */; };
+ D665638A80E06A7C42019782 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93E214AAB0E1CB61B89EC75 /* PlayerView.swift */; };
+ DBFB6F75F59367A957B8F9B9 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE34000653EE789117CE9D9 /* UserProfile.swift */; };
+ E21B9936D15D2111807AAAE9 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28A422BCE1A702F8A10951FC /* OnboardingView.swift */; };
+ E4ED0B8CABBD3502EA468F21 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD1AF33C89F42294599C369A /* HomeViewModel.swift */; };
+ EDAFF4CD2ACE82CC2C097B3C /* PaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA21206AD91A5F95926EEA05 /* PaywallView.swift */; };
+ EE6C591611D52C36ED5E03C6 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC19129CD3C493C8B2AEFA8 /* AppState.swift */; };
+ F80248DC6213339BC8F9C9A2 /* TabataGoComplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA5D50FD057EF30BE7915F5 /* TabataGoComplication.swift */; };
+ FD47EC832E23E0AF1D6FFE47 /* PolicyViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525C7E8EC6EF89E00D34672E /* PolicyViews.swift */; };
+ FE14257B8CFFDC47C72AE079 /* HealthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63599808389B70FC2F6A43C3 /* HealthViewModel.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 199D4196A99EB5B38C309C5D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5D5CB9093007DF74EBDE3C98 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3945C3998B4B66F30759718C;
+ remoteInfo = TabataGoWatch;
+ };
+ D329F349FC6AF0E2D3C89FD3 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5D5CB9093007DF74EBDE3C98 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 90BAF2DB5D7456CD45975E26;
+ remoteInfo = TabataGoWatchWidget;
+ };
+ DB62F40A2069EDBDA1F2AE98 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5D5CB9093007DF74EBDE3C98 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 92991789C3A5B2A5FACF07A1;
+ remoteInfo = TabataGo;
+ };
+ F48E1ED0786D563407445F4E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5D5CB9093007DF74EBDE3C98 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 92991789C3A5B2A5FACF07A1;
+ remoteInfo = TabataGo;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 76FE977236B376F31232D242 /* Embed Watch Content */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
+ dstSubfolderSpec = 16;
+ files = (
+ 3F0F63E163BBA968C4CEFF81 /* TabataGoWatch.app in Embed Watch Content */,
+ );
+ name = "Embed Watch Content";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97F207A5CEE6835FA097805C /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 93457B73C62C4BEA4329BD4C /* TabataGoWatchWidget.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 01C5359B9E5850BE09484D2C /* TabataGoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabataGoTests.swift; sourceTree = ""; };
+ 0499FCA348FF1A127C8E4FAE /* HealthKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitService.swift; sourceTree = ""; };
+ 04C8A95404A7E4E05A1A7C34 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; };
+ 12715936CAA6BD90A7FBE9D7 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; };
+ 16C8FAFDECBFA2D9CF66505B /* ProgramsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramsTab.swift; sourceTree = ""; };
+ 185636F13439162E23F874D3 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; };
+ 255972F9906563A0921C47C0 /* TabataGoWatchWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TabataGoWatchWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ 25FD149C9626FEC155E8C72E /* PhoneConnectivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneConnectivityManager.swift; sourceTree = ""; };
+ 28A422BCE1A702F8A10951FC /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; };
+ 2C6156C6E0E1A543DAC87A90 /* MusicTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicTrack.swift; sourceTree = ""; };
+ 2D8CA90001C65B27E3B7BE34 /* WatchActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchActivityView.swift; sourceTree = ""; };
+ 31B4E4F3DA1047ACD980F581 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = ""; };
+ 33688B62F38435863620B90E /* TabataGo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TabataGo.entitlements; sourceTree = ""; };
+ 44A6F4FB39AE902BCED1C2D5 /* CompletionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionView.swift; sourceTree = ""; };
+ 484865AEFA8CCD26C4AE7F73 /* TabataGoWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TabataGoWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 495E38AB3B412E296F8C3649 /* WatchIdleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchIdleView.swift; sourceTree = ""; };
+ 4EDF0ED76A8476BF1F80EF8C /* SupabaseService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupabaseService.swift; sourceTree = ""; };
+ 514301FBE811B1260720D151 /* BodyZoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyZoneView.swift; sourceTree = ""; };
+ 525C7E8EC6EF89E00D34672E /* PolicyViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyViews.swift; sourceTree = ""; };
+ 5505FBD6E001AE3AFD413ADA /* MusicPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicPlayerViewModel.swift; sourceTree = ""; };
+ 58DEACB2D18F636B35BB2C48 /* TabataGoSchema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabataGoSchema.swift; sourceTree = ""; };
+ 5F5D3568A736B7A326874677 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; };
+ 61E5AA44513F793EA7FEBA00 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; };
+ 63599808389B70FC2F6A43C3 /* HealthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthViewModel.swift; sourceTree = ""; };
+ 6BAABCD29F20A3646B6A4036 /* AudioService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioService.swift; sourceTree = ""; };
+ 6BFF744890571DE314540E16 /* ProgramDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramDetailView.swift; sourceTree = ""; };
+ 6D558DAFE1AD94786AA674A4 /* TabataGoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabataGoUITests.swift; sourceTree = ""; };
+ 7482C05380DE017FF582C28B /* PreviewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewData.swift; sourceTree = ""; };
+ 7FE34000653EE789117CE9D9 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; };
+ 802638FA5E5FDB5B278123AC /* WatchConnectivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnectivityManager.swift; sourceTree = ""; };
+ 815C7C1CC22063B7E27F2F9B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
+ 8206D4F904F61E5685DE369E /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = ""; };
+ 84123E854DE0BF3E0D4F0912 /* ActivityTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTab.swift; sourceTree = ""; };
+ 8AEC37E6361DC4C7AE326139 /* WatchRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchRootView.swift; sourceTree = ""; };
+ 9006191EE89D06E6558786E3 /* TabataGoWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabataGoWatchApp.swift; sourceTree = ""; };
+ 973741405B4155D15137B3C4 /* WatchPlayerEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchPlayerEngine.swift; sourceTree = ""; };
+ 9B5DFE227FDB6400C8D7A4A4 /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = ""; };
+ 9CE731C42C570A89F2C6F613 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; };
+ 9EC19129CD3C493C8B2AEFA8 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; };
+ A7C07E8AF566483359CE2FEC /* TabataGoTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = TabataGoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ A84A0F7F17713D5D0A679122 /* PurchaseService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseService.swift; sourceTree = ""; };
+ AD1AF33C89F42294599C369A /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; };
+ B6E64CFB210A549AC85F878D /* WorkoutProgram.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutProgram.swift; sourceTree = ""; };
+ B7EDA5BF7F25E3279A4B1A61 /* TabataGoUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = TabataGoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ B8E005D62F3B3B80A2A53C2C /* PurchaseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseViewModel.swift; sourceTree = ""; };
+ BBBBFC7FC6A52DE9908EE4A6 /* WorkoutSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutSession.swift; sourceTree = ""; };
+ BD3DF875E3461305DADB554A /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = ""; };
+ C4C9BB1EEE2291A9A23B5F3C /* MusicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicService.swift; sourceTree = ""; };
+ CDA5D50FD057EF30BE7915F5 /* TabataGoComplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabataGoComplication.swift; sourceTree = ""; };
+ CDFE1E10182972315386F9D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ D09EB765FCE6A3EE95E86EB3 /* TabataGoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabataGoApp.swift; sourceTree = ""; };
+ D168B973B16C94426A15766A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ D593D23B6A2F633DFA166D91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ D8425C668A3901B0F12DBFCD /* WatchConnectivityTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatchConnectivityTypes.swift; path = ../../TabataGo/Services/WatchConnectivityTypes.swift; sourceTree = ""; };
+ D8A69F6B8DC5329436762B50 /* TabataGo.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = TabataGo.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ D983B6DEDE62A8F0E9E09E66 /* TabataGoWatch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TabataGoWatch.entitlements; sourceTree = ""; };
+ DA21206AD91A5F95926EEA05 /* PaywallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallView.swift; sourceTree = ""; };
+ DBC464C4D17B88E57FB5477C /* WatchPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchPlayerView.swift; sourceTree = ""; };
+ E93E214AAB0E1CB61B89EC75 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; };
+ F1DE8A4DAD846A879B8ED379 /* HealthSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSnapshot.swift; sourceTree = ""; };
+ F8242C26A4F51BE7AA779840 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; };
+ FB04FA5E81BD1E52DEFB3AC2 /* HomeTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTab.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 078CF2C46E747BF4F8A74030 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 20FD0BC9A6E01E8EA182E030 /* Supabase in Frameworks */,
+ CA45F50F953886A372F22AA4 /* RevenueCat in Frameworks */,
+ 80214BDEB93076416728E9BD /* PostHog in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 0F5986A46D5DAC67217BB243 /* Services */ = {
+ isa = PBXGroup;
+ children = (
+ 04C8A95404A7E4E05A1A7C34 /* AnalyticsService.swift */,
+ 6BAABCD29F20A3646B6A4036 /* AudioService.swift */,
+ 0499FCA348FF1A127C8E4FAE /* HealthKitService.swift */,
+ C4C9BB1EEE2291A9A23B5F3C /* MusicService.swift */,
+ 25FD149C9626FEC155E8C72E /* PhoneConnectivityManager.swift */,
+ A84A0F7F17713D5D0A679122 /* PurchaseService.swift */,
+ 4EDF0ED76A8476BF1F80EF8C /* SupabaseService.swift */,
+ D8425C668A3901B0F12DBFCD /* WatchConnectivityTypes.swift */,
+ );
+ path = Services;
+ sourceTree = "";
+ };
+ 0FB4710828254C8629904415 /* Complications */ = {
+ isa = PBXGroup;
+ children = (
+ D168B973B16C94426A15766A /* Info.plist */,
+ CDA5D50FD057EF30BE7915F5 /* TabataGoComplication.swift */,
+ );
+ path = Complications;
+ sourceTree = "";
+ };
+ 162DB0E4C0FB82BCAC489598 /* Paywall */ = {
+ isa = PBXGroup;
+ children = (
+ DA21206AD91A5F95926EEA05 /* PaywallView.swift */,
+ );
+ path = Paywall;
+ sourceTree = "";
+ };
+ 1648643216F3497AF84AE1C3 /* Theme */ = {
+ isa = PBXGroup;
+ children = (
+ 61E5AA44513F793EA7FEBA00 /* Theme.swift */,
+ );
+ path = Theme;
+ sourceTree = "";
+ };
+ 1F1136D2B7FEBD67D18C7679 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ D983B6DEDE62A8F0E9E09E66 /* TabataGoWatch.entitlements */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+ 2CA2BFA0975A88FFA1C41C86 = {
+ isa = PBXGroup;
+ children = (
+ 9070E6C405709C0B4D9623F9 /* Config */,
+ BD69946901F21DE2BEE0D8D9 /* TabataGo */,
+ DB98CF3F29FCFCDE4D54B1A8 /* TabataGoTests */,
+ 479C55D953F3D9AA136DE1BA /* TabataGoUITests */,
+ 66E9DD477B9F90EF36226076 /* TabataGoWatch */,
+ F992A53DB1C399DCFE3C8BF2 /* Products */,
+ );
+ sourceTree = "";
+ };
+ 3AD368BB11C8E02894C64345 /* Complete */ = {
+ isa = PBXGroup;
+ children = (
+ 44A6F4FB39AE902BCED1C2D5 /* CompletionView.swift */,
+ );
+ path = Complete;
+ sourceTree = "";
+ };
+ 3D6274962C5E19238FBFD579 /* App */ = {
+ isa = PBXGroup;
+ children = (
+ 9006191EE89D06E6558786E3 /* TabataGoWatchApp.swift */,
+ );
+ path = App;
+ sourceTree = "";
+ };
+ 479C55D953F3D9AA136DE1BA /* TabataGoUITests */ = {
+ isa = PBXGroup;
+ children = (
+ 6D558DAFE1AD94786AA674A4 /* TabataGoUITests.swift */,
+ );
+ path = TabataGoUITests;
+ sourceTree = "";
+ };
+ 4B4FB2F47AD5A34A21679372 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 2D8CA90001C65B27E3B7BE34 /* WatchActivityView.swift */,
+ 495E38AB3B412E296F8C3649 /* WatchIdleView.swift */,
+ DBC464C4D17B88E57FB5477C /* WatchPlayerView.swift */,
+ 8AEC37E6361DC4C7AE326139 /* WatchRootView.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ 562818BD97AE83644CD2F695 /* Onboarding */ = {
+ isa = PBXGroup;
+ children = (
+ 28A422BCE1A702F8A10951FC /* OnboardingView.swift */,
+ );
+ path = Onboarding;
+ sourceTree = "";
+ };
+ 66E9DD477B9F90EF36226076 /* TabataGoWatch */ = {
+ isa = PBXGroup;
+ children = (
+ 3D6274962C5E19238FBFD579 /* App */,
+ 0FB4710828254C8629904415 /* Complications */,
+ 1F1136D2B7FEBD67D18C7679 /* Resources */,
+ F514B75119B8194DB1791B95 /* Services */,
+ 4B4FB2F47AD5A34A21679372 /* Views */,
+ );
+ path = TabataGoWatch;
+ sourceTree = "";
+ };
+ 8B90DED418BDA3697748C37D /* Tabs */ = {
+ isa = PBXGroup;
+ children = (
+ 84123E854DE0BF3E0D4F0912 /* ActivityTab.swift */,
+ FB04FA5E81BD1E52DEFB3AC2 /* HomeTab.swift */,
+ 12715936CAA6BD90A7FBE9D7 /* MainTabView.swift */,
+ 9B5DFE227FDB6400C8D7A4A4 /* ProfileTab.swift */,
+ 16C8FAFDECBFA2D9CF66505B /* ProgramsTab.swift */,
+ );
+ path = Tabs;
+ sourceTree = "";
+ };
+ 9070E6C405709C0B4D9623F9 /* Config */ = {
+ isa = PBXGroup;
+ children = (
+ BD3DF875E3461305DADB554A /* Secrets.xcconfig */,
+ );
+ path = Config;
+ sourceTree = "";
+ };
+ 98E39E3A8C6A422F8AED106C /* Health */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = Health;
+ sourceTree = "";
+ };
+ A21FBA0964CB3FB318DE42E8 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 3AD368BB11C8E02894C64345 /* Complete */,
+ 98E39E3A8C6A422F8AED106C /* Health */,
+ 562818BD97AE83644CD2F695 /* Onboarding */,
+ 162DB0E4C0FB82BCAC489598 /* Paywall */,
+ A4BD547C912B1970BB98FC73 /* Player */,
+ C9F93E0937EB534BC24EF2FA /* Programs */,
+ EEAEB608A9FC8E5981B57A89 /* Settings */,
+ 8B90DED418BDA3697748C37D /* Tabs */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ A4BD547C912B1970BB98FC73 /* Player */ = {
+ isa = PBXGroup;
+ children = (
+ 31B4E4F3DA1047ACD980F581 /* NowPlayingView.swift */,
+ E93E214AAB0E1CB61B89EC75 /* PlayerView.swift */,
+ );
+ path = Player;
+ sourceTree = "";
+ };
+ A5772C2339F48ED5CF255A29 /* Utilities */ = {
+ isa = PBXGroup;
+ children = (
+ 185636F13439162E23F874D3 /* Environment.swift */,
+ 9CE731C42C570A89F2C6F613 /* Strings.swift */,
+ );
+ path = Utilities;
+ sourceTree = "";
+ };
+ BD69946901F21DE2BEE0D8D9 /* TabataGo */ = {
+ isa = PBXGroup;
+ children = (
+ D72E5B5E6AA55AA4DD27D119 /* App */,
+ DC96ED5F68F75A02548ECD40 /* Models */,
+ DA785899F5E5947229C419DA /* Resources */,
+ 0F5986A46D5DAC67217BB243 /* Services */,
+ 1648643216F3497AF84AE1C3 /* Theme */,
+ A5772C2339F48ED5CF255A29 /* Utilities */,
+ CD710A86C04A7CE744A97D63 /* ViewModels */,
+ A21FBA0964CB3FB318DE42E8 /* Views */,
+ );
+ path = TabataGo;
+ sourceTree = "";
+ };
+ C9F93E0937EB534BC24EF2FA /* Programs */ = {
+ isa = PBXGroup;
+ children = (
+ 514301FBE811B1260720D151 /* BodyZoneView.swift */,
+ 6BFF744890571DE314540E16 /* ProgramDetailView.swift */,
+ );
+ path = Programs;
+ sourceTree = "";
+ };
+ CD710A86C04A7CE744A97D63 /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ 63599808389B70FC2F6A43C3 /* HealthViewModel.swift */,
+ AD1AF33C89F42294599C369A /* HomeViewModel.swift */,
+ 5505FBD6E001AE3AFD413ADA /* MusicPlayerViewModel.swift */,
+ 8206D4F904F61E5685DE369E /* PlayerViewModel.swift */,
+ B8E005D62F3B3B80A2A53C2C /* PurchaseViewModel.swift */,
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ D72E5B5E6AA55AA4DD27D119 /* App */ = {
+ isa = PBXGroup;
+ children = (
+ 9EC19129CD3C493C8B2AEFA8 /* AppState.swift */,
+ F8242C26A4F51BE7AA779840 /* RootView.swift */,
+ D09EB765FCE6A3EE95E86EB3 /* TabataGoApp.swift */,
+ );
+ path = App;
+ sourceTree = "";
+ };
+ DA785899F5E5947229C419DA /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ CDFE1E10182972315386F9D7 /* Assets.xcassets */,
+ D593D23B6A2F633DFA166D91 /* Info.plist */,
+ 5F5D3568A736B7A326874677 /* Localizable.xcstrings */,
+ 33688B62F38435863620B90E /* TabataGo.entitlements */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+ DB98CF3F29FCFCDE4D54B1A8 /* TabataGoTests */ = {
+ isa = PBXGroup;
+ children = (
+ 01C5359B9E5850BE09484D2C /* TabataGoTests.swift */,
+ );
+ path = TabataGoTests;
+ sourceTree = "";
+ };
+ DC96ED5F68F75A02548ECD40 /* Models */ = {
+ isa = PBXGroup;
+ children = (
+ F1DE8A4DAD846A879B8ED379 /* HealthSnapshot.swift */,
+ 2C6156C6E0E1A543DAC87A90 /* MusicTrack.swift */,
+ 7482C05380DE017FF582C28B /* PreviewData.swift */,
+ 58DEACB2D18F636B35BB2C48 /* TabataGoSchema.swift */,
+ 7FE34000653EE789117CE9D9 /* UserProfile.swift */,
+ B6E64CFB210A549AC85F878D /* WorkoutProgram.swift */,
+ BBBBFC7FC6A52DE9908EE4A6 /* WorkoutSession.swift */,
+ );
+ path = Models;
+ sourceTree = "";
+ };
+ EEAEB608A9FC8E5981B57A89 /* Settings */ = {
+ isa = PBXGroup;
+ children = (
+ 525C7E8EC6EF89E00D34672E /* PolicyViews.swift */,
+ 815C7C1CC22063B7E27F2F9B /* SettingsView.swift */,
+ );
+ path = Settings;
+ sourceTree = "";
+ };
+ F514B75119B8194DB1791B95 /* Services */ = {
+ isa = PBXGroup;
+ children = (
+ 802638FA5E5FDB5B278123AC /* WatchConnectivityManager.swift */,
+ D8425C668A3901B0F12DBFCD /* WatchConnectivityTypes.swift */,
+ 973741405B4155D15137B3C4 /* WatchPlayerEngine.swift */,
+ );
+ path = Services;
+ sourceTree = "";
+ };
+ F992A53DB1C399DCFE3C8BF2 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ D8A69F6B8DC5329436762B50 /* TabataGo.app */,
+ A7C07E8AF566483359CE2FEC /* TabataGoTests.xctest */,
+ B7EDA5BF7F25E3279A4B1A61 /* TabataGoUITests.xctest */,
+ 484865AEFA8CCD26C4AE7F73 /* TabataGoWatch.app */,
+ 255972F9906563A0921C47C0 /* TabataGoWatchWidget.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 3945C3998B4B66F30759718C /* TabataGoWatch */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E706A289AD3B8CCB1F3310BE /* Build configuration list for PBXNativeTarget "TabataGoWatch" */;
+ buildPhases = (
+ 688CFA91471FB46E21B9EFB2 /* Sources */,
+ 97F207A5CEE6835FA097805C /* Embed Foundation Extensions */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ CAF3AD48883D2945C69905DF /* PBXTargetDependency */,
+ );
+ name = TabataGoWatch;
+ packageProductDependencies = (
+ );
+ productName = TabataGoWatch;
+ productReference = 484865AEFA8CCD26C4AE7F73 /* TabataGoWatch.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 90BAF2DB5D7456CD45975E26 /* TabataGoWatchWidget */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 569F5B8F2ACCAC8356B6D8A0 /* Build configuration list for PBXNativeTarget "TabataGoWatchWidget" */;
+ buildPhases = (
+ 45E43A5DEF67C8A8869BB577 /* Sources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = TabataGoWatchWidget;
+ packageProductDependencies = (
+ );
+ productName = TabataGoWatchWidget;
+ productReference = 255972F9906563A0921C47C0 /* TabataGoWatchWidget.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+ 92991789C3A5B2A5FACF07A1 /* TabataGo */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = D920067B8306F17FB19B987C /* Build configuration list for PBXNativeTarget "TabataGo" */;
+ buildPhases = (
+ CD55FA03BDAE961D1DB600EC /* Sources */,
+ 3D4690E104FE866070533A03 /* Resources */,
+ 078CF2C46E747BF4F8A74030 /* Frameworks */,
+ 76FE977236B376F31232D242 /* Embed Watch Content */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 08E32451A0A32FD65422174D /* PBXTargetDependency */,
+ );
+ name = TabataGo;
+ packageProductDependencies = (
+ 2A66A0F120927A9EBC548828 /* Supabase */,
+ C18748562E7BDB56A11C0FB3 /* RevenueCat */,
+ FE5B048B90231B158C0027EA /* PostHog */,
+ );
+ productName = TabataGo;
+ productReference = D8A69F6B8DC5329436762B50 /* TabataGo.app */;
+ productType = "com.apple.product-type.application";
+ };
+ D1E2CCCC7C9BD41029959883 /* TabataGoTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8588823708FAA81CCE59E4E2 /* Build configuration list for PBXNativeTarget "TabataGoTests" */;
+ buildPhases = (
+ 58CC6E4A44750A943FC0D3D2 /* Sources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 4F860670537B1B4727317A1F /* PBXTargetDependency */,
+ );
+ name = TabataGoTests;
+ packageProductDependencies = (
+ );
+ productName = TabataGoTests;
+ productReference = A7C07E8AF566483359CE2FEC /* TabataGoTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ D77CBB3569E06BDB4239862D /* TabataGoUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = B6509A4A480437475C2BD19E /* Build configuration list for PBXNativeTarget "TabataGoUITests" */;
+ buildPhases = (
+ D7CD98EDC444042E53355A5B /* Sources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ A88A993C9D116DA16F65647B /* PBXTargetDependency */,
+ );
+ name = TabataGoUITests;
+ packageProductDependencies = (
+ );
+ productName = TabataGoUITests;
+ productReference = B7EDA5BF7F25E3279A4B1A61 /* TabataGoUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 5D5CB9093007DF74EBDE3C98 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 2600;
+ TargetAttributes = {
+ D77CBB3569E06BDB4239862D = {
+ TestTargetID = 92991789C3A5B2A5FACF07A1;
+ };
+ };
+ };
+ buildConfigurationList = D511FD795A1B4393C6DBCCE8 /* Build configuration list for PBXProject "TabataGo" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ Base,
+ de,
+ en,
+ es,
+ fr,
+ );
+ mainGroup = 2CA2BFA0975A88FFA1C41C86;
+ minimizedProjectReferenceProxies = 1;
+ packageReferences = (
+ 2750E748A2B654070ACF7FDB /* XCRemoteSwiftPackageReference "posthog-ios" */,
+ C3900C1BDD31EBECA68AC321 /* XCRemoteSwiftPackageReference "purchases-ios" */,
+ B502009078E4064243CF221F /* XCRemoteSwiftPackageReference "supabase-swift" */,
+ );
+ preferredProjectObjectVersion = 77;
+ productRefGroup = F992A53DB1C399DCFE3C8BF2 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 92991789C3A5B2A5FACF07A1 /* TabataGo */,
+ D1E2CCCC7C9BD41029959883 /* TabataGoTests */,
+ D77CBB3569E06BDB4239862D /* TabataGoUITests */,
+ 3945C3998B4B66F30759718C /* TabataGoWatch */,
+ 90BAF2DB5D7456CD45975E26 /* TabataGoWatchWidget */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 3D4690E104FE866070533A03 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 70DEC8E97218C774A46F7CEA /* Assets.xcassets in Resources */,
+ 86EF518650FBD42FF912DB58 /* Localizable.xcstrings in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 45E43A5DEF67C8A8869BB577 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ F80248DC6213339BC8F9C9A2 /* TabataGoComplication.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 58CC6E4A44750A943FC0D3D2 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8045D997CFE2447CBED7BF71 /* TabataGoTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 688CFA91471FB46E21B9EFB2 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9F9695303EEC1516B1845417 /* TabataGoWatchApp.swift in Sources */,
+ 4371D8DD5F2638905606513A /* WatchActivityView.swift in Sources */,
+ 66E87ABBDC5C3B36B3E932FB /* WatchConnectivityManager.swift in Sources */,
+ B5591230E8B61A2B18F5DD87 /* WatchConnectivityTypes.swift in Sources */,
+ 556620C10FA0BC85E1BDE529 /* WatchIdleView.swift in Sources */,
+ 850C700B060F46134C2D4569 /* WatchPlayerEngine.swift in Sources */,
+ 90728D374B15A38DD9A75E5B /* WatchPlayerView.swift in Sources */,
+ 6060D95D485E4188EAABDDED /* WatchRootView.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ CD55FA03BDAE961D1DB600EC /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 60503F963221C7FCF719C493 /* ActivityTab.swift in Sources */,
+ CCCCEFD2D61ED1D7DDB9040C /* AnalyticsService.swift in Sources */,
+ EE6C591611D52C36ED5E03C6 /* AppState.swift in Sources */,
+ 14578A06877E3D67A49650A9 /* AudioService.swift in Sources */,
+ CDFA9A56DB6DA111B728FF48 /* BodyZoneView.swift in Sources */,
+ 29DA1C9905E244CDC316D5AA /* CompletionView.swift in Sources */,
+ C2CB48B999939D3550A50936 /* Environment.swift in Sources */,
+ 53FDC12EFCD8159045C105C0 /* HealthKitService.swift in Sources */,
+ 5CE2F2210BEF17AC304F2AC2 /* HealthSnapshot.swift in Sources */,
+ FE14257B8CFFDC47C72AE079 /* HealthViewModel.swift in Sources */,
+ 59B482DEBAA43EE5F24B883D /* HomeTab.swift in Sources */,
+ E4ED0B8CABBD3502EA468F21 /* HomeViewModel.swift in Sources */,
+ 14EC768D950BC071AFBEFDF2 /* MainTabView.swift in Sources */,
+ 5A402D7E31059AB7107B625C /* MusicPlayerViewModel.swift in Sources */,
+ 09285D4F326731E9A27827B2 /* MusicService.swift in Sources */,
+ 367B00BF0E8537F9BA15530F /* MusicTrack.swift in Sources */,
+ D422758C736D40CB0E9C4063 /* NowPlayingView.swift in Sources */,
+ E21B9936D15D2111807AAAE9 /* OnboardingView.swift in Sources */,
+ EDAFF4CD2ACE82CC2C097B3C /* PaywallView.swift in Sources */,
+ B60D023230A8BA995A812FC3 /* PhoneConnectivityManager.swift in Sources */,
+ D665638A80E06A7C42019782 /* PlayerView.swift in Sources */,
+ 3E2E78027B1973F72E05D8D2 /* PlayerViewModel.swift in Sources */,
+ FD47EC832E23E0AF1D6FFE47 /* PolicyViews.swift in Sources */,
+ AA17AD2E25DF408ECE100F99 /* PreviewData.swift in Sources */,
+ 3FAAAAC1576A7861AB833E39 /* ProfileTab.swift in Sources */,
+ 5A25DA9A1B21F5EED15BA370 /* ProgramDetailView.swift in Sources */,
+ 3A1A6EA59BD9CDEFBF22763F /* ProgramsTab.swift in Sources */,
+ 7B4E626E8A28525094C19B8D /* PurchaseService.swift in Sources */,
+ 61BD8C313424F89F13FDE92E /* PurchaseViewModel.swift in Sources */,
+ D65673484CBB4DDA03C23225 /* RootView.swift in Sources */,
+ B4CFD4E752EF66F6535AD173 /* SettingsView.swift in Sources */,
+ 8C3B87A3ACCDE45862C33913 /* Strings.swift in Sources */,
+ 2D5CE02211FB67CD2CFDAA11 /* SupabaseService.swift in Sources */,
+ 725EBACF4CF7BC23D2C476AA /* TabataGoApp.swift in Sources */,
+ D03E8BFA9CC4400EC8718884 /* TabataGoSchema.swift in Sources */,
+ 70C2DAC704F628494A59EF56 /* Theme.swift in Sources */,
+ DBFB6F75F59367A957B8F9B9 /* UserProfile.swift in Sources */,
+ 9633A730F910E47C28A288AC /* WatchConnectivityTypes.swift in Sources */,
+ 1955D0D74D9B09D10705104C /* WorkoutProgram.swift in Sources */,
+ 192F8CFFE1888005ABF339E8 /* WorkoutSession.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ D7CD98EDC444042E53355A5B /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 18E74EE69364472DA7F0D9EC /* TabataGoUITests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 08E32451A0A32FD65422174D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3945C3998B4B66F30759718C /* TabataGoWatch */;
+ targetProxy = 199D4196A99EB5B38C309C5D /* PBXContainerItemProxy */;
+ };
+ 4F860670537B1B4727317A1F /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 92991789C3A5B2A5FACF07A1 /* TabataGo */;
+ targetProxy = F48E1ED0786D563407445F4E /* PBXContainerItemProxy */;
+ };
+ A88A993C9D116DA16F65647B /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 92991789C3A5B2A5FACF07A1 /* TabataGo */;
+ targetProxy = DB62F40A2069EDBDA1F2AE98 /* PBXContainerItemProxy */;
+ };
+ CAF3AD48883D2945C69905DF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 90BAF2DB5D7456CD45975E26 /* TabataGoWatchWidget */;
+ targetProxy = D329F349FC6AF0E2D3C89FD3 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 25D3B39EEB6D3E72B237E8F0 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.uitests;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = TabataGo;
+ };
+ name = Release;
+ };
+ 2917BEF34363D24B944CDBA1 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.tests;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TabataGo.app/TabataGo";
+ };
+ name = Release;
+ };
+ 3DD4336C0CCAAAFD3B5F30DF /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ INFOPLIST_FILE = TabataGoWatch/Complications/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.watchkitapp.widget;
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = 4;
+ WATCHOS_DEPLOYMENT_TARGET = 11.0;
+ };
+ name = Release;
+ };
+ 44F45541FA6C5BFE5CCF517D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ INFOPLIST_FILE = TabataGoWatch/Complications/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.watchkitapp.widget;
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = 4;
+ WATCHOS_DEPLOYMENT_TARGET = 11.0;
+ };
+ name = Debug;
+ };
+ 5B2758E3710E78A54FCD6BD2 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = BD3DF875E3461305DADB554A /* Secrets.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = TabataGo/Resources/TabataGo.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = TabataGo/Resources/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 637DF2A54A733BD3A292CA2D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "DEBUG=1",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 6.0;
+ WATCHOS_DEPLOYMENT_TARGET = 11.0;
+ };
+ name = Debug;
+ };
+ 6C10FBA626E18FAABA747C30 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.tests;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TabataGo.app/TabataGo";
+ };
+ name = Debug;
+ };
+ 95D96CBB4EFBF492A95A92BA /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = BD3DF875E3461305DADB554A /* Secrets.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = TabataGo/Resources/TabataGo.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = TabataGo/Resources/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ AC1DE840AE0DA9E76B9BB4EC /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = TabataGoWatch/Resources/TabataGoWatch.entitlements;
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = TabataGoWatch/Resources/Info.plist;
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.watchkitapp;
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = 4;
+ WATCHOS_DEPLOYMENT_TARGET = 11.0;
+ };
+ name = Debug;
+ };
+ C771DA656A325C34900A1CA9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.uitests;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = TabataGo;
+ };
+ name = Debug;
+ };
+ C7CBC011FEB78B687C73EE91 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = TabataGoWatch/Resources/TabataGoWatch.entitlements;
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = TabataGoWatch/Resources/Info.plist;
+ PRODUCT_BUNDLE_IDENTIFIER = com.tabatago.app.watchkitapp;
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = 4;
+ WATCHOS_DEPLOYMENT_TARGET = 11.0;
+ };
+ name = Release;
+ };
+ D464BA09CF5D5EE9E52925D8 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ SWIFT_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 6.0;
+ WATCHOS_DEPLOYMENT_TARGET = 11.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 569F5B8F2ACCAC8356B6D8A0 /* Build configuration list for PBXNativeTarget "TabataGoWatchWidget" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 44F45541FA6C5BFE5CCF517D /* Debug */,
+ 3DD4336C0CCAAAFD3B5F30DF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ 8588823708FAA81CCE59E4E2 /* Build configuration list for PBXNativeTarget "TabataGoTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6C10FBA626E18FAABA747C30 /* Debug */,
+ 2917BEF34363D24B944CDBA1 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ B6509A4A480437475C2BD19E /* Build configuration list for PBXNativeTarget "TabataGoUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C771DA656A325C34900A1CA9 /* Debug */,
+ 25D3B39EEB6D3E72B237E8F0 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ D511FD795A1B4393C6DBCCE8 /* Build configuration list for PBXProject "TabataGo" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 637DF2A54A733BD3A292CA2D /* Debug */,
+ D464BA09CF5D5EE9E52925D8 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ D920067B8306F17FB19B987C /* Build configuration list for PBXNativeTarget "TabataGo" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 95D96CBB4EFBF492A95A92BA /* Debug */,
+ 5B2758E3710E78A54FCD6BD2 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ E706A289AD3B8CCB1F3310BE /* Build configuration list for PBXNativeTarget "TabataGoWatch" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ AC1DE840AE0DA9E76B9BB4EC /* Debug */,
+ C7CBC011FEB78B687C73EE91 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ 2750E748A2B654070ACF7FDB /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/PostHog/posthog-ios";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 3.0.0;
+ };
+ };
+ B502009078E4064243CF221F /* XCRemoteSwiftPackageReference "supabase-swift" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/supabase/supabase-swift";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 2.5.0;
+ };
+ };
+ C3900C1BDD31EBECA68AC321 /* XCRemoteSwiftPackageReference "purchases-ios" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/RevenueCat/purchases-ios";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 5.0.0;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 2A66A0F120927A9EBC548828 /* Supabase */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = B502009078E4064243CF221F /* XCRemoteSwiftPackageReference "supabase-swift" */;
+ productName = Supabase;
+ };
+ C18748562E7BDB56A11C0FB3 /* RevenueCat */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = C3900C1BDD31EBECA68AC321 /* XCRemoteSwiftPackageReference "purchases-ios" */;
+ productName = RevenueCat;
+ };
+ FE5B048B90231B158C0027EA /* PostHog */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 2750E748A2B654070ACF7FDB /* XCRemoteSwiftPackageReference "posthog-ios" */;
+ productName = PostHog;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 5D5CB9093007DF74EBDE3C98 /* Project object */;
+}
diff --git a/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..a31781a
--- /dev/null
+++ b/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,96 @@
+{
+ "originHash" : "42d1f35b4500c2779457daf99841f2333a14d9a2965835305d89fd95beda836e",
+ "pins" : [
+ {
+ "identity" : "plcrashreporter",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/microsoft/plcrashreporter.git",
+ "state" : {
+ "revision" : "0254f941c646b1ed17b243654723d0f071e990d0",
+ "version" : "1.12.2"
+ }
+ },
+ {
+ "identity" : "posthog-ios",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/PostHog/posthog-ios",
+ "state" : {
+ "revision" : "c3efdae383a5e7a5a88c34fd774e9d7dc915b9d4",
+ "version" : "3.55.0"
+ }
+ },
+ {
+ "identity" : "purchases-ios",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/RevenueCat/purchases-ios",
+ "state" : {
+ "revision" : "bd63241b2258ea519020eb32a349db44fb44b119",
+ "version" : "5.68.0"
+ }
+ },
+ {
+ "identity" : "supabase-swift",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/supabase/supabase-swift",
+ "state" : {
+ "revision" : "17261e93c60aa721e3c17312bfeb2ae6de3d6f8a",
+ "version" : "2.43.1"
+ }
+ },
+ {
+ "identity" : "swift-asn1",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-asn1.git",
+ "state" : {
+ "revision" : "eb50cbd14606a9161cbc5d452f18797c90ef0bab",
+ "version" : "1.7.0"
+ }
+ },
+ {
+ "identity" : "swift-clocks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-clocks",
+ "state" : {
+ "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
+ "version" : "1.0.6"
+ }
+ },
+ {
+ "identity" : "swift-concurrency-extras",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-concurrency-extras",
+ "state" : {
+ "revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
+ "version" : "1.3.2"
+ }
+ },
+ {
+ "identity" : "swift-crypto",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-crypto.git",
+ "state" : {
+ "revision" : "476538ccb827f2dd18efc5de754cc87d77127a47",
+ "version" : "4.4.0"
+ }
+ },
+ {
+ "identity" : "swift-http-types",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-http-types.git",
+ "state" : {
+ "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca",
+ "version" : "1.5.1"
+ }
+ },
+ {
+ "identity" : "xctest-dynamic-overlay",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
+ "state" : {
+ "revision" : "dfd70507def84cb5fb821278448a262c6ff2bbad",
+ "version" : "1.9.0"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/xcuserdata/20015659.xcuserdatad/UserInterfaceState.xcuserstate b/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/xcuserdata/20015659.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 0000000..d6c2fba
Binary files /dev/null and b/tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/xcuserdata/20015659.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/tabatago-swift/TabataGo.xcodeproj/xcuserdata/20015659.xcuserdatad/xcschemes/xcschememanagement.plist b/tabatago-swift/TabataGo.xcodeproj/xcuserdata/20015659.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..2d748fa
--- /dev/null
+++ b/tabatago-swift/TabataGo.xcodeproj/xcuserdata/20015659.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ SchemeUserState
+
+ TabataGo.xcscheme_^#shared#^_
+
+ orderHint
+ 2
+
+ TabataGoWatch.xcscheme_^#shared#^_
+
+ orderHint
+ 3
+
+ TabataGoWatchWidget.xcscheme_^#shared#^_
+
+ orderHint
+ 4
+
+
+
+
diff --git a/tabatago-swift/TabataGo/App/AppState.swift b/tabatago-swift/TabataGo/App/AppState.swift
new file mode 100644
index 0000000..a18b154
--- /dev/null
+++ b/tabatago-swift/TabataGo/App/AppState.swift
@@ -0,0 +1,18 @@
+import Foundation
+import Observation
+
+/// Global app bootstrap state — initialises all services once at launch.
+@Observable
+final class AppState {
+
+ var isBootstrapped = false
+
+ @MainActor
+ func bootstrap() async {
+ guard !isBootstrapped else { return }
+ guard !AppEnvironment.isPreview else { isBootstrapped = true; return }
+ await PurchaseService.shared.initialize()
+ AnalyticsService.shared.initialize()
+ isBootstrapped = true
+ }
+}
diff --git a/tabatago-swift/TabataGo/App/RootView.swift b/tabatago-swift/TabataGo/App/RootView.swift
new file mode 100644
index 0000000..33b80cd
--- /dev/null
+++ b/tabatago-swift/TabataGo/App/RootView.swift
@@ -0,0 +1,21 @@
+import SwiftUI
+import SwiftData
+
+/// Root entry point: decides between onboarding and main tab view.
+struct RootView: View {
+ @Environment(AppState.self) private var appState
+ @Query private var profiles: [UserProfile]
+
+ private var profile: UserProfile? { profiles.first }
+
+ var body: some View {
+ Group {
+ if let profile, profile.onboardingCompleted {
+ MainTabView()
+ } else {
+ OnboardingView()
+ }
+ }
+ .animation(.easeInOut(duration: 0.3), value: profile?.onboardingCompleted)
+ }
+}
diff --git a/tabatago-swift/TabataGo/App/TabataGoApp.swift b/tabatago-swift/TabataGo/App/TabataGoApp.swift
new file mode 100644
index 0000000..cfdb0f9
--- /dev/null
+++ b/tabatago-swift/TabataGo/App/TabataGoApp.swift
@@ -0,0 +1,19 @@
+import SwiftUI
+import SwiftData
+
+@main
+struct TabataGoApp: App {
+
+ @State private var appState = AppState()
+
+ var body: some Scene {
+ WindowGroup {
+ RootView()
+ .environment(appState)
+ .modelContainer(TabataGoSchema.container)
+ .task {
+ await appState.bootstrap()
+ }
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Models/HealthSnapshot.swift b/tabatago-swift/TabataGo/Models/HealthSnapshot.swift
new file mode 100644
index 0000000..daaafe7
--- /dev/null
+++ b/tabatago-swift/TabataGo/Models/HealthSnapshot.swift
@@ -0,0 +1,32 @@
+import SwiftData
+import Foundation
+
+/// Snapshot of HealthKit data — refreshed when user opens Activity tab.
+@Model
+final class HealthSnapshot: @unchecked Sendable {
+
+ var fetchedAt: Date = Date()
+
+ // Activity rings
+ var activeCaloricBurn: Double = 0 // kcal today
+ var exerciseMinutes: Double = 0 // minutes today
+ var standHours: Int = 0 // hours today
+
+ // Resting metrics
+ var restingHeartRate: Double? = nil // bpm
+ var bodyMassKg: Double? = nil // kg
+
+ // Weekly summary
+ var weeklyActiveCalories: Double = 0
+ var weeklyExerciseMinutes: Double = 0
+ var weeklyWorkoutCount: Int = 0
+
+ // VO2 Max (if available)
+ var vo2Max: Double? = nil
+
+ var isStale: Bool {
+ Date().timeIntervalSince(fetchedAt) > 900 // 15 min TTL
+ }
+
+ init() {}
+}
diff --git a/tabatago-swift/TabataGo/Models/MusicTrack.swift b/tabatago-swift/TabataGo/Models/MusicTrack.swift
new file mode 100644
index 0000000..e38dfae
--- /dev/null
+++ b/tabatago-swift/TabataGo/Models/MusicTrack.swift
@@ -0,0 +1,31 @@
+import Foundation
+
+/// A single music track for workout playback.
+struct MusicTrack: Identifiable, Equatable, Sendable {
+ let id: String
+ let title: String
+ let artist: String
+ let duration: Int // seconds
+ let url: URL
+ let vibe: MusicVibe
+}
+
+/// Workout music mood — maps to genre buckets in the download_items table.
+enum MusicVibe: String, CaseIterable, Sendable {
+ case electronic
+ case hipHop = "hip-hop"
+ case pop
+ case rock
+ case chill
+
+ /// Database genres that map to this vibe (matches Expo VIBE_TO_GENRES).
+ var genres: [String] {
+ switch self {
+ case .electronic: return ["edm", "house", "drum-and-bass", "dubstep"]
+ case .hipHop: return ["hip-hop", "r-and-b"]
+ case .pop: return ["pop", "latin"]
+ case .rock: return ["rock", "metal", "country"]
+ case .chill: return ["ambient"]
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Models/PreviewData.swift b/tabatago-swift/TabataGo/Models/PreviewData.swift
new file mode 100644
index 0000000..8d80fa0
--- /dev/null
+++ b/tabatago-swift/TabataGo/Models/PreviewData.swift
@@ -0,0 +1,111 @@
+import SwiftData
+import Foundation
+
+/// Seed data for SwiftUI previews and unit tests.
+enum PreviewData {
+
+ static func seed(into context: ModelContext) {
+ // User profile
+ let profile = UserProfile()
+ profile.name = "Alex"
+ profile.fitnessLevel = .intermediate
+ profile.goal = .cardio
+ profile.weeklyFrequency = 4
+ profile.onboardingCompleted = true
+ profile.subscription = .premiumMonthly
+ context.insert(profile)
+
+ // Workout history
+ let sessions: [(String, String, String, Int)] = [
+ ("upper-body-beginner", "Upper Body Blast", "upper", 1440),
+ ("full-body-intermediate", "Full Body HIIT", "full", 2040),
+ ("lower-body-beginner", "Lower Body Burn", "lower", 1200),
+ ("upper-body-intermediate", "Arms & Core", "upper", 1680),
+ ("full-body-beginner", "Total Body", "full", 960),
+ ]
+
+ for (i, (id, title, zone, duration)) in sessions.enumerated() {
+ let daysAgo = Double(i * 2)
+ let session = WorkoutSession(
+ programId: id,
+ programTitle: title,
+ bodyZone: zone,
+ level: "Intermediate",
+ startedAt: Date().addingTimeInterval(-(daysAgo * 86400 + Double(duration))),
+ completedAt: Date().addingTimeInterval(-daysAgo * 86400),
+ durationSeconds: duration,
+ caloriesBurned: Double(duration / 10),
+ roundsCompleted: 8,
+ totalRounds: 8
+ )
+ context.insert(session)
+ }
+
+ // Health snapshot
+ let snapshot = HealthSnapshot()
+ snapshot.activeCaloricBurn = 320
+ snapshot.exerciseMinutes = 28
+ snapshot.standHours = 9
+ snapshot.restingHeartRate = 58
+ snapshot.bodyMassKg = 72
+ snapshot.weeklyActiveCalories = 1850
+ snapshot.weeklyExerciseMinutes = 148
+ snapshot.weeklyWorkoutCount = 4
+ context.insert(snapshot)
+
+ try? context.save()
+ }
+
+ /// A single workout program for player previews.
+ static var sampleProgram: WorkoutProgram {
+ WorkoutProgram(
+ id: "upper-body-beginner",
+ title: "Upper Body Blast",
+ titleEn: "Upper Body Blast",
+ description: "Sculpt your upper body with this intense Tabata workout.",
+ descriptionEn: "Sculpt your upper body with this intense Tabata workout.",
+ bodyZone: "upper",
+ level: "Beginner",
+ musicVibe: "electronic",
+ accentColor: "#FF6B35",
+ isFree: true,
+ estimatedCalories: 180,
+ estimatedDuration: 24,
+ totalRounds: 32,
+ warmup: WarmupSection(
+ movements: [
+ TimedMovement(name: "Arm Circles", nameEn: "Arm Circles", duration: 30, videoUrl: nil),
+ TimedMovement(name: "High Knees", nameEn: "High Knees", duration: 30, videoUrl: nil),
+ ],
+ totalDuration: 150
+ ),
+ cooldown: CooldownSection(
+ movements: [
+ TimedMovement(name: "Shoulder Stretch", nameEn: "Shoulder Stretch", duration: 30, videoUrl: nil),
+ ],
+ totalDuration: 90
+ ),
+ blocks: [
+ TabataBlock(
+ id: "block-1",
+ position: 1,
+ exercise1: TabataExercise(name: "Push-ups", nameEn: "Push-ups", tip: "Keep core tight"),
+ exercise2: TabataExercise(name: "Tricep Dips", nameEn: "Tricep Dips"),
+ rounds: 8,
+ workTime: 20,
+ restTime: 10
+ ),
+ TabataBlock(
+ id: "block-2",
+ position: 2,
+ exercise1: TabataExercise(name: "Pike Push-ups", nameEn: "Pike Push-ups"),
+ exercise2: TabataExercise(name: "Plank Shoulder Taps", nameEn: "Plank Shoulder Taps"),
+ rounds: 8,
+ workTime: 20,
+ restTime: 10
+ ),
+ ],
+ thumbnailUrl: nil
+ )
+ }
+}
diff --git a/tabatago-swift/TabataGo/Models/TabataGoSchema.swift b/tabatago-swift/TabataGo/Models/TabataGoSchema.swift
new file mode 100644
index 0000000..898bf9d
--- /dev/null
+++ b/tabatago-swift/TabataGo/Models/TabataGoSchema.swift
@@ -0,0 +1,41 @@
+import SwiftData
+import Foundation
+
+/// Central schema definition + ModelContainer factory.
+enum TabataGoSchema {
+
+ static var schema: Schema {
+ Schema([
+ UserProfile.self,
+ WorkoutSession.self,
+ CachedProgramData.self,
+ HealthSnapshot.self,
+ ])
+ }
+
+ static var container: ModelContainer {
+ let config = ModelConfiguration(
+ schema: schema,
+ isStoredInMemoryOnly: false,
+ allowsSave: true
+ )
+ do {
+ return try ModelContainer(for: schema, configurations: config)
+ } catch {
+ fatalError("Failed to create SwiftData container: \(error)")
+ }
+ }
+
+ /// In-memory container for SwiftUI Previews and unit tests.
+ @MainActor
+ static var previewContainer: ModelContainer {
+ let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
+ guard let container = try? ModelContainer(for: schema, configurations: config) else {
+ // Last-resort: if even a bare in-memory container fails, the schema itself is
+ // broken and we want a clear crash with a useful message rather than a silent hang.
+ fatalError("[TabataGoSchema] Cannot create even a bare in-memory ModelContainer — check your @Model definitions for conflicts.")
+ }
+ PreviewData.seed(into: container.mainContext)
+ return container
+ }
+}
diff --git a/tabatago-swift/TabataGo/Models/UserProfile.swift b/tabatago-swift/TabataGo/Models/UserProfile.swift
new file mode 100644
index 0000000..dc1d2a7
--- /dev/null
+++ b/tabatago-swift/TabataGo/Models/UserProfile.swift
@@ -0,0 +1,123 @@
+import SwiftData
+import Foundation
+
+// ─── Enums ──────────────────────────────────────────────────────
+
+enum FitnessLevel: String, Codable, CaseIterable {
+ case beginner, intermediate, advanced
+
+ var label: String {
+ switch self {
+ case .beginner: "Beginner"
+ case .intermediate: "Intermediate"
+ case .advanced: "Advanced"
+ }
+ }
+}
+
+enum FitnessGoal: String, Codable, CaseIterable {
+ case weightLoss = "weight-loss"
+ case cardio, strength, wellness
+
+ var label: String {
+ switch self {
+ case .weightLoss: "Weight Loss"
+ case .cardio: "Cardio"
+ case .strength: "Strength"
+ case .wellness: "Wellness"
+ }
+ }
+}
+
+enum SubscriptionPlan: String, Codable {
+ case free
+ case premiumMonthly = "premium-monthly"
+ case premiumYearly = "premium-yearly"
+
+ var isPremium: Bool { self != .free }
+}
+
+enum SyncStatus: String, Codable {
+ case neverSynced = "never-synced"
+ case promptPending = "prompt-pending"
+ case synced
+ case failed
+}
+
+// ─── Model ──────────────────────────────────────────────────────
+
+/// Singleton user profile — created once at onboarding, updated in place.
+@Model
+final class UserProfile {
+
+ // Identity
+ var name: String = ""
+ var email: String = ""
+ var joinDate: Date = Date()
+
+ // Fitness preferences
+ var fitnessLevelRaw: String = FitnessLevel.beginner.rawValue
+ var goalRaw: String = FitnessGoal.cardio.rawValue
+ var weeklyFrequency: Int = 3
+ var barriers: [String] = []
+
+ // Subscription
+ var subscriptionRaw: String = SubscriptionPlan.free.rawValue
+
+ // Onboarding
+ var onboardingCompleted: Bool = false
+
+ // Cloud sync
+ var syncStatusRaw: String = SyncStatus.neverSynced.rawValue
+ var supabaseUserId: String? = nil
+
+ // App settings
+ var hapticsEnabled: Bool = true
+ var soundEffectsEnabled: Bool = true
+ var voiceCoachingEnabled: Bool = true
+ var musicEnabled: Bool = true
+ var musicVolume: Double = 0.5
+ var remindersEnabled: Bool = false
+ var reminderTimeHour: Int = 9
+ var reminderTimeMinute: Int = 0
+ var hasPromptedReview: Bool = false
+
+ // Saved workouts
+ var savedWorkoutIds: [String] = []
+
+ // Computed accessors
+ var fitnessLevel: FitnessLevel {
+ get { FitnessLevel(rawValue: fitnessLevelRaw) ?? .beginner }
+ set { fitnessLevelRaw = newValue.rawValue }
+ }
+
+ var goal: FitnessGoal {
+ get { FitnessGoal(rawValue: goalRaw) ?? .cardio }
+ set { goalRaw = newValue.rawValue }
+ }
+
+ var subscription: SubscriptionPlan {
+ get { SubscriptionPlan(rawValue: subscriptionRaw) ?? .free }
+ set { subscriptionRaw = newValue.rawValue }
+ }
+
+ var syncStatus: SyncStatus {
+ get { SyncStatus(rawValue: syncStatusRaw) ?? .neverSynced }
+ set { syncStatusRaw = newValue.rawValue }
+ }
+
+ var reminderTime: DateComponents {
+ get {
+ var c = DateComponents()
+ c.hour = reminderTimeHour
+ c.minute = reminderTimeMinute
+ return c
+ }
+ set {
+ reminderTimeHour = newValue.hour ?? 9
+ reminderTimeMinute = newValue.minute ?? 0
+ }
+ }
+
+ init() {}
+}
diff --git a/tabatago-swift/TabataGo/Models/WorkoutProgram.swift b/tabatago-swift/TabataGo/Models/WorkoutProgram.swift
new file mode 100644
index 0000000..735394c
--- /dev/null
+++ b/tabatago-swift/TabataGo/Models/WorkoutProgram.swift
@@ -0,0 +1,89 @@
+import SwiftData
+import Foundation
+
+// ─── Value types (not persisted, used in memory) ────────────────
+
+struct TabataExercise: Codable, Hashable, Sendable {
+ var name: String
+ var nameEn: String
+ var tip: String?
+ var tipEn: String?
+ var modification: String?
+ var modificationEn: String?
+ var progression: String?
+ var progressionEn: String?
+ var videoUrl: String?
+}
+
+struct TabataBlock: Codable, Hashable, Sendable {
+ var id: String
+ var position: Int
+ var exercise1: TabataExercise
+ var exercise2: TabataExercise
+ var rounds: Int
+ var workTime: Int // seconds
+ var restTime: Int // seconds
+}
+
+struct TimedMovement: Codable, Hashable, Sendable {
+ var name: String
+ var nameEn: String
+ var duration: Int // seconds
+ var videoUrl: String?
+}
+
+struct WarmupSection: Codable, Hashable, Sendable {
+ var movements: [TimedMovement]
+ var totalDuration: Int
+}
+
+struct CooldownSection: Codable, Hashable, Sendable {
+ var movements: [TimedMovement]
+ var totalDuration: Int
+}
+
+/// In-memory representation of a full workout program (decoded from Supabase cache).
+struct WorkoutProgram: Codable, Hashable, Identifiable, Sendable {
+ var id: String
+ var title: String
+ var titleEn: String
+ var description: String
+ var descriptionEn: String
+ var bodyZone: String
+ var level: String
+ var musicVibe: String
+ var accentColor: String
+ var isFree: Bool
+ var estimatedCalories: Int
+ var estimatedDuration: Int // minutes
+ var totalRounds: Int
+ var warmup: WarmupSection
+ var cooldown: CooldownSection
+ var blocks: [TabataBlock]
+ var thumbnailUrl: String?
+}
+
+// ─── SwiftData cache model ───────────────────────────────────────
+
+/// Supabase workout programs cached locally. TTL = 1 hour.
+@Model
+final class CachedProgramData {
+
+ var cacheKey: String = ""
+ var cachedAt: Date = Date()
+ var jsonData: Data = Data()
+
+ var isExpired: Bool {
+ Date().timeIntervalSince(cachedAt) > 3600 // 1 hour TTL
+ }
+
+ init(cacheKey: String, programs: [WorkoutProgram]) throws {
+ self.cacheKey = cacheKey
+ self.cachedAt = Date()
+ self.jsonData = try JSONEncoder().encode(programs)
+ }
+
+ func decode() throws -> [WorkoutProgram] {
+ try JSONDecoder().decode([WorkoutProgram].self, from: jsonData)
+ }
+}
diff --git a/tabatago-swift/TabataGo/Models/WorkoutSession.swift b/tabatago-swift/TabataGo/Models/WorkoutSession.swift
new file mode 100644
index 0000000..2343172
--- /dev/null
+++ b/tabatago-swift/TabataGo/Models/WorkoutSession.swift
@@ -0,0 +1,60 @@
+import SwiftData
+import Foundation
+
+/// One completed Tabata workout session — written to SwiftData + HealthKit.
+@Model
+final class WorkoutSession: @unchecked Sendable {
+
+ var id: UUID = UUID()
+ var programId: String = ""
+ var programTitle: String = ""
+ var bodyZone: String = ""
+ var level: String = ""
+
+ // Timing
+ var startedAt: Date = Date()
+ var completedAt: Date = Date()
+ var durationSeconds: Int = 0
+
+ // Effort
+ var caloriesBurned: Double = 0
+ var roundsCompleted: Int = 0
+ var totalRounds: Int = 0
+
+ /// Completion rate 0..1
+ var completionRate: Double {
+ guard totalRounds > 0 else { return 0 }
+ return Double(roundsCompleted) / Double(totalRounds)
+ }
+
+ // HealthKit link
+ var healthKitWorkoutId: UUID? = nil
+
+ // Heart rate (populated from HKWorkoutSession after completion)
+ var averageHeartRate: Double? = nil
+ var peakHeartRate: Double? = nil
+
+ init(
+ programId: String,
+ programTitle: String,
+ bodyZone: String,
+ level: String,
+ startedAt: Date,
+ completedAt: Date,
+ durationSeconds: Int,
+ caloriesBurned: Double,
+ roundsCompleted: Int,
+ totalRounds: Int
+ ) {
+ self.programId = programId
+ self.programTitle = programTitle
+ self.bodyZone = bodyZone
+ self.level = level
+ self.startedAt = startedAt
+ self.completedAt = completedAt
+ self.durationSeconds = durationSeconds
+ self.caloriesBurned = caloriesBurned
+ self.roundsCompleted = roundsCompleted
+ self.totalRounds = totalRounds
+ }
+}
diff --git a/tabatago-swift/TabataGo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/tabatago-swift/TabataGo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..5dcb929
--- /dev/null
+++ b/tabatago-swift/TabataGo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors": [
+ {
+ "color": {
+ "color-space": "srgb",
+ "components": {
+ "alpha": "1.000",
+ "blue": "0.208",
+ "green": "0.420",
+ "red": "1.000"
+ }
+ },
+ "idiom": "universal"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "color": {
+ "color-space": "srgb",
+ "components": {
+ "alpha": "1.000",
+ "blue": "0.208",
+ "green": "0.420",
+ "red": "1.000"
+ }
+ },
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/tabatago-swift/TabataGo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/tabatago-swift/TabataGo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..b121e3b
--- /dev/null
+++ b/tabatago-swift/TabataGo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images": [
+ {
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/tabatago-swift/TabataGo/Resources/Assets.xcassets/Contents.json b/tabatago-swift/TabataGo/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..74d6a72
--- /dev/null
+++ b/tabatago-swift/TabataGo/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/tabatago-swift/TabataGo/Resources/Info.plist b/tabatago-swift/TabataGo/Resources/Info.plist
new file mode 100644
index 0000000..bb6a0dc
--- /dev/null
+++ b/tabatago-swift/TabataGo/Resources/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ TabataGo
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSHealthShareUsageDescription
+ TabataGo reads your health data to show fitness stats and personalize your workouts.
+ NSHealthUpdateUsageDescription
+ TabataGo saves your Tabata workouts to Apple Health to track calories, heart rate, and contribute to your Activity Rings.
+ NSMotionUsageDescription
+ TabataGo uses motion data to improve calorie estimates during workouts.
+ POSTHOG_API_KEY
+ $(POSTHOG_API_KEY)
+ REVENUECAT_API_KEY
+ $(REVENUECAT_API_KEY)
+ SUPABASE_ANON_KEY
+ $(SUPABASE_ANON_KEY)
+ SUPABASE_URL
+ $(SUPABASE_URL)
+ UILaunchScreen
+
+ UIColorName
+
+ UIImageName
+
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+
+
diff --git a/tabatago-swift/TabataGo/Resources/Localizable.xcstrings b/tabatago-swift/TabataGo/Resources/Localizable.xcstrings
new file mode 100644
index 0000000..14a9bbf
--- /dev/null
+++ b/tabatago-swift/TabataGo/Resources/Localizable.xcstrings
@@ -0,0 +1,2736 @@
+{
+ "sourceLanguage" : "en",
+ "strings" : {
+ "%@" : {
+
+ },
+ "%@ / month" : {
+
+ },
+ "%@ / year — save 40%%" : {
+
+ },
+ "%@ bpm" : {
+
+ },
+ "%@ kcal" : {
+
+ },
+ "%@m" : {
+
+ },
+ "%@x" : {
+
+ },
+ "action.back" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Zurück"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Back"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Volver"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Retour"
+ }
+ }
+ }
+ },
+ "action.cancel" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Abbrechen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cancel"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cancelar"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Annuler"
+ }
+ }
+ }
+ },
+ "action.continue" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Weiter"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Continue"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Continuar"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Continuer"
+ }
+ }
+ }
+ },
+ "action.done" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Fertig"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Done"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Listo"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Terminé"
+ }
+ }
+ }
+ },
+ "action.restorePurchases" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Käufe wiederherstellen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Restore Purchases"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Restaurar compras"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Restaurer les achats"
+ }
+ }
+ }
+ },
+ "action.save" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Speichern"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Save"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Guardar"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Enregistrer"
+ }
+ }
+ }
+ },
+ "action.share" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Teilen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Share"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Compartir"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Partager"
+ }
+ }
+ }
+ },
+ "action.start" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "START"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "START"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "INICIAR"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "COMMENCER"
+ }
+ }
+ }
+ },
+ "action.startTraining" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Training beginnen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Start Training"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Empezar a entrenar"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Commencer l'entraînement"
+ }
+ }
+ }
+ },
+ "action.startWorkout" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Training starten"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Start Workout"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Iniciar entrenamiento"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Commencer l'entraînement"
+ }
+ }
+ }
+ },
+ "action.unlockPremium" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Premium freischalten"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Unlock Premium"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Desbloquear Premium"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Débloquer Premium"
+ }
+ }
+ }
+ },
+ "activity.bestStreak" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Beste Serie"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Best Streak"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Mejor racha"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Meilleure série"
+ }
+ }
+ }
+ },
+ "activity.currentStreak" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Aktuelle Serie"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Current Streak"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Racha actual"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Série actuelle"
+ }
+ }
+ }
+ },
+ "activity.history" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Verlauf"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "History"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Historial"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Historique"
+ }
+ }
+ }
+ },
+ "activity.minutes" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Minuten"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Minutes"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Minutos"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Minutes"
+ }
+ }
+ }
+ },
+ "activity.noWorkouts" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Noch keine Trainings"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "No workouts yet"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Aún sin entrenamientos"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Aucun entraînement"
+ }
+ }
+ }
+ },
+ "activity.noWorkoutsMessage" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Beende dein erstes Tabata, um deine Aktivität zu sehen."
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Complete your first Tabata to see your activity here."
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Completa tu primer Tabata para ver tu actividad aquí."
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Termine ton premier Tabata pour voir ton activité ici."
+ }
+ }
+ }
+ },
+ "activity.workouts" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Trainings"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Workouts"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Entrenamientos"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Entraînements"
+ }
+ }
+ }
+ },
+ "Any challenges?" : {
+
+ },
+ "Apple Health" : {
+
+ },
+ "Back to Home" : {
+
+ },
+ "Best Streak" : {
+
+ },
+ "BEST VALUE" : {
+
+ },
+ "Block %@ of %@" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Block %1$@ of %2$@"
+ }
+ }
+ }
+ },
+ "Cancel anytime. Prices in your local currency." : {
+
+ },
+ "Complete your first Tabata to see your activity here." : {
+
+ },
+ "complete.avgHeartRate" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ø Herzfrequenz"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Avg Heart Rate"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "FC media"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "FC moyenne"
+ }
+ }
+ }
+ },
+ "complete.backToHome" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Zur Startseite"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Back to Home"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Volver al inicio"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Retour à l'accueil"
+ }
+ }
+ }
+ },
+ "complete.calories" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Kalorien"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Calories"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Calorías"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Calories"
+ }
+ }
+ }
+ },
+ "complete.duration" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Dauer"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Duration"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Duración"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Durée"
+ }
+ }
+ }
+ },
+ "complete.rounds" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Runden"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rounds"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rondas"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rounds"
+ }
+ }
+ }
+ },
+ "complete.savedToHealth" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "In Apple Health gespeichert"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Saved to Apple Health"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Guardado en Apple Salud"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Enregistré dans Santé"
+ }
+ }
+ }
+ },
+ "complete.saveToHealth" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "In Apple Health speichern"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Save to Apple Health"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Guardar en Apple Salud"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Enregistrer dans Santé"
+ }
+ }
+ }
+ },
+ "complete.shareWorkout" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Training teilen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Share Workout"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Compartir entrenamiento"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Partager l'entraînement"
+ }
+ }
+ }
+ },
+ "complete.title" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Training abgeschlossen!"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Workout Complete!"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¡Entrenamiento completado!"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Entraînement terminé !"
+ }
+ }
+ }
+ },
+ "Current Streak" : {
+
+ },
+ "days" : {
+
+ },
+ "Explore" : {
+
+ },
+ "Failed to load programs" : {
+
+ },
+ "For privacy concerns, contact us at privacy@tabatago.app" : {
+
+ },
+ "FREE" : {
+
+ },
+ "goal.cardio" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cardio"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cardio"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cardio"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cardio"
+ }
+ }
+ }
+ },
+ "goal.strength" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Kraft"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Strength"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Fuerza"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Force"
+ }
+ }
+ }
+ },
+ "goal.weightLoss" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Gewichtsverlust"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Weight Loss"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Pérdida de peso"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Perte de poids"
+ }
+ }
+ }
+ },
+ "goal.wellness" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wohlbefinden"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wellness"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Bienestar"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Bien-être"
+ }
+ }
+ }
+ },
+ "health.appleHealth" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Apple Health"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Apple Health"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Apple Salud"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Santé Apple"
+ }
+ }
+ }
+ },
+ "health.exercise" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Training"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Exercise"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ejercicio"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Exercice"
+ }
+ }
+ }
+ },
+ "health.move" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Bewegen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Move"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Mover"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Bouger"
+ }
+ }
+ }
+ },
+ "health.restingHR" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ruhepuls"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Resting HR"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "FC en reposo"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "FC repos"
+ }
+ }
+ }
+ },
+ "health.stand" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Stehen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Stand"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "De pie"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Debout"
+ }
+ }
+ }
+ },
+ "Hey, %@! 👋" : {
+
+ },
+ "High-intensity Tabata workouts,\ndesigned for real results." : {
+
+ },
+ "History" : {
+
+ },
+ "home.allTime" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Gesamt"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "All Time"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Total"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Total"
+ }
+ }
+ }
+ },
+ "home.browseTitle" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Nach Zone"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Browse by Zone"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Explorar por zona"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Par zone musculaire"
+ }
+ }
+ }
+ },
+ "home.featuredSubtitle" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Für dich ausgewählt"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Handpicked for you"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Seleccionados para ti"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Sélectionnés pour vous"
+ }
+ }
+ }
+ },
+ "home.featuredTitle" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Empfohlen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Featured"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Destacados"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "À la une"
+ }
+ }
+ }
+ },
+ "home.streak" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Serie"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Streak"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Racha"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Série"
+ }
+ }
+ }
+ },
+ "home.thisWeek" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Diese Woche"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "This Week"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Esta semana"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cette semaine"
+ }
+ }
+ }
+ },
+ "Joined" : {
+
+ },
+ "Joined %@" : {
+
+ },
+ "level.advanced" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Profi"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Advanced"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Avanzado"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Avancé"
+ }
+ }
+ }
+ },
+ "level.beginner" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Anfänger"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Beginner"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Principiante"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Débutant"
+ }
+ }
+ }
+ },
+ "level.intermediate" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Fortgeschritten"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Intermediate"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Intermedio"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Intermédiaire"
+ }
+ }
+ }
+ },
+ "music.chill" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Chill"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Chill"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Relajado"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Chill"
+ }
+ }
+ }
+ },
+ "music.electronic" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Electronic"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Electronic"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Electrónica"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Électronique"
+ }
+ }
+ }
+ },
+ "music.hipHop" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Hip-Hop"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Hip-Hop"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Hip-Hop"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Hip-Hop"
+ }
+ }
+ }
+ },
+ "music.rock" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rock"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rock"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rock"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rock"
+ }
+ }
+ }
+ },
+ "Name" : {
+
+ },
+ "No workouts yet" : {
+
+ },
+ "onboarding.allSet" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Alles bereit"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "You're all set"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¡Todo listo"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Tout est prêt"
+ }
+ }
+ }
+ },
+ "onboarding.fitnessLevel" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wie ist dein Fitnesslevel?"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "What's your fitness level?"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¿Cuál es tu nivel de forma física?"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Quel est ton niveau de forme ?"
+ }
+ }
+ }
+ },
+ "onboarding.howOften" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wie oft kannst du trainieren?"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "How often can you train?"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¿Con qué frecuencia puedes entrenar?"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "À quelle fréquence peux-tu t'entraîner ?"
+ }
+ }
+ }
+ },
+ "onboarding.mainGoal" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Was ist dein Hauptziel?"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "What's your main goal?"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¿Cuál es tu objetivo principal?"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Quel est ton objectif principal ?"
+ }
+ }
+ }
+ },
+ "onboarding.whatIsYourName" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wie heißt du?"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "What's your name?"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¿Cómo te llamas?"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Quel est ton prénom ?"
+ }
+ }
+ }
+ },
+ "Optional — helps us personalise tips" : {
+
+ },
+ "paywall.cancelAnytime" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Jederzeit kündbar. Preise in Ihrer Landeswährung."
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cancel anytime. Prices in your local currency."
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cancela cuando quieras. Precios en tu moneda local."
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Annulez à tout moment. Prix dans votre devise locale."
+ }
+ }
+ }
+ },
+ "paywall.premiumActive" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Premium aktiv"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Premium Active"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Premium activo"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Premium actif"
+ }
+ }
+ }
+ },
+ "paywall.startPremium" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Premium starten"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Start Premium"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Iniciar Premium"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Démarrer Premium"
+ }
+ }
+ }
+ },
+ "paywall.subtitle" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Alle Workouts, jede Woche freischalten."
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Unlock every workout, every week."
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Desbloquea todos los entrenamientos cada semana."
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Débloquez tous les entraînements, chaque semaine."
+ }
+ }
+ }
+ },
+ "paywall.title" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "TabataGo Premium"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "TabataGo Premium"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "TabataGo Premium"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "TabataGo Premium"
+ }
+ }
+ }
+ },
+ "paywall.upgradePrompt" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Auf Premium upgraden"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Upgrade to Premium"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Actualizar a Premium"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Passer à Premium"
+ }
+ }
+ }
+ },
+ "per week" : {
+
+ },
+ "player.blockOf" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Block %1$lld von %2$lld"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Block %1$lld of %2$lld"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Bloque %1$lld de %2$lld"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Bloc %1$lld sur %2$lld"
+ }
+ }
+ }
+ },
+ "player.endWorkout" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Training beenden?"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "End Workout?"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¿Terminar entrenamiento?"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Terminer l'entraînement ?"
+ }
+ }
+ }
+ },
+ "player.endWorkoutMessage" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Dein Fortschritt wird nicht gespeichert."
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Your progress will not be saved."
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Tu progreso no se guardará."
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ta progression ne sera pas sauvegardée."
+ }
+ }
+ }
+ },
+ "player.keepGoing" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Weitermachen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Keep Going"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Seguir"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Continuer"
+ }
+ }
+ }
+ },
+ "player.phase.break" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "BLOCK-PAUSE"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "BREAK"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "PAUSA"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "PAUSE"
+ }
+ }
+ }
+ },
+ "player.phase.coolDown" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "ABKÜHLEN"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "COOL DOWN"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "ENFRIAMIENTO"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "RÉCUPÉRATION"
+ }
+ }
+ }
+ },
+ "player.phase.done" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "FERTIG"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "DONE"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¡LISTO!"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "TERMINÉ"
+ }
+ }
+ }
+ },
+ "player.phase.getReady" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "BEREIT MACHEN"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "GET READY"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "PREPÁRATE"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "PRÊT"
+ }
+ }
+ }
+ },
+ "player.phase.rest" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "PAUSE"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "REST"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "DESCANSO"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "REPOS"
+ }
+ }
+ }
+ },
+ "player.phase.warmUp" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "AUFWÄRMEN"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "WARM UP"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "CALENTAMIENTO"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "ÉCHAUFFEMENT"
+ }
+ }
+ }
+ },
+ "player.phase.work" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "ARBEIT"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "WORK"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "TRABAJO"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "EFFORT"
+ }
+ }
+ }
+ },
+ "Premium subscriptions are billed monthly or yearly through the App Store. Subscriptions automatically renew unless cancelled at least 24 hours before the renewal date." : {
+
+ },
+ "Programs for %@ are coming soon." : {
+
+ },
+ "Resting HR" : {
+
+ },
+ "Restore Purchases" : {
+
+ },
+ "Saved to Apple Health" : {
+
+ },
+ "settings.audio" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Audio"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Audio"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Audio"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Audio"
+ }
+ }
+ }
+ },
+ "settings.dailyReminder" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Tägliche Erinnerung"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Daily Reminder"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Recordatorio diario"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rappel quotidien"
+ }
+ }
+ }
+ },
+ "settings.hapticFeedback" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Haptisches Feedback"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Haptic Feedback"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Retroalimentación háptica"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Retour haptique"
+ }
+ }
+ }
+ },
+ "settings.haptics" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Haptik"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Haptics"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Hápticos"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Haptique"
+ }
+ }
+ }
+ },
+ "settings.music" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Musik"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Music"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Música"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Musique"
+ }
+ }
+ }
+ },
+ "settings.reminders" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Erinnerungen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Reminders"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Recordatorios"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Rappels"
+ }
+ }
+ }
+ },
+ "settings.resetProgress" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Fortschritt zurücksetzen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Reset All Progress"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Reiniciar todo el progreso"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Réinitialiser la progression"
+ }
+ }
+ }
+ },
+ "settings.soundEffects" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Soundeffekte"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Sound Effects"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Efectos de sonido"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Effets sonores"
+ }
+ }
+ }
+ },
+ "settings.title" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Einstellungen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Settings"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ajustes"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Paramètres"
+ }
+ }
+ }
+ },
+ "settings.voiceCoaching" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Sprachcoaching"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Voice Coaching"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Coaching de voz"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Coach vocal"
+ }
+ }
+ }
+ },
+ "Subscription purchases are handled by Apple's App Store and RevenueCat. We do not store your payment information." : {
+
+ },
+ "tab.activity" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Aktivität"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Activity"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Actividad"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Activité"
+ }
+ }
+ }
+ },
+ "tab.home" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Startseite"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Home"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Inicio"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Accueil"
+ }
+ }
+ }
+ },
+ "tab.profile" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Profil"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Profile"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Perfil"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Profil"
+ }
+ }
+ }
+ },
+ "tab.programs" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Programme"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Programs"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Programas"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Programmes"
+ }
+ }
+ }
+ },
+ "TabataGo" : {
+
+ },
+ "TabataGo collects minimal data to provide you with a great workout experience. We collect your name, fitness preferences, and workout history locally on your device." : {
+
+ },
+ "TabataGo is designed for fitness purposes. By using the app, you agree to use it responsibly and consult a healthcare professional before starting any new exercise program." : {
+
+ },
+ "TabataGo is not a medical device. The app does not provide medical advice. Always consult a doctor before beginning a new exercise program, especially if you have pre-existing health conditions." : {
+
+ },
+ "TabataGo is provided 'as is'. We are not liable for any injuries or health issues arising from the use of our workout programs." : {
+
+ },
+ "TabataGo Premium" : {
+
+ },
+ "This Week" : {
+
+ },
+ "This will permanently delete your workout history and streak. This cannot be undone." : {
+
+ },
+ "Today" : {
+
+ },
+ "Try changing your filters." : {
+
+ },
+ "Unlock every workout, every week." : {
+
+ },
+ "Version" : {
+
+ },
+ "We may update these terms at any time. Continued use of the app after changes constitutes acceptance of the new terms." : {
+
+ },
+ "We use PostHog to collect anonymised usage analytics to improve the app. No personally identifiable information is sent. You can opt out in your device privacy settings." : {
+
+ },
+ "When you grant permission, TabataGo saves your Tabata workouts to Apple Health, including calories burned, heart rate, and workout duration. This data stays on your device and is governed by Apple's privacy policies." : {
+
+ },
+ "Workout Complete!" : {
+
+ },
+ "You're all set, %@!" : {
+
+ },
+ "You're all set!" : {
+
+ },
+ "Your personalised Tabata plan is ready." : {
+
+ },
+ "Your progress will not be saved." : {
+
+ },
+ "Your workout history, profile, and settings are stored locally using SwiftData. If you enable cloud sync, data is securely stored in Supabase with industry-standard encryption." : {
+
+ },
+ "zone.full" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ganzkörper"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Full Body"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cuerpo completo"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Corps entier"
+ }
+ }
+ }
+ },
+ "zone.lower" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Unterkörper"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Lower Body"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Parte inferior"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Bas du corps"
+ }
+ }
+ }
+ },
+ "zone.upper" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Oberkörper"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Upper Body"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Parte superior"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Haut du corps"
+ }
+ }
+ }
+ }
+ },
+ "version" : "1.0"
+}
\ No newline at end of file
diff --git a/tabatago-swift/TabataGo/Resources/TabataGo.entitlements b/tabatago-swift/TabataGo/Resources/TabataGo.entitlements
new file mode 100644
index 0000000..09890e8
--- /dev/null
+++ b/tabatago-swift/TabataGo/Resources/TabataGo.entitlements
@@ -0,0 +1,16 @@
+
+
+
+
+ com.apple.developer.healthkit
+
+ com.apple.developer.healthkit.access
+
+ health-records
+
+ com.apple.security.application-groups
+
+ group.com.tabatago.app
+
+
+
diff --git a/tabatago-swift/TabataGo/Services/AnalyticsService.swift b/tabatago-swift/TabataGo/Services/AnalyticsService.swift
new file mode 100644
index 0000000..bd04448
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/AnalyticsService.swift
@@ -0,0 +1,105 @@
+import Foundation
+import PostHog
+
+/// PostHog analytics — mirrors the event taxonomy from the Expo app.
+final class AnalyticsService: @unchecked Sendable {
+
+ static let shared = AnalyticsService()
+
+ private let apiKey: String =
+ Bundle.main.infoDictionary?["POSTHOG_API_KEY"] as? String ?? ""
+
+ private let host = "https://eu.posthog.com"
+
+ private init() {}
+
+ func initialize() {
+ guard !apiKey.isEmpty else { return }
+ let config = PostHogConfig(apiKey: apiKey, host: host)
+ config.captureApplicationLifecycleEvents = true
+ config.captureScreenViews = false // manual tracking
+ PostHogSDK.shared.setup(config)
+ }
+
+ // ─── Screens ────────────────────────────────────────────────
+
+ func screen(_ name: String, properties: [String: Any] = [:]) {
+ PostHogSDK.shared.screen(name, properties: properties)
+ }
+
+ // ─── Onboarding ─────────────────────────────────────────────
+
+ func onboardingCompleted(name: String, level: String, goal: String, frequency: Int) {
+ capture("onboarding_completed", properties: [
+ "fitness_level": level,
+ "goal": goal,
+ "weekly_frequency": frequency,
+ ])
+ }
+
+ // ─── Workouts ────────────────────────────────────────────────
+
+ func workoutStarted(programId: String, programTitle: String, bodyZone: String, level: String) {
+ capture("workout_started", properties: [
+ "program_id": programId,
+ "program_title": programTitle,
+ "body_zone": bodyZone,
+ "level": level,
+ ])
+ }
+
+ func workoutCompleted(
+ programId: String,
+ durationSeconds: Int,
+ calories: Double,
+ completionRate: Double,
+ healthKitSaved: Bool
+ ) {
+ capture("workout_completed", properties: [
+ "program_id": programId,
+ "duration_seconds": durationSeconds,
+ "calories": calories,
+ "completion_rate": completionRate,
+ "healthkit_saved": healthKitSaved,
+ ])
+ }
+
+ func workoutAbandoned(programId: String, atRound: Int, totalRounds: Int) {
+ capture("workout_abandoned", properties: [
+ "program_id": programId,
+ "at_round": atRound,
+ "total_rounds": totalRounds,
+ "completion_rate": Double(atRound) / Double(max(totalRounds, 1)),
+ ])
+ }
+
+ // ─── Paywall ─────────────────────────────────────────────────
+
+ func paywallViewed(source: String) {
+ capture("paywall_viewed", properties: ["source": source])
+ }
+
+ func subscriptionStarted(plan: String) {
+ capture("subscription_started", properties: ["plan": plan])
+ }
+
+ func subscriptionRestored() {
+ capture("subscription_restored")
+ }
+
+ // ─── HealthKit ───────────────────────────────────────────────
+
+ func healthKitPermissionGranted() {
+ capture("healthkit_permission_granted")
+ }
+
+ func healthKitPermissionDenied() {
+ capture("healthkit_permission_denied")
+ }
+
+ // ─── Private ─────────────────────────────────────────────────
+
+ private func capture(_ event: String, properties: [String: Any] = [:]) {
+ PostHogSDK.shared.capture(event, properties: properties.isEmpty ? nil : properties)
+ }
+}
diff --git a/tabatago-swift/TabataGo/Services/AudioService.swift b/tabatago-swift/TabataGo/Services/AudioService.swift
new file mode 100644
index 0000000..0b053a3
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/AudioService.swift
@@ -0,0 +1,147 @@
+import Foundation
+import AVFoundation
+
+/// Manages all workout audio: timer beeps, phase cues, voice coaching, music.
+@MainActor
+@Observable
+final class AudioService {
+
+ static let shared = AudioService()
+
+ var isMusicEnabled = true
+ var musicVolume: Float = 0.5
+ var isVoiceCoachingEnabled = true
+ var isSoundEffectsEnabled = true
+
+ private var audioSession: AVAudioSession { AVAudioSession.sharedInstance() }
+ private var beepPlayer: AVAudioPlayer?
+ private var speechSynthesizer = AVSpeechSynthesizer()
+
+ private init() {
+ configureSession()
+ }
+
+ // ─── Session Setup ────────────────────────────────────────────
+
+ private func configureSession() {
+ do {
+ try audioSession.setCategory(
+ .playback,
+ mode: .default,
+ options: [.mixWithOthers, .allowAirPlay, .allowBluetooth]
+ )
+ try audioSession.setActive(true)
+ } catch {
+ print("[Audio] Session configuration failed: \(error)")
+ }
+ }
+
+ // ─── Phase Audio Cues ─────────────────────────────────────────
+
+ func playPhaseStart(_ phase: TimerPhase) {
+ guard isSoundEffectsEnabled else { return }
+ switch phase {
+ case .warmup: playTone(frequency: 523, duration: 0.15, count: 2)
+ case .work: playTone(frequency: 880, duration: 0.2, count: 3)
+ case .rest: playTone(frequency: 440, duration: 0.3, count: 1)
+ case .interBlockRest: playTone(frequency: 392, duration: 0.4, count: 2)
+ case .cooldown: playTone(frequency: 660, duration: 0.2, count: 2)
+ case .complete: playTone(frequency: 880, duration: 0.15, count: 5)
+ case .prep: break
+ }
+ }
+
+ func playCountdown(secondsLeft: Int) {
+ guard isSoundEffectsEnabled, secondsLeft <= 3, secondsLeft > 0 else { return }
+ playTone(frequency: secondsLeft == 1 ? 880 : 660, duration: 0.08, count: 1)
+ }
+
+ // ─── Voice Coaching ───────────────────────────────────────────
+
+ func speak(_ text: String, locale: String = "en-US") {
+ guard isVoiceCoachingEnabled else { return }
+ speechSynthesizer.stopSpeaking(at: .immediate)
+ let utterance = AVSpeechUtterance(string: text)
+ utterance.voice = AVSpeechSynthesisVoice(language: locale)
+ utterance.rate = 0.52
+ utterance.pitchMultiplier = 1.0
+ utterance.volume = 0.9
+ speechSynthesizer.speak(utterance)
+ }
+
+ func announceExercise(_ exercise: TabataExercise) {
+ speak(exercise.nameEn)
+ }
+
+ func announcePhase(_ phase: TimerPhase) {
+ guard isVoiceCoachingEnabled else { return }
+ let text: String
+ switch phase {
+ case .prep: text = "Get ready"
+ case .warmup: text = "Warm up"
+ case .work: text = "Work"
+ case .rest: text = "Rest"
+ case .interBlockRest: text = "Take a break"
+ case .cooldown: text = "Cool down"
+ case .complete: text = "Workout complete. Great job!"
+ }
+ speak(text)
+ }
+
+ // ─── Programmatic Tone Generation ────────────────────────────
+
+ private func playTone(frequency: Double, duration: Double, count: Int) {
+ guard let url = generateToneURL(frequency: frequency, duration: duration) else { return }
+ Task {
+ for i in 0.. 0 { try? await Task.sleep(for: .milliseconds(Int(duration * 1000) + 50)) }
+ try? beepPlayer = AVAudioPlayer(contentsOf: url)
+ beepPlayer?.volume = 0.8
+ beepPlayer?.play()
+ }
+ }
+ }
+
+ private func generateToneURL(frequency: Double, duration: Double) -> URL? {
+ let sampleRate = 44100.0
+ let numSamples = Int(sampleRate * duration)
+ var samples = [Int16](repeating: 0, count: numSamples)
+ let fadeFrames = Int(sampleRate * 0.005) // 5ms fade
+
+ for i in 0.. numSamples - fadeFrames { amplitude *= Double(numSamples - i) / Double(fadeFrames) }
+ samples[i] = Int16(amplitude * Double(Int16.max) * sin(angle))
+ }
+
+ let url = FileManager.default.temporaryDirectory
+ .appendingPathComponent("tabata_tone_\(Int(frequency)).wav")
+
+ let headerSize = 44
+ var data = Data(count: headerSize + numSamples * 2)
+ data.withUnsafeMutableBytes { ptr in
+ let raw = ptr.baseAddress!
+ // RIFF header
+ "RIFF".utf8.enumerated().forEach { raw.storeBytes(of: $0.element, toByteOffset: $0.offset, as: UInt8.self) }
+ let chunkSize = UInt32(36 + numSamples * 2).littleEndian
+ withUnsafeBytes(of: chunkSize) { raw.advanced(by: 4).copyMemory(from: $0.baseAddress!, byteCount: 4) }
+ "WAVE".utf8.enumerated().forEach { raw.storeBytes(of: $0.element, toByteOffset: 8 + $0.offset, as: UInt8.self) }
+ "fmt ".utf8.enumerated().forEach { raw.storeBytes(of: $0.element, toByteOffset: 12 + $0.offset, as: UInt8.self) }
+ let fmtSize = UInt32(16).littleEndian; withUnsafeBytes(of: fmtSize) { raw.advanced(by: 16).copyMemory(from: $0.baseAddress!, byteCount: 4) }
+ let audioFmt = UInt16(1).littleEndian; withUnsafeBytes(of: audioFmt) { raw.advanced(by: 20).copyMemory(from: $0.baseAddress!, byteCount: 2) }
+ let channels = UInt16(1).littleEndian; withUnsafeBytes(of: channels) { raw.advanced(by: 22).copyMemory(from: $0.baseAddress!, byteCount: 2) }
+ let sr = UInt32(sampleRate).littleEndian; withUnsafeBytes(of: sr) { raw.advanced(by: 24).copyMemory(from: $0.baseAddress!, byteCount: 4) }
+ let byteRate = UInt32(sampleRate * 2).littleEndian; withUnsafeBytes(of: byteRate) { raw.advanced(by: 28).copyMemory(from: $0.baseAddress!, byteCount: 4) }
+ let blockAlign = UInt16(2).littleEndian; withUnsafeBytes(of: blockAlign) { raw.advanced(by: 32).copyMemory(from: $0.baseAddress!, byteCount: 2) }
+ let bitsPerSample = UInt16(16).littleEndian; withUnsafeBytes(of: bitsPerSample) { raw.advanced(by: 34).copyMemory(from: $0.baseAddress!, byteCount: 2) }
+ "data".utf8.enumerated().forEach { raw.storeBytes(of: $0.element, toByteOffset: 36 + $0.offset, as: UInt8.self) }
+ let dataSize = UInt32(numSamples * 2).littleEndian; withUnsafeBytes(of: dataSize) { raw.advanced(by: 40).copyMemory(from: $0.baseAddress!, byteCount: 4) }
+ samples.withUnsafeBytes { raw.advanced(by: 44).copyMemory(from: $0.baseAddress!, byteCount: numSamples * 2) }
+ }
+
+ try? data.write(to: url)
+ return url
+ }
+}
diff --git a/tabatago-swift/TabataGo/Services/HealthKitService.swift b/tabatago-swift/TabataGo/Services/HealthKitService.swift
new file mode 100644
index 0000000..bda8dd3
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/HealthKitService.swift
@@ -0,0 +1,380 @@
+import Foundation
+import HealthKit
+
+// NSPredicate (returned by HKQuery.predicateForSamples) is not Sendable,
+// but it is documented as thread-safe. Suppress the check globally here.
+extension NSPredicate: @unchecked Sendable {}
+
+// ─── Shared HealthKit store ───────────────────────────────────────
+
+/// Full HealthKit integration: read rings/HR/weight, write workouts, live session.
+actor HealthKitService {
+
+ static let shared = HealthKitService()
+ private let store = HKHealthStore()
+
+ // ─── Permission types ────────────────────────────────────────
+
+ private var writeTypes: Set {
+ [
+ HKWorkoutType.workoutType(),
+ HKQuantityType(.activeEnergyBurned),
+ HKQuantityType(.heartRate),
+ ]
+ }
+
+ private var readTypes: Set {
+ [
+ HKWorkoutType.workoutType(),
+ HKQuantityType(.activeEnergyBurned),
+ HKQuantityType(.restingHeartRate),
+ HKQuantityType(.heartRate),
+ HKQuantityType(.bodyMass),
+ HKQuantityType(.vo2Max),
+ HKQuantityType(.appleExerciseTime),
+ HKQuantityType(.appleStandTime),
+ HKCategoryType(.appleStandHour),
+ HKActivitySummaryType.activitySummaryType(),
+ ]
+ }
+
+ // ─── Authorization ────────────────────────────────────────────
+
+ nonisolated var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() }
+
+ func requestAuthorization() async throws {
+ guard isAvailable else { return }
+ try await store.requestAuthorization(toShare: writeTypes, read: readTypes)
+ }
+
+ var isAuthorized: Bool {
+ get async {
+ guard isAvailable else { return false }
+ let status = store.authorizationStatus(for: HKWorkoutType.workoutType())
+ return status == .sharingAuthorized
+ }
+ }
+
+ // ─── Save a completed workout ─────────────────────────────────
+
+ /// Plain Sendable snapshot of a WorkoutSession — safe to cross actor boundaries.
+ struct WorkoutSaveData: Sendable {
+ let startedAt: Date
+ let completedAt: Date
+ let caloriesBurned: Double
+ let averageHeartRate: Double?
+ }
+
+ @discardableResult
+ func saveWorkout(_ data: WorkoutSaveData) async throws -> HKWorkout {
+ let config = HKWorkoutConfiguration()
+ config.activityType = .highIntensityIntervalTraining
+ config.locationType = .indoor
+
+ let builder = HKWorkoutBuilder(healthStore: store, configuration: config, device: .local())
+ try await builder.beginCollection(at: data.startedAt)
+
+ // Active energy samples
+ if data.caloriesBurned > 0 {
+ let energyType = HKQuantityType(.activeEnergyBurned)
+ let energy = HKQuantity(unit: .kilocalorie(), doubleValue: data.caloriesBurned)
+ let sample = HKQuantitySample(
+ type: energyType,
+ quantity: energy,
+ start: data.startedAt,
+ end: data.completedAt
+ )
+ try await builder.addSamples([sample])
+ }
+
+ // Heart rate samples (if captured during workout)
+ if let avgHR = data.averageHeartRate {
+ let hrType = HKQuantityType(.heartRate)
+ let hrUnit = HKUnit.count().unitDivided(by: .minute())
+ let hrQuantity = HKQuantity(unit: hrUnit, doubleValue: avgHR)
+ let hrSample = HKQuantitySample(
+ type: hrType,
+ quantity: hrQuantity,
+ start: data.startedAt,
+ end: data.completedAt
+ )
+ try await builder.addSamples([hrSample])
+ }
+
+ try await builder.endCollection(at: data.completedAt)
+ guard let workout = try await builder.finishWorkout() else {
+ throw HealthKitError.workoutSaveFailed
+ }
+ return workout
+ }
+
+ // ─── Read: Health Snapshot ─────────────────────────────────────
+
+ func fetchSnapshot() async throws -> HealthSnapshot {
+ let snapshot = HealthSnapshot()
+ snapshot.fetchedAt = Date()
+
+ async let activeCalories = fetchTodayQuantity(type: .activeEnergyBurned, unit: .kilocalorie())
+ async let exerciseMinutes = fetchTodayQuantity(type: .appleExerciseTime, unit: .minute())
+ async let restingHR = fetchMostRecent(type: .restingHeartRate, unit: HKUnit.count().unitDivided(by: .minute()))
+ async let bodyMass = fetchMostRecent(type: .bodyMass, unit: .gramUnit(with: .kilo))
+ async let vo2Max = fetchMostRecent(type: .vo2Max, unit: HKUnit(from: "ml/kg·min"))
+ async let standHours = fetchTodayStandHours()
+ async let weekly = fetchWeeklySummary()
+
+ snapshot.activeCaloricBurn = (try? await activeCalories) ?? 0
+ snapshot.exerciseMinutes = (try? await exerciseMinutes) ?? 0
+ snapshot.restingHeartRate = try? await restingHR
+ snapshot.bodyMassKg = try? await bodyMass
+ snapshot.vo2Max = try? await vo2Max
+ snapshot.standHours = (try? await standHours) ?? 0
+
+ let weeklySummary = try? await weekly
+ snapshot.weeklyActiveCalories = weeklySummary?.calories ?? 0
+ snapshot.weeklyExerciseMinutes = weeklySummary?.exerciseMinutes ?? 0
+ snapshot.weeklyWorkoutCount = weeklySummary?.workoutCount ?? 0
+
+ return snapshot
+ }
+
+ // ─── Private helpers ──────────────────────────────────────────
+
+ private func fetchTodayQuantity(type identifier: HKQuantityTypeIdentifier, unit: HKUnit) async throws -> Double {
+ let quantityType = HKQuantityType(identifier)
+ let now = Date()
+ let startOfDay = Calendar.current.startOfDay(for: now)
+ let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now)
+
+ return try await withCheckedThrowingContinuation { continuation in
+ let query = HKStatisticsQuery(
+ quantityType: quantityType,
+ quantitySamplePredicate: predicate,
+ options: .cumulativeSum
+ ) { _, result, error in
+ if let error { continuation.resume(throwing: error); return }
+ let value = result?.sumQuantity()?.doubleValue(for: unit) ?? 0
+ continuation.resume(returning: value)
+ }
+ store.execute(query)
+ }
+ }
+
+ private func fetchMostRecent(type identifier: HKQuantityTypeIdentifier, unit: HKUnit) async throws -> Double? {
+ let quantityType = HKQuantityType(identifier)
+ let predicate = HKQuery.predicateForSamples(withStart: .distantPast, end: Date())
+ let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
+
+ return try await withCheckedThrowingContinuation { continuation in
+ let query = HKSampleQuery(
+ sampleType: quantityType,
+ predicate: predicate,
+ limit: 1,
+ sortDescriptors: [sortDescriptor]
+ ) { _, samples, error in
+ if let error { continuation.resume(throwing: error); return }
+ guard let sample = samples?.first as? HKQuantitySample else {
+ continuation.resume(returning: nil); return
+ }
+ continuation.resume(returning: sample.quantity.doubleValue(for: unit))
+ }
+ store.execute(query)
+ }
+ }
+
+ private func fetchTodayStandHours() async throws -> Int {
+ let standType = HKCategoryType(.appleStandHour)
+ let now = Date()
+ let startOfDay = Calendar.current.startOfDay(for: now)
+ let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now)
+
+ return try await withCheckedThrowingContinuation { continuation in
+ let query = HKSampleQuery(
+ sampleType: standType,
+ predicate: predicate,
+ limit: HKObjectQueryNoLimit,
+ sortDescriptors: nil
+ ) { _, samples, error in
+ if let error { continuation.resume(throwing: error); return }
+ let stood = samples?.compactMap { $0 as? HKCategorySample }
+ .filter { $0.value == HKCategoryValueAppleStandHour.stood.rawValue }
+ .count ?? 0
+ continuation.resume(returning: stood)
+ }
+ store.execute(query)
+ }
+ }
+
+ struct WeeklySummary {
+ var calories: Double
+ var exerciseMinutes: Double
+ var workoutCount: Int
+ }
+
+ private func fetchWeeklySummary() async throws -> WeeklySummary {
+ let now = Date()
+ let weekAgo = Calendar.current.date(byAdding: .day, value: -7, to: now)!
+ // Capture Sendable Date values; create NSPredicate inside each closure
+ // to avoid sending non-Sendable NSPredicate across actor boundaries.
+ let start = weekAgo
+ let end = now
+
+ async let calories = withCheckedThrowingContinuation { (c: CheckedContinuation) in
+ let query = HKStatisticsQuery(
+ quantityType: HKQuantityType(.activeEnergyBurned),
+ quantitySamplePredicate: HKQuery.predicateForSamples(withStart: start, end: end),
+ options: .cumulativeSum
+ ) { _, result, error in
+ if let error { c.resume(throwing: error); return }
+ c.resume(returning: result?.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0)
+ }
+ store.execute(query)
+ }
+
+ async let exerciseMinutes = withCheckedThrowingContinuation { (c: CheckedContinuation) in
+ let query = HKStatisticsQuery(
+ quantityType: HKQuantityType(.appleExerciseTime),
+ quantitySamplePredicate: HKQuery.predicateForSamples(withStart: start, end: end),
+ options: .cumulativeSum
+ ) { _, result, error in
+ if let error { c.resume(throwing: error); return }
+ c.resume(returning: result?.sumQuantity()?.doubleValue(for: .minute()) ?? 0)
+ }
+ store.execute(query)
+ }
+
+ async let workoutCount = withCheckedThrowingContinuation { (c: CheckedContinuation) in
+ let query = HKSampleQuery(
+ sampleType: HKWorkoutType.workoutType(),
+ predicate: HKQuery.predicateForSamples(withStart: start, end: end),
+ limit: HKObjectQueryNoLimit,
+ sortDescriptors: nil
+ ) { _, samples, error in
+ if let error { c.resume(throwing: error); return }
+ c.resume(returning: samples?.count ?? 0)
+ }
+ store.execute(query)
+ }
+
+ return try await WeeklySummary(
+ calories: calories,
+ exerciseMinutes: exerciseMinutes,
+ workoutCount: workoutCount
+ )
+ }
+}
+
+// ─── Live workout session ─────────────────────────────────────────
+
+/// Manages a live HKWorkoutSession during an active Tabata workout.
+/// Provides real-time heart rate and calorie updates.
+@Observable
+final class LiveWorkoutSession: NSObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate, @unchecked Sendable {
+
+ private(set) var heartRate: Double = 0
+ private(set) var activeCalories: Double = 0
+ private(set) var isActive = false
+
+ private var workoutSession: HKWorkoutSession?
+ private var builder: HKLiveWorkoutBuilder?
+ private let store = HKHealthStore()
+
+ var onHeartRateUpdate: ((Double) -> Void)?
+ var onCaloriesUpdate: ((Double) -> Void)?
+
+ func start(startDate: Date) async throws {
+ let config = HKWorkoutConfiguration()
+ config.activityType = .highIntensityIntervalTraining
+ config.locationType = .indoor
+
+ workoutSession = try HKWorkoutSession(healthStore: store, configuration: config)
+ builder = workoutSession?.associatedWorkoutBuilder()
+ builder?.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: config)
+
+ workoutSession?.delegate = self
+ builder?.delegate = self
+
+ workoutSession?.startActivity(with: startDate)
+ try await builder?.beginCollection(at: startDate)
+ isActive = true
+ }
+
+ func pause() {
+ workoutSession?.pause()
+ }
+
+ func resume() {
+ workoutSession?.resume()
+ }
+
+ func end() async throws -> (calories: Double, avgHeartRate: Double?) {
+ guard let session = workoutSession, let builder = builder else {
+ return (activeCalories, heartRate > 0 ? heartRate : nil)
+ }
+ session.end()
+ try await builder.endCollection(at: Date())
+ _ = try await builder.finishWorkout()
+ isActive = false
+ return (activeCalories, heartRate > 0 ? heartRate : nil)
+ }
+
+ // ─── HKWorkoutSessionDelegate ─────────────────────────────────
+
+ nonisolated func workoutSession(
+ _ workoutSession: HKWorkoutSession,
+ didChangeTo toState: HKWorkoutSessionState,
+ from fromState: HKWorkoutSessionState,
+ date: Date
+ ) {}
+
+ nonisolated func workoutSession(
+ _ workoutSession: HKWorkoutSession,
+ didFailWithError error: Error
+ ) {
+ print("[LiveWorkout] Session error: \(error)")
+ }
+
+ // ─── HKLiveWorkoutBuilderDelegate ────────────────────────────
+
+ nonisolated func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {}
+
+ nonisolated func workoutBuilder(
+ _ workoutBuilder: HKLiveWorkoutBuilder,
+ didCollectDataOf collectedTypes: Set
+ ) {
+ for type in collectedTypes {
+ guard let quantityType = type as? HKQuantityType else { continue }
+ let stats = workoutBuilder.statistics(for: quantityType)
+
+ if quantityType == HKQuantityType(.heartRate) {
+ let hr = stats?.mostRecentQuantity()?.doubleValue(for: HKUnit.count().unitDivided(by: .minute())) ?? 0
+ Task { @MainActor in
+ self.heartRate = hr
+ self.onHeartRateUpdate?(hr)
+ }
+ } else if quantityType == HKQuantityType(.activeEnergyBurned) {
+ let cal = stats?.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0
+ Task { @MainActor in
+ self.activeCalories = cal
+ self.onCaloriesUpdate?(cal)
+ }
+ }
+ }
+ }
+
+}
+
+// ─── Errors ───────────────────────────────────────────────────────
+
+enum HealthKitError: LocalizedError {
+ case notAvailable
+ case notAuthorized
+ case workoutSaveFailed
+
+ var errorDescription: String? {
+ switch self {
+ case .notAvailable: "HealthKit is not available on this device."
+ case .notAuthorized: "HealthKit access was not granted."
+ case .workoutSaveFailed: "Failed to save workout to Health."
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Services/MusicService.swift b/tabatago-swift/TabataGo/Services/MusicService.swift
new file mode 100644
index 0000000..7319ff0
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/MusicService.swift
@@ -0,0 +1,165 @@
+import Foundation
+import Supabase
+
+/// Fetches music tracks from Supabase `download_items` table, with mock fallback.
+actor MusicService {
+
+ static let shared = MusicService()
+
+ private var cache: [MusicVibe: [MusicTrack]] = [:]
+
+ // ─── Public API ──────────────────────────────────────────────
+
+ /// Load tracks for a given vibe. Returns cached results on subsequent calls.
+ func loadTracks(for vibe: MusicVibe) async -> [MusicTrack] {
+ if let cached = cache[vibe] { return cached }
+
+ // Try Supabase first
+ if let tracks = await fetchFromSupabase(vibe: vibe), !tracks.isEmpty {
+ cache[vibe] = tracks
+ return tracks
+ }
+
+ // Fallback to mock tracks
+ let mocks = Self.mockTracks(for: vibe)
+ cache[vibe] = mocks
+ return mocks
+ }
+
+ /// Return `count` distinct random tracks for a vibe.
+ func randomTracks(for vibe: MusicVibe, count: Int = 3) async -> [MusicTrack] {
+ let pool = await loadTracks(for: vibe)
+ guard !pool.isEmpty else { return [] }
+ if pool.count <= count {
+ return pool.shuffled()
+ }
+ return Array(pool.shuffled().prefix(count))
+ }
+
+ /// Next track after `currentId`, cycling through the list.
+ func nextTrack(in tracks: [MusicTrack], after currentId: String) -> MusicTrack? {
+ guard tracks.count > 1 else { return tracks.first }
+ let idx = tracks.firstIndex(where: { $0.id == currentId }) ?? 0
+ return tracks[(idx + 1) % tracks.count]
+ }
+
+ func clearCache(for vibe: MusicVibe? = nil) {
+ if let vibe { cache.removeValue(forKey: vibe) } else { cache.removeAll() }
+ }
+
+ // ─── Supabase Fetch ──────────────────────────────────────────
+
+ private func fetchFromSupabase(vibe: MusicVibe) async -> [MusicTrack]? {
+ // Re-use the existing SupabaseService's client configuration.
+ // We build our own client here because SupabaseService is an actor
+ // and doesn't expose the raw client.
+ guard !AppEnvironment.isPreview else { return nil }
+
+ let urlRaw = Bundle.main.infoDictionary?["SUPABASE_URL"] as? String ?? ""
+ let key = Bundle.main.infoDictionary?["SUPABASE_ANON_KEY"] as? String ?? ""
+ guard !urlRaw.isEmpty, urlRaw != "https://localhost", !key.isEmpty,
+ let url = URL(string: urlRaw) else {
+ return nil
+ }
+
+ let client = SupabaseClient(supabaseURL: url, supabaseKey: key)
+
+ do {
+ let rows: [DownloadItemRow] = try await client
+ .from("download_items")
+ .select("id, video_id, title, duration_seconds, public_url, storage_path, genre")
+ .eq("status", value: "completed")
+ .in("genre", values: vibe.genres)
+ .limit(50)
+ .execute()
+ .value
+
+ let tracks: [MusicTrack] = rows.compactMap { row in
+ guard let trackURL = row.resolvedURL(supabaseBase: urlRaw) else { return nil }
+
+ let (artist, title) = Self.parseTitle(row.title ?? "Unknown Track")
+
+ return MusicTrack(
+ id: row.id ?? UUID().uuidString,
+ title: title,
+ artist: artist,
+ duration: row.duration_seconds ?? 180,
+ url: trackURL,
+ vibe: vibe
+ )
+ }
+
+ #if DEBUG
+ print("[MusicService] Loaded \(tracks.count) tracks for \(vibe.rawValue)")
+ #endif
+
+ return tracks.isEmpty ? nil : tracks
+ } catch {
+ #if DEBUG
+ print("[MusicService] Supabase error: \(error)")
+ #endif
+ return nil
+ }
+ }
+
+ // ─── Helpers ─────────────────────────────────────────────────
+
+ /// Splits "Artist - Title" strings.
+ private static func parseTitle(_ raw: String) -> (artist: String, title: String) {
+ if raw.contains(" - ") {
+ let parts = raw.components(separatedBy: " - ").map { $0.trimmingCharacters(in: .whitespaces) }
+ return (parts[0], parts.dropFirst().joined(separator: " - "))
+ }
+ return ("YouTube Music", raw)
+ }
+
+ // ─── Mock Tracks ─────────────────────────────────────────────
+
+ private static let testBase = "https://www2.cs.uic.edu/~i101/SoundFiles"
+
+ private static func mockTracks(for vibe: MusicVibe) -> [MusicTrack] {
+ let names: [(String, String)] = {
+ switch vibe {
+ case .electronic: return [("Energy Pulse", "Neon Dreams"), ("Cyber Sprint", "Digital Flux"), ("High Voltage", "Circuit Breakers")]
+ case .hipHop: return [("Street Heat", "Urban Flow"), ("Rhythm Power", "Beat Masters"), ("Flow State", "MC Dynamic")]
+ case .pop: return [("Summer Energy", "The Popstars"), ("Upbeat Vibes", "Chart Toppers"), ("Feel Good", "Radio Hits")]
+ case .rock: return [("Power Chord", "The Amplifiers"), ("High Gain", "Distortion"), ("Adrenaline", "Thunderstruck")]
+ case .chill: return [("Smooth Flow", "Lo-Fi Beats"), ("Zen Mode", "Calm Collective"), ("Deep Breath", "Mindful Tones")]
+ }
+ }()
+
+ let files = ["StarWars60.wav", "tapioca.wav", "preamble10.wav"]
+
+ return names.enumerated().map { i, pair in
+ let (title, artist) = pair
+ return MusicTrack(
+ id: "\(vibe.rawValue)-\(i)",
+ title: title,
+ artist: artist,
+ duration: 200,
+ url: URL(string: "\(testBase)/\(files[i % files.count])")!,
+ vibe: vibe
+ )
+ }
+ }
+}
+
+// ─── Supabase Row ────────────────────────────────────────────────
+
+private struct DownloadItemRow: Decodable {
+ let id: String?
+ let video_id: String?
+ let title: String?
+ let duration_seconds: Int?
+ let public_url: String?
+ let storage_path: String?
+ let genre: String?
+
+ func resolvedURL(supabaseBase: String) -> URL? {
+ if let pub = public_url, let url = URL(string: pub) { return url }
+ if let path = storage_path {
+ return URL(string: "\(supabaseBase)/storage/v1/object/public/workout-audio/\(path)")
+ }
+ return nil
+ }
+}
diff --git a/tabatago-swift/TabataGo/Services/PhoneConnectivityManager.swift b/tabatago-swift/TabataGo/Services/PhoneConnectivityManager.swift
new file mode 100644
index 0000000..9a0a023
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/PhoneConnectivityManager.swift
@@ -0,0 +1,136 @@
+import Foundation
+import WatchConnectivity
+
+/// Phone-side WatchConnectivity manager.
+/// Sends workout payloads to Watch, receives HR/calorie updates back.
+@MainActor
+@Observable
+final class PhoneConnectivityManager: NSObject, WCSessionDelegate {
+
+ static let shared = PhoneConnectivityManager()
+
+ private(set) var isWatchReachable = false
+ private(set) var isWatchAppInstalled = false
+
+ var onHeartRateUpdate: ((Double) -> Void)?
+ var onCaloriesUpdate: ((Double) -> Void)?
+ var onSessionCompleted: ((WatchSessionResult) -> Void)?
+
+ private var session: WCSession?
+
+ private override init() {
+ super.init()
+ guard WCSession.isSupported() else { return }
+ session = WCSession.default
+ session?.delegate = self
+ session?.activate()
+ }
+
+ // ─── Send workout to Watch ─────────────────────────────────────
+
+ func sendWorkout(_ program: WorkoutProgram) {
+ guard let session, session.isReachable else { return }
+
+ let blocks = program.blocks.map { block in
+ WatchTabataBlock(
+ position: block.position,
+ exercise1Name: block.exercise1.nameEn,
+ exercise2Name: block.exercise2.nameEn,
+ rounds: block.rounds,
+ workTime: block.workTime,
+ restTime: block.restTime
+ )
+ }
+
+ let payload = WatchWorkoutPayload(
+ programId: program.id,
+ programTitle: program.titleEn,
+ bodyZone: program.bodyZone,
+ level: program.level,
+ totalRounds: program.totalRounds,
+ blocks: blocks,
+ warmupDuration: program.warmup.totalDuration,
+ cooldownDuration: program.cooldown.totalDuration
+ )
+
+ guard let data = try? JSONEncoder().encode(payload) else { return }
+
+ session.sendMessage(
+ [WCMessageKey.type: WCMessageType.startWorkout.rawValue,
+ WCMessageKey.workoutPayload: data],
+ replyHandler: nil
+ )
+ }
+
+ func sendTimerTick(_ tick: TimerTickPayload) {
+ guard let session, session.isReachable else { return }
+ guard let data = try? JSONEncoder().encode(tick) else { return }
+ session.sendMessage(
+ [WCMessageKey.type: WCMessageType.timerTick.rawValue,
+ "tick": data],
+ replyHandler: nil
+ )
+ }
+
+ func sendPause() {
+ sendSimple(type: .pauseWorkout)
+ }
+
+ func sendResume() {
+ sendSimple(type: .resumeWorkout)
+ }
+
+ func sendEndWorkout() {
+ sendSimple(type: .endWorkout)
+ }
+
+ // ─── WCSessionDelegate ────────────────────────────────────────
+
+ nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
+ let reachable = session.isReachable
+ let installed = session.isWatchAppInstalled
+ Task { @MainActor in
+ self.isWatchReachable = reachable
+ self.isWatchAppInstalled = installed
+ }
+ }
+
+ nonisolated func sessionReachabilityDidChange(_ session: WCSession) {
+ let reachable = session.isReachable
+ Task { @MainActor in
+ self.isWatchReachable = reachable
+ }
+ }
+
+ nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
+ guard let typeRaw = message[WCMessageKey.type] as? String,
+ let type = WCMessageType(rawValue: typeRaw) else { return }
+
+ switch type {
+ case .heartRateUpdate:
+ let hr = message[WCMessageKey.heartRate] as? Double ?? 0
+ Task { @MainActor in self.onHeartRateUpdate?(hr) }
+
+ case .sessionCompleted:
+ guard let data = message[WCMessageKey.sessionResult] as? Data,
+ let result = try? JSONDecoder().decode(WatchSessionResult.self, from: data) else { return }
+ Task { @MainActor in self.onSessionCompleted?(result) }
+
+ default:
+ break
+ }
+ }
+
+ // Required for iOS
+ nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
+ nonisolated func sessionDidDeactivate(_ session: WCSession) {
+ session.activate()
+ }
+
+ // ─── Private helpers ──────────────────────────────────────────
+
+ private func sendSimple(type: WCMessageType) {
+ guard let session, session.isReachable else { return }
+ session.sendMessage([WCMessageKey.type: type.rawValue], replyHandler: nil)
+ }
+}
diff --git a/tabatago-swift/TabataGo/Services/PurchaseService.swift b/tabatago-swift/TabataGo/Services/PurchaseService.swift
new file mode 100644
index 0000000..b5c04c4
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/PurchaseService.swift
@@ -0,0 +1,103 @@
+import Foundation
+import RevenueCat
+
+/// Wraps RevenueCat — manages entitlements and purchase flows.
+@MainActor
+@Observable
+final class PurchaseService {
+
+ static let shared = PurchaseService()
+
+ private(set) var isInitialized = false
+ private(set) var currentPlan: SubscriptionPlan = .free
+ private(set) var offerings: Offerings? = nil
+ private(set) var isPurchasing = false
+ private(set) var error: String? = nil
+
+ var isPremium: Bool { currentPlan.isPremium }
+
+ private static let apiKey: String =
+ Bundle.main.infoDictionary?["REVENUECAT_API_KEY"] as? String ?? ""
+
+ private static let entitlementId = "1000 Corp Pro"
+
+ private init() {}
+
+ @MainActor
+ func initialize() async {
+ guard !isInitialized, !AppEnvironment.isPreview else { return }
+
+ let key = Self.apiKey
+ guard !key.isEmpty else {
+ #if DEBUG
+ print("[PurchaseService] No API key configured — skipping RevenueCat init")
+ #endif
+ isInitialized = true
+ return
+ }
+
+ #if DEBUG
+ Purchases.logLevel = .debug
+ #endif
+ Purchases.configure(withAPIKey: key)
+ isInitialized = true
+ await refreshEntitlement()
+ await loadOfferings()
+ }
+
+ /// True when RevenueCat SDK has been configured with a valid-looking key.
+ private var isSDKConfigured: Bool {
+ isInitialized && !Self.apiKey.isEmpty
+ }
+
+ @MainActor
+ func refreshEntitlement() async {
+ guard isSDKConfigured else { return }
+ do {
+ let info = try await Purchases.shared.customerInfo()
+ updatePlan(from: info)
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ @MainActor
+ func loadOfferings() async {
+ guard isSDKConfigured else { return }
+ do {
+ offerings = try await Purchases.shared.offerings()
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ @MainActor
+ func purchase(package: Package) async throws {
+ guard isSDKConfigured else { return }
+ isPurchasing = true
+ defer { isPurchasing = false }
+ let result = try await Purchases.shared.purchase(package: package)
+ updatePlan(from: result.customerInfo)
+ }
+
+ @MainActor
+ func restorePurchases() async throws {
+ guard isSDKConfigured else { return }
+ let info = try await Purchases.shared.restorePurchases()
+ updatePlan(from: info)
+ }
+
+ private func updatePlan(from info: CustomerInfo) {
+ let isActive = info.entitlements[Self.entitlementId]?.isActive == true
+ if isActive {
+ // Determine monthly vs yearly from active subscriptions
+ if info.activeSubscriptions.contains(where: { $0.contains("yearly") || $0.contains("annual") }) {
+ currentPlan = .premiumYearly
+ } else {
+ currentPlan = .premiumMonthly
+ }
+ } else {
+ currentPlan = .free
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Services/SupabaseService.swift b/tabatago-swift/TabataGo/Services/SupabaseService.swift
new file mode 100644
index 0000000..ee2f896
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/SupabaseService.swift
@@ -0,0 +1,247 @@
+import Foundation
+import Supabase
+
+// ─── Service ─────────────────────────────────────────────────────
+
+/// Fetches workout programs from Supabase, with offline cache fallback.
+actor SupabaseService {
+
+ static let shared = SupabaseService()
+
+ private let client: SupabaseClient
+
+ /// True only when both URL and key are real values, and we are not in a preview/test sandbox.
+ private let isConfigured: Bool
+
+ private init() {
+ // Bail out entirely in Xcode Canvas / unit-test sandboxes — no network
+ // calls, no SupabaseClient init with placeholder credentials.
+ guard !AppEnvironment.isPreview else {
+ client = SupabaseClient(
+ supabaseURL: URL(string: "https://localhost")!,
+ supabaseKey: ""
+ )
+ isConfigured = false
+ return
+ }
+
+ let urlRaw = Bundle.main.infoDictionary?["SUPABASE_URL"] as? String ?? ""
+ let key = Bundle.main.infoDictionary?["SUPABASE_ANON_KEY"] as? String ?? ""
+ let url = URL(string: urlRaw) ?? URL(string: "https://localhost")!
+
+ // Consider configured only when both values look non-empty and non-placeholder.
+ let validURL = !urlRaw.isEmpty && urlRaw != "https://localhost"
+ let validKey = !key.isEmpty
+ isConfigured = validURL && validKey
+
+ client = SupabaseClient(supabaseURL: url, supabaseKey: key)
+
+ #if DEBUG
+ print("[SupabaseService] URL=\(urlRaw) configured=\(isConfigured)")
+ #endif
+ }
+
+ // ─── Program Fetching ────────────────────────────────────────
+
+ /// Fetch all workout programs. Returns nil in preview/test environments or when unconfigured.
+ func fetchAllPrograms() async throws -> [WorkoutProgram]? {
+ guard isConfigured else { return nil }
+
+ let programRows: [WorkoutProgramRow] = try await client
+ .from("workout_programs")
+ .select("*")
+ .order("sort_order")
+ .execute()
+ .value
+
+ let tabataRows: [ProgramTabataRow] = try await client
+ .from("program_tabatas")
+ .select("*")
+ .order("position")
+ .execute()
+ .value
+
+ let warmupRows: [TimedExerciseRow] = try await client
+ .from("workout_warmup_exercises")
+ .select("*")
+ .order("position")
+ .execute()
+ .value
+
+ let stretchRows: [TimedExerciseRow] = try await client
+ .from("workout_stretch_exercises")
+ .select("*")
+ .order("position")
+ .execute()
+ .value
+
+ return assemble(
+ programs: programRows,
+ tabatas: tabataRows,
+ warmups: warmupRows,
+ stretches: stretchRows
+ )
+ }
+
+ /// Sync a completed workout session to Supabase (premium users only).
+ func syncSession(_ session: WorkoutSession) async throws {
+ guard isConfigured else { return }
+ let payload = SessionPayload(
+ workout_id: session.programId,
+ completed_at: ISO8601DateFormatter().string(from: session.completedAt),
+ duration_seconds: session.durationSeconds,
+ calories_burned: session.caloriesBurned,
+ average_heart_rate: session.averageHeartRate
+ )
+ try await client.from("workout_sessions").insert(payload).execute()
+ }
+
+ // ─── Assembly ────────────────────────────────────────────────
+
+ private func assemble(
+ programs: [WorkoutProgramRow],
+ tabatas: [ProgramTabataRow],
+ warmups: [TimedExerciseRow],
+ stretches: [TimedExerciseRow]
+ ) -> [WorkoutProgram] {
+ programs.map { prog in
+ let progTabatas = tabatas
+ .filter { $0.program_id == prog.id }
+ .sorted { $0.position < $1.position }
+
+ let progWarmups = warmups
+ .filter { $0.program_id == prog.id }
+ .sorted { $0.position < $1.position }
+
+ let progStretches = stretches
+ .filter { $0.program_id == prog.id }
+ .sorted { $0.position < $1.position }
+
+ let blocks = progTabatas.map { row in
+ TabataBlock(
+ id: row.id,
+ position: row.position,
+ exercise1: TabataExercise(
+ name: row.exercise_1_name,
+ nameEn: row.exercise_1_name_en ?? row.exercise_1_name,
+ tip: row.exercise_1_tip,
+ tipEn: row.exercise_1_tip_en,
+ modification: row.exercise_1_modification,
+ modificationEn: row.exercise_1_modification_en,
+ progression: row.exercise_1_progression,
+ progressionEn: row.exercise_1_progression_en,
+ videoUrl: row.exercise_1_video_url
+ ),
+ exercise2: TabataExercise(
+ name: row.exercise_2_name,
+ nameEn: row.exercise_2_name_en ?? row.exercise_2_name,
+ tip: row.exercise_2_tip,
+ tipEn: row.exercise_2_tip_en,
+ modification: row.exercise_2_modification,
+ modificationEn: row.exercise_2_modification_en,
+ progression: row.exercise_2_progression,
+ progressionEn: row.exercise_2_progression_en,
+ videoUrl: row.exercise_2_video_url
+ ),
+ rounds: row.rounds,
+ workTime: row.work_time,
+ restTime: row.rest_time
+ )
+ }
+
+ let warmupMovements = progWarmups.map {
+ TimedMovement(name: $0.name, nameEn: $0.name_en ?? $0.name, duration: $0.duration, videoUrl: $0.video_url)
+ }
+ let stretchMovements = progStretches.map {
+ TimedMovement(name: $0.name, nameEn: $0.name_en ?? $0.name, duration: $0.duration, videoUrl: $0.video_url)
+ }
+
+ let totalRounds = blocks.reduce(0) { $0 + $1.rounds }
+ let workSeconds = blocks.reduce(0) { $0 + $1.rounds * ($1.workTime + $1.restTime) }
+ let warmupTotal = warmupMovements.reduce(0) { $0 + $1.duration }
+ let stretchTotal = stretchMovements.reduce(0) { $0 + $1.duration }
+
+ return WorkoutProgram(
+ id: prog.id,
+ title: prog.title,
+ titleEn: prog.title_en ?? prog.title,
+ description: prog.description ?? "",
+ descriptionEn: prog.description_en ?? prog.description ?? "",
+ bodyZone: prog.body_zone,
+ level: prog.level,
+ musicVibe: prog.music_vibe ?? "electronic",
+ accentColor: prog.accent_color ?? "#FF6B35",
+ isFree: prog.is_free,
+ estimatedCalories: prog.estimated_calories ?? 0,
+ estimatedDuration: (warmupTotal + workSeconds + stretchTotal) / 60,
+ totalRounds: totalRounds,
+ warmup: WarmupSection(movements: warmupMovements, totalDuration: warmupTotal),
+ cooldown: CooldownSection(movements: stretchMovements, totalDuration: stretchTotal),
+ blocks: blocks,
+ thumbnailUrl: nil
+ )
+ }
+ }
+}
+
+// ─── Row types (Supabase decode) ─────────────────────────────────
+
+struct WorkoutProgramRow: Decodable {
+ let id: String
+ let title: String
+ let title_en: String?
+ let description: String?
+ let description_en: String?
+ let body_zone: String
+ let level: String
+ let music_vibe: String?
+ let accent_color: String?
+ let is_free: Bool
+ let estimated_calories: Int?
+ let sort_order: Int?
+}
+
+struct ProgramTabataRow: Decodable {
+ let id: String
+ let program_id: String
+ let position: Int
+ let exercise_1_name: String
+ let exercise_1_name_en: String?
+ let exercise_1_tip: String?
+ let exercise_1_tip_en: String?
+ let exercise_1_modification: String?
+ let exercise_1_modification_en: String?
+ let exercise_1_progression: String?
+ let exercise_1_progression_en: String?
+ let exercise_1_video_url: String?
+ let exercise_2_name: String
+ let exercise_2_name_en: String?
+ let exercise_2_tip: String?
+ let exercise_2_tip_en: String?
+ let exercise_2_modification: String?
+ let exercise_2_modification_en: String?
+ let exercise_2_progression: String?
+ let exercise_2_progression_en: String?
+ let exercise_2_video_url: String?
+ let rounds: Int
+ let work_time: Int
+ let rest_time: Int
+}
+
+struct TimedExerciseRow: Decodable {
+ let id: String
+ let program_id: String
+ let name: String
+ let name_en: String?
+ let duration: Int
+ let position: Int
+ let video_url: String?
+}
+
+struct SessionPayload: Encodable {
+ let workout_id: String
+ let completed_at: String
+ let duration_seconds: Int
+ let calories_burned: Double
+ let average_heart_rate: Double?
+}
diff --git a/tabatago-swift/TabataGo/Services/WatchConnectivityTypes.swift b/tabatago-swift/TabataGo/Services/WatchConnectivityTypes.swift
new file mode 100644
index 0000000..6decbe6
--- /dev/null
+++ b/tabatago-swift/TabataGo/Services/WatchConnectivityTypes.swift
@@ -0,0 +1,72 @@
+import Foundation
+
+// ─── Shared message keys (used by both iOS and watchOS targets) ───
+
+enum WCMessageKey {
+ static let type = "type"
+ static let workoutPayload = "workout"
+ static let sessionResult = "sessionResult"
+ static let heartRate = "heartRate"
+ static let calories = "calories"
+ static let phase = "phase"
+ static let timeRemaining = "timeRemaining"
+ static let round = "round"
+ static let totalRounds = "totalRounds"
+ static let exerciseName = "exerciseName"
+ static let programId = "programId"
+ static let programTitle = "programTitle"
+}
+
+enum WCMessageType: String {
+ case startWorkout = "startWorkout"
+ case pauseWorkout = "pauseWorkout"
+ case resumeWorkout = "resumeWorkout"
+ case endWorkout = "endWorkout"
+ case timerTick = "timerTick" // phone → watch: sync state
+ case sessionCompleted = "sessionCompleted" // watch → phone: HR/cal data
+ case heartRateUpdate = "heartRateUpdate" // watch → phone: live HR
+}
+
+// ─── Codable payload for starting a workout on Watch ─────────────
+
+struct WatchWorkoutPayload: Codable {
+ var programId: String
+ var programTitle: String
+ var bodyZone: String
+ var level: String
+ var totalRounds: Int
+ var blocks: [WatchTabataBlock]
+ var warmupDuration: Int // seconds
+ var cooldownDuration: Int // seconds
+}
+
+struct WatchTabataBlock: Codable {
+ var position: Int
+ var exercise1Name: String
+ var exercise2Name: String
+ var rounds: Int
+ var workTime: Int
+ var restTime: Int
+}
+
+// ─── Timer tick payload (phone keeps Watch in sync) ───────────────
+
+struct TimerTickPayload: Codable {
+ var phase: String
+ var timeRemaining: Int
+ var currentRound: Int
+ var totalRoundsInBlock: Int
+ var exerciseName: String?
+}
+
+// ─── Session result (Watch → phone after workout ends) ────────────
+
+struct WatchSessionResult: Codable {
+ var programId: String
+ var startedAt: Date
+ var completedAt: Date
+ var durationSeconds: Int
+ var activeCalories: Double
+ var averageHeartRate: Double?
+ var peakHeartRate: Double?
+}
diff --git a/tabatago-swift/TabataGo/Theme/Theme.swift b/tabatago-swift/TabataGo/Theme/Theme.swift
new file mode 100644
index 0000000..60c3ff8
--- /dev/null
+++ b/tabatago-swift/TabataGo/Theme/Theme.swift
@@ -0,0 +1,163 @@
+import SwiftUI
+
+/// TabataGo design tokens — adaptive light/dark theme.
+enum Theme {
+
+ // ─── Brand Colors (invariant — work on both light & dark) ─────
+ static let brand = Color(red: 1.0, green: 0.42, blue: 0.21) // #FF6B35 Flame orange
+ static let brandLight = Color(red: 1.0, green: 0.73, blue: 0.58)
+ static let rest = Color(red: 0.35, green: 0.78, blue: 0.98) // #5AC8FA Ice blue
+ static let success = Color(red: 0.19, green: 0.82, blue: 0.35) // #30D158 Energy green
+ static let prep = Color(red: 1.0, green: 0.58, blue: 0.0) // #FF9500 Orange-yellow
+
+ // ─── Adaptive Surface Colors ──────────────────────────────────
+ // Dark: custom navy surfaces. Light: system defaults.
+
+ /// Main screen background
+ static let surfaceBackground = Color(uiColor: UIColor { traits in
+ traits.userInterfaceStyle == .dark
+ ? UIColor(red: 0.04, green: 0.09, blue: 0.16, alpha: 1) // #0A1628
+ : .systemBackground
+ })
+
+ /// Card / grouped inset background
+ static let surfaceCard = Color(uiColor: UIColor { traits in
+ traits.userInterfaceStyle == .dark
+ ? UIColor(red: 0.07, green: 0.11, blue: 0.18, alpha: 1) // #111D2E
+ : .secondarySystemBackground
+ })
+
+ /// Elevated card / hover state
+ static let surfaceElevated = Color(uiColor: UIColor { traits in
+ traits.userInterfaceStyle == .dark
+ ? UIColor(red: 0.10, green: 0.16, blue: 0.24, alpha: 1) // #192A3E
+ : .tertiarySystemBackground
+ })
+
+ /// Subtle overlay / separator tint
+ static let surfaceOverlay = Color(uiColor: UIColor { traits in
+ traits.userInterfaceStyle == .dark
+ ? UIColor(white: 1.0, alpha: 0.05)
+ : UIColor(white: 0.0, alpha: 0.03)
+ })
+
+ /// Adaptive border color
+ static let border = Color(uiColor: UIColor { traits in
+ traits.userInterfaceStyle == .dark
+ ? UIColor(white: 1.0, alpha: 0.10)
+ : UIColor(white: 0.0, alpha: 0.08)
+ })
+
+ // ─── Phase Colors ─────────────────────────────────────────────
+ static func phaseColor(_ phase: TimerPhase) -> Color {
+ switch phase {
+ case .prep: return prep
+ case .warmup: return prep
+ case .work: return brand
+ case .rest: return rest
+ case .interBlockRest: return rest.opacity(0.7)
+ case .cooldown: return Color(red: 0.40, green: 0.75, blue: 0.90)
+ case .complete: return success
+ }
+ }
+
+ static func phaseLabel(_ phase: TimerPhase) -> String {
+ switch phase {
+ case .prep: return "GET READY"
+ case .warmup: return "WARM UP"
+ case .work: return "WORK"
+ case .rest: return "REST"
+ case .interBlockRest: return "BREAK"
+ case .cooldown: return "COOL DOWN"
+ case .complete: return "DONE"
+ }
+ }
+
+ // ─── Body Zone Gradients ──────────────────────────────────────
+ static func zoneGradient(_ zone: String) -> LinearGradient {
+ switch zone.lowercased() {
+ case "upper-body", "upper":
+ return LinearGradient(colors: [.orange, .red.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing)
+ case "lower-body", "lower":
+ return LinearGradient(colors: [.blue, .purple.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing)
+ case "full-body", "full":
+ return LinearGradient(colors: [brand, .purple], startPoint: .topLeading, endPoint: .bottomTrailing)
+ default:
+ return LinearGradient(colors: [.gray, .secondary], startPoint: .topLeading, endPoint: .bottomTrailing)
+ }
+ }
+
+ static func zoneColor(_ zone: String) -> Color {
+ switch zone.lowercased() {
+ case "upper-body", "upper": return .orange
+ case "lower-body", "lower": return .blue
+ case "full-body", "full": return brand
+ default: return .gray
+ }
+ }
+
+ // ─── Level Colors ─────────────────────────────────────────────
+ static func levelColor(_ level: String) -> Color {
+ switch level.lowercased() {
+ case "beginner": return success
+ case "intermediate": return prep
+ case "advanced": return brand
+ default: return .gray
+ }
+ }
+
+ // ─── Typography ───────────────────────────────────────────────
+ static let timerFont = Font.system(size: 96, weight: .black, design: .rounded)
+ .monospacedDigit()
+ static let timerSmallFont = Font.system(size: 60, weight: .bold, design: .rounded)
+ .monospacedDigit()
+ static let roundFont = Font.system(size: 22, weight: .semibold, design: .rounded)
+ static let phaseFont = Font.system(size: 18, weight: .bold, design: .rounded)
+}
+
+// ─── Glass Effect Modifier ────────────────────────────────────────
+
+struct GlassCard: ViewModifier {
+ var cornerRadius: CGFloat = 20
+
+ func body(content: Content) -> some View {
+ content
+ .background(.ultraThinMaterial)
+ .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
+ }
+}
+
+extension View {
+ func glassCard(cornerRadius: CGFloat = 20) -> some View {
+ modifier(GlassCard(cornerRadius: cornerRadius))
+ }
+}
+
+// ─── Stat Badge ───────────────────────────────────────────────────
+
+struct StatBadge: View {
+ let label: String
+ let value: String
+ var color: Color = .primary
+ var icon: String? = nil
+
+ var body: some View {
+ VStack(spacing: 4) {
+ if let icon {
+ Image(systemName: icon)
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundStyle(color)
+ }
+ Text(value)
+ .font(.system(size: 22, weight: .bold, design: .rounded))
+ .foregroundStyle(color)
+ .monospacedDigit()
+ Text(label)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 12)
+ .glassCard()
+ }
+}
diff --git a/tabatago-swift/TabataGo/Utilities/Environment.swift b/tabatago-swift/TabataGo/Utilities/Environment.swift
new file mode 100644
index 0000000..46dcf01
--- /dev/null
+++ b/tabatago-swift/TabataGo/Utilities/Environment.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+/// Helpers for detecting the current runtime environment.
+enum AppEnvironment {
+ /// True when running inside the Xcode preview sandbox.
+ static let isPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
+}
diff --git a/tabatago-swift/TabataGo/Utilities/Strings.swift b/tabatago-swift/TabataGo/Utilities/Strings.swift
new file mode 100644
index 0000000..6a74038
--- /dev/null
+++ b/tabatago-swift/TabataGo/Utilities/Strings.swift
@@ -0,0 +1,138 @@
+import Foundation
+
+/// Type-safe string keys for `Localizable.xcstrings`.
+/// Usage: Text(L10n.action.start) or String(localized: L10n.player.phase.work)
+enum L10n {
+ enum action {
+ static let back = LocalizedStringResource("action.back")
+ static let cancel = LocalizedStringResource("action.cancel")
+ static let `continue` = LocalizedStringResource("action.continue")
+ static let done = LocalizedStringResource("action.done")
+ static let save = LocalizedStringResource("action.save")
+ static let share = LocalizedStringResource("action.share")
+ static let start = LocalizedStringResource("action.start")
+ static let startWorkout = LocalizedStringResource("action.startWorkout")
+ static let startTraining = LocalizedStringResource("action.startTraining")
+ static let unlockPremium = LocalizedStringResource("action.unlockPremium")
+ static let restorePurchases = LocalizedStringResource("action.restorePurchases")
+ }
+ enum tab {
+ static let home = LocalizedStringResource("tab.home")
+ static let programs = LocalizedStringResource("tab.programs")
+ static let activity = LocalizedStringResource("tab.activity")
+ static let profile = LocalizedStringResource("tab.profile")
+ }
+ enum home {
+ static let featuredTitle = LocalizedStringResource("home.featuredTitle")
+ static let featuredSubtitle = LocalizedStringResource("home.featuredSubtitle")
+ static let browseTitle = LocalizedStringResource("home.browseTitle")
+ static let streak = LocalizedStringResource("home.streak")
+ static let thisWeek = LocalizedStringResource("home.thisWeek")
+ static let allTime = LocalizedStringResource("home.allTime")
+ }
+ enum zone {
+ static let upper = LocalizedStringResource("zone.upper")
+ static let lower = LocalizedStringResource("zone.lower")
+ static let full = LocalizedStringResource("zone.full")
+
+ static func label(for zone: String) -> LocalizedStringResource {
+ switch zone.lowercased() {
+ case "upper": return upper
+ case "lower": return lower
+ case "full": return full
+ default: return LocalizedStringResource(stringLiteral: zone.capitalized)
+ }
+ }
+ }
+ enum level {
+ static let beginner = LocalizedStringResource("level.beginner")
+ static let intermediate = LocalizedStringResource("level.intermediate")
+ static let advanced = LocalizedStringResource("level.advanced")
+ }
+ enum player {
+ enum phase {
+ static let getReady = LocalizedStringResource("player.phase.getReady")
+ static let warmUp = LocalizedStringResource("player.phase.warmUp")
+ static let work = LocalizedStringResource("player.phase.work")
+ static let rest = LocalizedStringResource("player.phase.rest")
+ static let `break` = LocalizedStringResource("player.phase.break")
+ static let coolDown = LocalizedStringResource("player.phase.coolDown")
+ static let done = LocalizedStringResource("player.phase.done")
+
+ static func label(for phase: TimerPhase) -> LocalizedStringResource {
+ switch phase {
+ case .prep: return getReady
+ case .warmup: return warmUp
+ case .work: return work
+ case .rest: return rest
+ case .interBlockRest: return `break`
+ case .cooldown: return coolDown
+ case .complete: return done
+ }
+ }
+ }
+ static let endWorkout = LocalizedStringResource("player.endWorkout")
+ static let endWorkoutMessage = LocalizedStringResource("player.endWorkoutMessage")
+ static let keepGoing = LocalizedStringResource("player.keepGoing")
+ }
+ enum complete {
+ static let title = LocalizedStringResource("complete.title")
+ static let saveToHealth = LocalizedStringResource("complete.saveToHealth")
+ static let savedToHealth = LocalizedStringResource("complete.savedToHealth")
+ static let backToHome = LocalizedStringResource("complete.backToHome")
+ static let duration = LocalizedStringResource("complete.duration")
+ static let calories = LocalizedStringResource("complete.calories")
+ static let rounds = LocalizedStringResource("complete.rounds")
+ static let avgHeartRate = LocalizedStringResource("complete.avgHeartRate")
+ static let shareWorkout = LocalizedStringResource("complete.shareWorkout")
+ }
+ enum activity {
+ static let currentStreak = LocalizedStringResource("activity.currentStreak")
+ static let bestStreak = LocalizedStringResource("activity.bestStreak")
+ static let history = LocalizedStringResource("activity.history")
+ static let workouts = LocalizedStringResource("activity.workouts")
+ static let minutes = LocalizedStringResource("activity.minutes")
+ static let noWorkouts = LocalizedStringResource("activity.noWorkouts")
+ static let noWorkoutsMessage = LocalizedStringResource("activity.noWorkoutsMessage")
+ }
+ enum onboarding {
+ static let whatIsYourName = LocalizedStringResource("onboarding.whatIsYourName")
+ static let fitnessLevel = LocalizedStringResource("onboarding.fitnessLevel")
+ static let mainGoal = LocalizedStringResource("onboarding.mainGoal")
+ static let howOften = LocalizedStringResource("onboarding.howOften")
+ static let allSet = LocalizedStringResource("onboarding.allSet")
+ }
+ enum goal {
+ static let weightLoss = LocalizedStringResource("goal.weightLoss")
+ static let cardio = LocalizedStringResource("goal.cardio")
+ static let strength = LocalizedStringResource("goal.strength")
+ static let wellness = LocalizedStringResource("goal.wellness")
+ }
+ enum settings {
+ static let title = LocalizedStringResource("settings.title")
+ static let audio = LocalizedStringResource("settings.audio")
+ static let soundEffects = LocalizedStringResource("settings.soundEffects")
+ static let voiceCoaching = LocalizedStringResource("settings.voiceCoaching")
+ static let music = LocalizedStringResource("settings.music")
+ static let haptics = LocalizedStringResource("settings.haptics")
+ static let hapticFeedback = LocalizedStringResource("settings.hapticFeedback")
+ static let reminders = LocalizedStringResource("settings.reminders")
+ static let dailyReminder = LocalizedStringResource("settings.dailyReminder")
+ static let resetProgress = LocalizedStringResource("settings.resetProgress")
+ }
+ enum paywall {
+ static let title = LocalizedStringResource("paywall.title")
+ static let subtitle = LocalizedStringResource("paywall.subtitle")
+ static let startPremium = LocalizedStringResource("paywall.startPremium")
+ static let premiumActive = LocalizedStringResource("paywall.premiumActive")
+ static let upgradePrompt = LocalizedStringResource("paywall.upgradePrompt")
+ static let cancelAnytime = LocalizedStringResource("paywall.cancelAnytime")
+ }
+ enum health {
+ static let appleHealth = LocalizedStringResource("health.appleHealth")
+ static let move = LocalizedStringResource("health.move")
+ static let exercise = LocalizedStringResource("health.exercise")
+ static let stand = LocalizedStringResource("health.stand")
+ static let restingHR = LocalizedStringResource("health.restingHR")
+ }
+}
diff --git a/tabatago-swift/TabataGo/ViewModels/HealthViewModel.swift b/tabatago-swift/TabataGo/ViewModels/HealthViewModel.swift
new file mode 100644
index 0000000..d076b43
--- /dev/null
+++ b/tabatago-swift/TabataGo/ViewModels/HealthViewModel.swift
@@ -0,0 +1,23 @@
+import Foundation
+import SwiftData
+
+@MainActor
+final class HealthViewModel: ObservableObject {
+
+ @Published var snapshot: HealthSnapshot? = nil
+ @Published var isLoading = false
+
+ func refresh() async {
+ guard HealthKitService.shared.isAvailable else { return }
+ guard await HealthKitService.shared.isAuthorized else { return }
+
+ isLoading = true
+ defer { isLoading = false }
+
+ do {
+ snapshot = try await HealthKitService.shared.fetchSnapshot()
+ } catch {
+ print("[HealthVM] Failed to fetch snapshot: \(error)")
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/ViewModels/HomeViewModel.swift b/tabatago-swift/TabataGo/ViewModels/HomeViewModel.swift
new file mode 100644
index 0000000..ed95a3a
--- /dev/null
+++ b/tabatago-swift/TabataGo/ViewModels/HomeViewModel.swift
@@ -0,0 +1,60 @@
+import Foundation
+import SwiftData
+
+/// Loads workout programs from Supabase with local SwiftData cache fallback.
+@MainActor
+final class HomeViewModel: ObservableObject {
+
+ @Published var allPrograms: [WorkoutProgram] = []
+ @Published var isLoading = false
+ @Published var error: String? = nil
+
+ var featuredPrograms: [WorkoutProgram] {
+ allPrograms.filter { $0.isFree }.prefix(5).map { $0 }
+ }
+
+ /// Unique body zones derived from fetched programs, falling back to known zones before data loads.
+ var availableZones: [String] {
+ let preferred = ["full-body", "upper-body", "lower-body"]
+ guard !allPrograms.isEmpty else { return preferred }
+ let unique = Array(Set(allPrograms.map(\.bodyZone)))
+ return unique.sorted { a, b in
+ let ia = preferred.firstIndex(of: a) ?? Int.max
+ let ib = preferred.firstIndex(of: b) ?? Int.max
+ return ia < ib
+ }
+ }
+
+ private let cacheKey = "tabatafit-programs-v1"
+
+ /// Designated init for production use.
+ init() {}
+
+ /// Preview/test init — injects programs directly, no async loading needed.
+ init(previewPrograms: [WorkoutProgram]) {
+ self.allPrograms = previewPrograms
+ }
+
+ func loadPrograms() async {
+ guard allPrograms.isEmpty else { return }
+ await fetchPrograms()
+ }
+
+ func refresh() async {
+ await fetchPrograms()
+ }
+
+ private func fetchPrograms() async {
+ // isConfigured guard inside SupabaseService handles preview/unconfigured cases.
+ isLoading = true
+ defer { isLoading = false }
+
+ do {
+ if let remote = try await SupabaseService.shared.fetchAllPrograms() {
+ allPrograms = remote
+ }
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/ViewModels/MusicPlayerViewModel.swift b/tabatago-swift/TabataGo/ViewModels/MusicPlayerViewModel.swift
new file mode 100644
index 0000000..9ed1e59
--- /dev/null
+++ b/tabatago-swift/TabataGo/ViewModels/MusicPlayerViewModel.swift
@@ -0,0 +1,124 @@
+import AVFoundation
+import Combine
+import SwiftUI
+
+/// Streams workout music via AVPlayer, synced to the workout timer state.
+/// Fetches tracks from Supabase via MusicService, auto-advances on finish.
+@MainActor
+final class MusicPlayerViewModel: ObservableObject {
+
+ // ── Public State ──────────────────────────────────────────────
+ @Published private(set) var currentTrack: MusicTrack?
+ @Published private(set) var isReady = false
+ @Published private(set) var error: String?
+
+ // ── Config ────────────────────────────────────────────────────
+ private let vibe: MusicVibe
+ private let audio = AudioService.shared
+
+ // ── Player ────────────────────────────────────────────────────
+ private var player: AVPlayer?
+ private var tracks: [MusicTrack] = []
+ private var currentIndex = 0
+ private var endObserver: Any?
+
+ // ── Init / Deinit ─────────────────────────────────────────────
+
+ init(vibe: MusicVibe) {
+ self.vibe = vibe
+ }
+
+ /// Call once after init (e.g. in .onAppear / .task).
+ func load() async {
+ tracks = await MusicService.shared.loadTracks(for: vibe)
+ guard !tracks.isEmpty else {
+ error = "No tracks available"
+ return
+ }
+
+ currentIndex = Int.random(in: 0.. 1 {
+ timeRemaining -= 1
+ } else {
+ timer?.invalidate()
+ advancePhase()
+ }
+ }
+
+ // ─── Phase Transitions ────────────────────────────────────────
+
+ private func advancePhase() {
+ switch phase {
+ case .prep:
+ if !program.warmup.movements.isEmpty {
+ warmupIndex = 0
+ enterPhase(.warmup)
+ } else {
+ currentBlockIndex = 0
+ currentRound = 1
+ enterPhase(.work)
+ }
+
+ case .warmup:
+ warmupIndex += 1
+ if warmupIndex < program.warmup.movements.count {
+ enterPhase(.warmup) // next warmup movement
+ } else {
+ currentBlockIndex = 0
+ currentRound = 1
+ enterPhase(.work)
+ }
+
+ case .work:
+ enterPhase(.rest)
+
+ case .rest:
+ let block = program.blocks[currentBlockIndex]
+ if currentRound < block.rounds {
+ currentRound += 1
+ enterPhase(.work)
+ } else {
+ // End of block
+ let nextBlockIndex = currentBlockIndex + 1
+ if nextBlockIndex < program.blocks.count {
+ currentBlockIndex = nextBlockIndex
+ currentRound = 1
+ enterPhase(.interBlockRest)
+ } else {
+ // All blocks done
+ if !program.cooldown.movements.isEmpty {
+ cooldownIndex = 0
+ enterPhase(.cooldown)
+ } else {
+ enterPhase(.complete)
+ }
+ }
+ }
+
+ case .interBlockRest:
+ enterPhase(.work)
+
+ case .cooldown:
+ cooldownIndex += 1
+ if cooldownIndex < program.cooldown.movements.count {
+ enterPhase(.cooldown)
+ } else {
+ enterPhase(.complete)
+ }
+
+ case .complete:
+ break
+ }
+ }
+
+ private func enterPhase(_ newPhase: TimerPhase) {
+ withAnimation(.spring(duration: 0.4)) {
+ phase = newPhase
+ }
+
+ haptics.impactOccurred(intensity: newPhase == .work ? 1.0 : 0.6)
+ audio.playPhaseStart(newPhase)
+ audio.announcePhase(newPhase)
+
+ switch newPhase {
+ case .prep:
+ timeRemaining = 5
+ totalPhaseTime = 5
+ currentExercise = nil
+
+ case .warmup:
+ let movement = program.warmup.movements[warmupIndex]
+ timeRemaining = movement.duration
+ totalPhaseTime = movement.duration
+ currentExercise = TabataExercise(name: movement.name, nameEn: movement.nameEn)
+
+ case .work:
+ guard let block = currentBlock else { return }
+ let exercise = currentRound % 2 == 1 ? block.exercise1 : block.exercise2
+ timeRemaining = block.workTime
+ totalPhaseTime = block.workTime
+ totalRoundsInBlock = block.rounds
+ currentExercise = exercise
+ audio.announceExercise(exercise)
+
+ case .rest:
+ guard let block = currentBlock else { return }
+ timeRemaining = block.restTime
+ totalPhaseTime = block.restTime
+ // Preview next exercise
+ let nextIsExercise1 = (currentRound + 1) % 2 == 1
+ currentExercise = nextIsExercise1 ? block.exercise1 : block.exercise2
+
+ case .interBlockRest:
+ timeRemaining = 60
+ totalPhaseTime = 60
+ currentExercise = nil
+
+ case .cooldown:
+ let movement = program.cooldown.movements[cooldownIndex]
+ timeRemaining = movement.duration
+ totalPhaseTime = movement.duration
+ currentExercise = TabataExercise(name: movement.name, nameEn: movement.nameEn)
+
+ case .complete:
+ currentExercise = nil
+ timeRemaining = 0
+ Task { await finishWorkout() }
+ }
+
+ if isRunning && !isPaused {
+ startTimer()
+ }
+ }
+
+ // ─── Workout Completion ───────────────────────────────────────
+
+ private func finishWorkout() async {
+ let now = Date()
+ let duration = Int(now.timeIntervalSince(startedAt ?? now))
+
+ // Collect HealthKit data
+ let (hkCalories, avgHR) = (try? await liveSession.end()) ?? (0, nil)
+ let finalCalories = hkCalories > 0 ? hkCalories : estimateCalories()
+
+ // Build session
+ let session = WorkoutSession(
+ programId: program.id,
+ programTitle: program.titleEn,
+ bodyZone: program.bodyZone,
+ level: program.level,
+ startedAt: startedAt ?? now,
+ completedAt: now,
+ durationSeconds: duration,
+ caloriesBurned: finalCalories,
+ roundsCompleted: program.totalRounds,
+ totalRounds: program.totalRounds
+ )
+ session.averageHeartRate = avgHR
+
+ modelContext?.insert(session)
+ try? modelContext?.save()
+
+ completedSession = session
+ isComplete = true
+
+ AnalyticsService.shared.workoutCompleted(
+ programId: program.id,
+ durationSeconds: duration,
+ calories: finalCalories,
+ completionRate: 1.0,
+ healthKitSaved: false // updated after user confirms save in CompletionView
+ )
+ }
+
+ private func estimateCalories() -> Double {
+ Double(program.estimatedCalories)
+ }
+}
diff --git a/tabatago-swift/TabataGo/ViewModels/PurchaseViewModel.swift b/tabatago-swift/TabataGo/ViewModels/PurchaseViewModel.swift
new file mode 100644
index 0000000..7171dc0
--- /dev/null
+++ b/tabatago-swift/TabataGo/ViewModels/PurchaseViewModel.swift
@@ -0,0 +1,46 @@
+import Foundation
+import RevenueCat
+
+@MainActor
+final class PurchaseViewModel: ObservableObject {
+
+ @Published var offerings: Offerings? = nil
+ @Published var selectedPackage: Package? = nil
+ @Published var isPurchasing = false
+ @Published var showError = false
+ @Published var errorMessage: String? = nil
+
+ private let service = PurchaseService.shared
+
+ func loadOfferings() async {
+ await service.loadOfferings()
+ offerings = service.offerings
+ // Pre-select yearly if available
+ selectedPackage = offerings?.current?.availablePackages.first {
+ $0.packageType == .annual
+ } ?? offerings?.current?.availablePackages.first
+ }
+
+ func purchase() async {
+ guard let package = selectedPackage else { return }
+ isPurchasing = true
+ do {
+ try await service.purchase(package: package)
+ AnalyticsService.shared.subscriptionStarted(plan: package.identifier)
+ } catch {
+ errorMessage = error.localizedDescription
+ showError = true
+ }
+ isPurchasing = false
+ }
+
+ func restorePurchases() async {
+ do {
+ try await service.restorePurchases()
+ AnalyticsService.shared.subscriptionRestored()
+ } catch {
+ errorMessage = error.localizedDescription
+ showError = true
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Views/Complete/CompletionView.swift b/tabatago-swift/TabataGo/Views/Complete/CompletionView.swift
new file mode 100644
index 0000000..8fa0976
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Complete/CompletionView.swift
@@ -0,0 +1,241 @@
+import SwiftUI
+import SwiftData
+
+/// Workout completion screen — summary, HealthKit save, share.
+struct CompletionView: View {
+ let session: WorkoutSession?
+ let program: WorkoutProgram
+ var onDone: () -> Void = {}
+
+ @Environment(\.modelContext) private var context
+ @Environment(\.dismiss) private var dismiss
+ @State private var healthKitSaved = false
+ @State private var isSavingToHealth = false
+ @State private var showShareSheet = false
+ @State private var confettiTrigger = 0
+
+ var body: some View {
+ ZStack {
+ // Background
+ Theme.surfaceBackground
+ .ignoresSafeArea()
+
+ ScrollView {
+ VStack(spacing: 28) {
+
+ // ── Trophy Header ──────────────────────────────
+ VStack(spacing: 12) {
+ Image(systemName: "trophy.fill")
+ .font(.system(size: 72))
+ .foregroundStyle(
+ LinearGradient(colors: [.yellow, .orange], startPoint: .top, endPoint: .bottom)
+ )
+ .symbolEffect(.bounce, value: confettiTrigger)
+ .padding(.top, 32)
+
+ Text("Workout Complete!")
+ .font(.system(size: 32, weight: .black, design: .rounded))
+ .foregroundStyle(.primary)
+
+ Text(program.titleEn)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+
+ // ── Stats Grid ─────────────────────────────────
+ if let session {
+ LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
+ CompletionStat(
+ label: "Duration",
+ value: formatDuration(session.durationSeconds),
+ icon: "clock.fill",
+ color: Theme.rest
+ )
+ CompletionStat(
+ label: "Calories",
+ value: "\(Int(session.caloriesBurned)) kcal",
+ icon: "flame.fill",
+ color: Theme.brand
+ )
+ CompletionStat(
+ label: "Rounds",
+ value: "\(session.roundsCompleted) / \(session.totalRounds)",
+ icon: "repeat",
+ color: Theme.success
+ )
+ if let hr = session.averageHeartRate {
+ CompletionStat(
+ label: "Avg Heart Rate",
+ value: "\(Int(hr)) bpm",
+ icon: "heart.fill",
+ color: .red
+ )
+ } else {
+ CompletionStat(
+ label: "Completion",
+ value: "\(Int(session.completionRate * 100))%",
+ icon: "checkmark.circle.fill",
+ color: Theme.success
+ )
+ }
+ }
+ .padding(.horizontal)
+ }
+
+ // ── Apple Health Save ──────────────────────────
+ if !healthKitSaved, HealthKitService.shared.isAvailable {
+ Button {
+ Task { await saveToHealth() }
+ } label: {
+ HStack {
+ Image(systemName: "heart.text.square.fill")
+ .foregroundStyle(.red)
+ Text(isSavingToHealth ? "Saving..." : "Save to Apple Health")
+ .fontWeight(.semibold)
+ Spacer()
+ if isSavingToHealth {
+ ProgressView()
+ } else {
+ Image(systemName: "chevron.right")
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding()
+ .glassCard()
+ }
+ .buttonStyle(.plain)
+ .disabled(isSavingToHealth)
+ .padding(.horizontal)
+ } else if healthKitSaved {
+ HStack {
+ Image(systemName: "checkmark.circle.fill")
+ .foregroundStyle(Theme.success)
+ Text("Saved to Apple Health")
+ .fontWeight(.semibold)
+ .foregroundStyle(.primary)
+ }
+ .padding()
+ .glassCard()
+ .padding(.horizontal)
+ }
+
+ // ── Actions ────────────────────────────────────
+ VStack(spacing: 12) {
+ Button {
+ showShareSheet = true
+ } label: {
+ Label("Share Workout", systemImage: "square.and.arrow.up")
+ .font(.headline)
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(.ultraThinMaterial)
+ .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
+ }
+ .buttonStyle(.plain)
+
+ Button {
+ onDone()
+ } label: {
+ Text("Back to Home")
+ .font(.headline.weight(.bold))
+ .foregroundStyle(.white)
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(Theme.brand)
+ .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
+ }
+ }
+ .padding(.horizontal)
+ .padding(.bottom, 40)
+ }
+ }
+ }
+ .navigationBarHidden(true)
+ .onAppear {
+ confettiTrigger += 1
+ }
+ .sheet(isPresented: $showShareSheet) {
+ if let session {
+ ShareSheet(text: generateShareText(session: session, program: program))
+ }
+ }
+ }
+
+ private func saveToHealth() async {
+ guard let session else { return }
+ isSavingToHealth = true
+ // Extract Sendable values on @MainActor before crossing into HealthKitService actor.
+ let saveData = HealthKitService.WorkoutSaveData(
+ startedAt: session.startedAt,
+ completedAt: session.completedAt,
+ caloriesBurned: session.caloriesBurned,
+ averageHeartRate: session.averageHeartRate
+ )
+ do {
+ try await HealthKitService.shared.requestAuthorization()
+ let workout = try await HealthKitService.shared.saveWorkout(saveData)
+ session.healthKitWorkoutId = workout.uuid
+ try? context.save()
+ healthKitSaved = true
+ AnalyticsService.shared.workoutCompleted(
+ programId: program.id,
+ durationSeconds: session.durationSeconds,
+ calories: session.caloriesBurned,
+ completionRate: session.completionRate,
+ healthKitSaved: true
+ )
+ } catch {
+ print("[Completion] HealthKit save failed: \(error)")
+ }
+ isSavingToHealth = false
+ }
+
+ private func generateShareText(session: WorkoutSession, program: WorkoutProgram) -> String {
+ "Just crushed a \(session.durationSeconds / 60)-minute \(program.titleEn) Tabata workout with TabataGo! 🔥 \(Int(session.caloriesBurned)) kcal burned."
+ }
+
+ private func formatDuration(_ seconds: Int) -> String {
+ let m = seconds / 60
+ let s = seconds % 60
+ return s > 0 ? "\(m)m \(s)s" : "\(m)m"
+ }
+}
+
+struct CompletionStat: View {
+ let label: String
+ let value: String
+ let icon: String
+ let color: Color
+
+ var body: some View {
+ VStack(spacing: 8) {
+ Image(systemName: icon)
+ .font(.system(size: 24, weight: .semibold))
+ .foregroundStyle(color)
+ Text(value)
+ .font(.system(size: 22, weight: .bold, design: .rounded))
+ .monospacedDigit()
+ Text(label)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 16)
+ .glassCard()
+ }
+}
+
+struct ShareSheet: UIViewControllerRepresentable {
+ let text: String
+
+ func makeUIViewController(context: Context) -> UIActivityViewController {
+ UIActivityViewController(activityItems: [text], applicationActivities: nil)
+ }
+
+ func updateUIViewController(_ vc: UIActivityViewController, context: Context) {}
+}
+
+#Preview {
+ CompletionView(session: nil, program: PreviewData.sampleProgram)
+ .modelContainer(TabataGoSchema.previewContainer)
+}
diff --git a/tabatago-swift/TabataGo/Views/Onboarding/OnboardingView.swift b/tabatago-swift/TabataGo/Views/Onboarding/OnboardingView.swift
new file mode 100644
index 0000000..aa8fe38
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Onboarding/OnboardingView.swift
@@ -0,0 +1,641 @@
+import SwiftUI
+import SwiftData
+
+/// Multi-step onboarding — 6-screen conversion funnel with polished animations.
+struct OnboardingView: View {
+ @State private var step: Step = .welcome
+ @State private var name = ""
+ @State private var fitnessLevel: FitnessLevel = .beginner
+ @State private var goal: FitnessGoal = .cardio
+ @State private var weeklyFrequency: Int = 3
+ @State private var selectedBarriers: Set = []
+ @Environment(\.modelContext) private var context
+
+ enum Step: Int, CaseIterable {
+ case welcome, name, level, goal, frequency, ready
+ var progress: Double { Double(rawValue) / Double(Step.allCases.count - 1) }
+ }
+
+ private let barriers = ["Time", "Motivation", "Equipment", "Knowledge", "Injuries", "Energy"]
+
+ var body: some View {
+ ZStack {
+ Theme.surfaceBackground
+ .ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ // ── Header: Progress + Back ──────────────────────
+ if step != .welcome {
+ VStack(spacing: 12) {
+ // Back button
+ HStack {
+ Button {
+ withAnimation(.spring(duration: 0.45)) {
+ if let prev = Step(rawValue: step.rawValue - 1) {
+ step = prev
+ }
+ }
+ } label: {
+ Image(systemName: "chevron.left")
+ .font(.system(size: 17, weight: .semibold))
+ .foregroundStyle(.secondary)
+ .frame(width: 44, height: 44)
+ }
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+
+ // Segmented progress bar
+ HStack(spacing: 4) {
+ ForEach(Step.allCases, id: \.rawValue) { s in
+ Capsule()
+ .fill(s.rawValue <= step.rawValue ? Theme.brand : Theme.surfaceElevated)
+ .frame(height: 4)
+ }
+ }
+ .padding(.horizontal, 24)
+ .animation(.spring(duration: 0.5), value: step)
+ }
+ .padding(.top, 8)
+ }
+
+ // ── Step Content ─────────────────────────────────
+ Group {
+ switch step {
+ case .welcome: WelcomeStep()
+ case .name: NameStep(name: $name, onContinue: { advance() })
+ case .level: LevelStep(selection: $fitnessLevel)
+ case .goal: GoalStep(selection: $goal)
+ case .frequency: FrequencyStep(frequency: $weeklyFrequency, barriers: $selectedBarriers, allBarriers: barriers)
+ case .ready: ReadyStep(name: name)
+ }
+ }
+ .transition(.asymmetric(
+ insertion: .opacity.combined(with: .offset(y: 20)),
+ removal: .opacity.combined(with: .offset(y: -10))
+ ))
+ .animation(.spring(duration: 0.45), value: step)
+
+ // ── Pinned bottom button ─────────────────────────
+ PrimaryButton(label: buttonLabel, action: buttonAction)
+ .disabled(step == .name && name.trimmingCharacters(in: .whitespaces).isEmpty)
+ .padding(.horizontal, 32)
+ .padding(.bottom, 48)
+ .padding(.top, 16)
+ }
+ }
+ }
+
+ private var buttonLabel: String {
+ switch step {
+ case .welcome: return "Get Started"
+ case .ready: return "Start My First Workout"
+ default: return "Continue"
+ }
+ }
+
+ private var buttonAction: () -> Void {
+ step == .ready ? completeOnboarding : advance
+ }
+
+ private func advance() {
+ guard let next = Step(rawValue: step.rawValue + 1) else { return }
+ withAnimation { step = next }
+ }
+
+ private func completeOnboarding() {
+ let profile = UserProfile()
+ profile.name = name.trimmingCharacters(in: .whitespaces)
+ profile.fitnessLevel = fitnessLevel
+ profile.goal = goal
+ profile.weeklyFrequency = weeklyFrequency
+ profile.barriers = Array(selectedBarriers)
+ profile.onboardingCompleted = true
+ profile.joinDate = Date()
+ context.insert(profile)
+ try? context.save()
+ AnalyticsService.shared.onboardingCompleted(
+ name: profile.name,
+ level: fitnessLevel.rawValue,
+ goal: goal.rawValue,
+ frequency: weeklyFrequency
+ )
+ }
+}
+
+// ─── Step Views ───────────────────────────────────────────────────
+
+private struct WelcomeStep: View {
+ @State private var showPills = false
+ @State private var pillStates = [false, false, false]
+
+ private let pills = [
+ ("bolt.fill", "4-Min Workouts"),
+ ("house.fill", "No Equipment"),
+ ("mic.fill", "Voice-Guided"),
+ ]
+
+ var body: some View {
+ VStack(spacing: 40) {
+ Spacer()
+
+ // Hero icon
+ Image(systemName: "bolt.fill")
+ .font(.system(size: 88))
+ .foregroundStyle(Theme.brand.gradient)
+ .symbolEffect(.pulse)
+
+ VStack(spacing: 14) {
+ Text("TabataGo")
+ .font(.system(size: 44, weight: .black, design: .rounded))
+ .foregroundStyle(.primary)
+
+ Text("High-intensity Tabata workouts,\ndesigned for real results.")
+ .font(.title3)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ }
+
+ // Feature pills
+ HStack(spacing: 10) {
+ ForEach(Array(pills.enumerated()), id: \.offset) { i, pill in
+ HStack(spacing: 6) {
+ Image(systemName: pill.0)
+ .font(.caption.weight(.semibold))
+ .foregroundStyle(Theme.brand)
+ Text(pill.1)
+ .font(.caption.weight(.medium))
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ .background(Theme.brand.opacity(0.1))
+ .clipShape(Capsule())
+ .overlay { Capsule().stroke(Theme.brand.opacity(0.2), lineWidth: 1) }
+ .opacity(pillStates[i] ? 1 : 0)
+ .offset(y: pillStates[i] ? 0 : 10)
+ }
+ }
+
+ Spacer()
+ }
+ .onAppear {
+ for i in 0..<3 {
+ withAnimation(.spring(duration: 0.5).delay(0.4 + Double(i) * 0.12)) {
+ pillStates[i] = true
+ }
+ }
+ }
+ }
+}
+
+private struct NameStep: View {
+ @Binding var name: String
+ let onContinue: () -> Void
+ @FocusState private var focused: Bool
+
+ var body: some View {
+ VStack(spacing: 32) {
+ Spacer()
+
+ OnboardingHeader(title: "What's your name?", subtitle: "We'll personalise your experience.")
+
+ VStack(spacing: 16) {
+ TextField("Enter your name", text: $name)
+ .font(.title2)
+ .multilineTextAlignment(.center)
+ .padding()
+ .background(Theme.surfaceCard)
+ .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
+ .overlay {
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .stroke(focused ? Theme.brand.opacity(0.6) : Theme.border, lineWidth: focused ? 2 : 1)
+ }
+ .padding(.horizontal, 32)
+ .focused($focused)
+ .submitLabel(.continue)
+ .onSubmit { if !name.isEmpty { onContinue() } }
+
+ // Live greeting
+ if !name.trimmingCharacters(in: .whitespaces).isEmpty {
+ Text("Hey, \(name.trimmingCharacters(in: .whitespaces))! 👋")
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(Theme.brand)
+ .transition(.opacity.combined(with: .offset(y: 8)))
+ .animation(.spring(duration: 0.4), value: name)
+ }
+ }
+
+ Spacer()
+ }
+ .onAppear { focused = true }
+ }
+}
+
+private struct LevelStep: View {
+ @Binding var selection: FitnessLevel
+ @State private var appeared = false
+
+ var body: some View {
+ VStack(spacing: 32) {
+ Spacer()
+ OnboardingHeader(title: "What's your fitness level?", subtitle: "We'll recommend the right workouts.")
+
+ VStack(spacing: 12) {
+ ForEach(Array(FitnessLevel.allCases.enumerated()), id: \.element) { i, level in
+ SelectionCard(
+ label: level.label,
+ subtitle: levelDescription(level),
+ icon: levelIcon(level),
+ isSelected: selection == level,
+ color: Theme.levelColor(level.rawValue)
+ ) {
+ selection = level
+ }
+ .opacity(appeared ? 1 : 0)
+ .offset(y: appeared ? 0 : 14)
+ .animation(.spring(duration: 0.45).delay(Double(i) * 0.08), value: appeared)
+ }
+ }
+ .padding(.horizontal, 24)
+
+ Spacer()
+ }
+ .onAppear { appeared = true }
+ }
+
+ private func levelDescription(_ level: FitnessLevel) -> String {
+ switch level {
+ case .beginner: return "New to HIIT or returning after a break"
+ case .intermediate: return "Regular exerciser, ready for more intensity"
+ case .advanced: return "Experienced athlete seeking maximum challenge"
+ }
+ }
+
+ private func levelIcon(_ level: FitnessLevel) -> String {
+ switch level {
+ case .beginner: return "figure.walk"
+ case .intermediate: return "figure.run"
+ case .advanced: return "figure.highintensity.intervaltraining"
+ }
+ }
+}
+
+private struct GoalStep: View {
+ @Binding var selection: FitnessGoal
+ @State private var appeared = false
+
+ var body: some View {
+ VStack(spacing: 32) {
+ Spacer()
+ OnboardingHeader(title: "What's your main goal?", subtitle: "This helps us curate your program.")
+
+ VStack(spacing: 12) {
+ ForEach(Array(FitnessGoal.allCases.enumerated()), id: \.element) { i, goal in
+ SelectionCard(
+ label: goal.label,
+ subtitle: goalDescription(goal),
+ icon: goalIcon(goal),
+ isSelected: selection == goal,
+ color: Theme.brand
+ ) {
+ selection = goal
+ }
+ .opacity(appeared ? 1 : 0)
+ .offset(y: appeared ? 0 : 14)
+ .animation(.spring(duration: 0.45).delay(Double(i) * 0.08), value: appeared)
+ }
+ }
+ .padding(.horizontal, 24)
+
+ Spacer()
+ }
+ .onAppear { appeared = true }
+ }
+
+ private func goalDescription(_ goal: FitnessGoal) -> String {
+ switch goal {
+ case .weightLoss: return "Burn calories and reduce body fat"
+ case .cardio: return "Improve cardiovascular endurance"
+ case .strength: return "Build muscle and increase power"
+ case .wellness: return "Improve overall health and energy"
+ }
+ }
+
+ private func goalIcon(_ goal: FitnessGoal) -> String {
+ switch goal {
+ case .weightLoss: return "scalemass"
+ case .cardio: return "heart.fill"
+ case .strength: return "dumbbell.fill"
+ case .wellness: return "leaf.fill"
+ }
+ }
+}
+
+private struct FrequencyStep: View {
+ @Binding var frequency: Int
+ @Binding var barriers: Set
+ let allBarriers: [String]
+
+ var body: some View {
+ ScrollView {
+ VStack(spacing: 28) {
+ Spacer(minLength: 20)
+
+ OnboardingHeader(title: "How often can you train?", subtitle: "Be realistic — consistency beats intensity.")
+
+ // Frequency picker
+ HStack(spacing: 12) {
+ ForEach([2, 3, 5], id: \.self) { n in
+ Button {
+ withAnimation(.spring(duration: 0.25)) { frequency = n }
+ } label: {
+ VStack(spacing: 6) {
+ Text("\(n)x")
+ .font(.system(size: 28, weight: .black, design: .rounded))
+ Text("per week")
+ .font(.caption)
+ }
+ .foregroundStyle(frequency == n ? .white : .primary)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 22)
+ .background {
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .fill(frequency == n ? Theme.brand : Theme.surfaceCard)
+ }
+ .overlay {
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .stroke(frequency == n ? Theme.brand : Theme.border, lineWidth: frequency == n ? 0 : 1)
+ }
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(.horizontal, 24)
+
+ // Barriers
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Any challenges?")
+ .font(.headline)
+ .padding(.horizontal, 24)
+ Text("Optional — helps us personalise tips")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 24)
+
+ WrappingHStack(items: allBarriers, spacing: 10, lineSpacing: 10) { barrier in
+ Button {
+ withAnimation(.spring(duration: 0.25)) {
+ if barriers.contains(barrier) { barriers.remove(barrier) }
+ else { barriers.insert(barrier) }
+ }
+ } label: {
+ Text(barrier)
+ .font(.subheadline.weight(barriers.contains(barrier) ? .semibold : .regular))
+ .padding(.horizontal, 16)
+ .padding(.vertical, 10)
+ .background {
+ Capsule().fill(barriers.contains(barrier) ? Theme.brand.opacity(0.15) : Theme.surfaceCard)
+ }
+ .overlay {
+ Capsule().stroke(barriers.contains(barrier) ? Theme.brand : Theme.border, lineWidth: barriers.contains(barrier) ? 1.5 : 1)
+ }
+ }
+ .buttonStyle(.plain)
+ }
+ .padding(.horizontal, 24)
+ }
+ }
+ .padding(.bottom, 12)
+ }
+ .scrollDismissesKeyboard(.immediately)
+ }
+}
+
+private struct ReadyStep: View {
+ let name: String
+ @State private var showContent = false
+ @State private var iconStates = [false, false, false]
+
+ private let celebrationIcons = ["flame.fill", "bolt.fill", "star.fill"]
+ private let celebrationColors: [Color] = [Theme.brand, Theme.prep, Theme.success]
+
+ var body: some View {
+ VStack(spacing: 36) {
+ Spacer()
+
+ // Celebration icons
+ HStack(spacing: 20) {
+ ForEach(Array(celebrationIcons.enumerated()), id: \.offset) { i, icon in
+ Image(systemName: icon)
+ .font(.system(size: 28, weight: .bold))
+ .foregroundStyle(celebrationColors[i])
+ .scaleEffect(iconStates[i] ? 1 : 0)
+ .animation(.spring(duration: 0.5, bounce: 0.5).delay(Double(i) * 0.15), value: iconStates[i])
+ }
+ }
+
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 80))
+ .foregroundStyle(Theme.success)
+ .symbolEffect(.bounce)
+
+ VStack(spacing: 14) {
+ if name.trimmingCharacters(in: .whitespaces).isEmpty {
+ Text("You're all set!")
+ .font(.system(size: 34, weight: .black, design: .rounded))
+ .foregroundStyle(.primary)
+ .multilineTextAlignment(.center)
+ } else {
+ let trimmedName = name.trimmingCharacters(in: .whitespaces)
+ Text("You're all set, \(Text(trimmedName).foregroundStyle(Theme.brand))!")
+ .font(.system(size: 34, weight: .black, design: .rounded))
+ .foregroundStyle(.primary)
+ .multilineTextAlignment(.center)
+ }
+
+ Text("Your personalised Tabata plan is ready.")
+ .font(.title3)
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 24)
+
+ Spacer()
+ }
+ .onAppear {
+ for i in 0..<3 {
+ iconStates[i] = true
+ }
+ }
+ }
+}
+
+// ─── Reusable components ──────────────────────────────────────────
+
+struct OnboardingHeader: View {
+ let title: String
+ let subtitle: String
+
+ var body: some View {
+ VStack(spacing: 10) {
+ Text(title)
+ .font(.system(size: 30, weight: .bold, design: .rounded))
+ .foregroundStyle(.primary)
+ .multilineTextAlignment(.center)
+ Text(subtitle)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ }
+ .padding(.horizontal, 32)
+ }
+}
+
+struct SelectionCard: View {
+ let label: String
+ let subtitle: String
+ let icon: String
+ let isSelected: Bool
+ let color: Color
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 16) {
+ // Icon circle
+ Image(systemName: icon)
+ .font(.system(size: 22, weight: .semibold))
+ .foregroundStyle(isSelected ? color : .secondary)
+ .frame(width: 44, height: 44)
+ .background(
+ Circle()
+ .fill(isSelected ? color.opacity(0.12) : Theme.surfaceOverlay)
+ )
+
+ VStack(alignment: .leading, spacing: 3) {
+ Text(label)
+ .font(.headline)
+ .foregroundStyle(.primary)
+ Text(subtitle)
+ .font(.caption)
+ .foregroundStyle(isSelected ? Color.primary.opacity(0.7) : Color.secondary)
+ .lineLimit(2)
+ }
+ Spacer()
+ if isSelected {
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 22))
+ .foregroundStyle(color)
+ .transition(.scale.combined(with: .opacity))
+ }
+ }
+ .padding(16)
+ .background {
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .fill(isSelected ? color.opacity(0.08) : Theme.surfaceCard)
+ .overlay {
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .stroke(isSelected ? color : Theme.border, lineWidth: isSelected ? 2 : 1)
+ }
+ }
+ }
+ .buttonStyle(ScaleButtonStyle())
+ .animation(.spring(duration: 0.25), value: isSelected)
+ }
+}
+
+struct PrimaryButton: View {
+ let label: String
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(label)
+ .font(.headline.weight(.bold))
+ .foregroundStyle(.white)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 18)
+ .background(Theme.brand.gradient)
+ .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
+ }
+ .buttonStyle(ScaleButtonStyle())
+ }
+}
+
+/// Button style that adds a subtle press scale effect.
+struct ScaleButtonStyle: ButtonStyle {
+ func makeBody(configuration: Configuration) -> some View {
+ configuration.label
+ .scaleEffect(configuration.isPressed ? 0.97 : 1.0)
+ .animation(.spring(duration: 0.2), value: configuration.isPressed)
+ }
+}
+
+// ─── Wrapping HStack (proper flow layout) ─────────────────────────
+
+struct WrappingHStack: View {
+ let items: [Item]
+ let spacing: CGFloat
+ let lineSpacing: CGFloat
+ let content: (Item) -> Content
+
+ init(items: [Item], spacing: CGFloat = 8, lineSpacing: CGFloat = 8, @ViewBuilder content: @escaping (Item) -> Content) {
+ self.items = items
+ self.spacing = spacing
+ self.lineSpacing = lineSpacing
+ self.content = content
+ }
+
+ var body: some View {
+ _WrappingLayout(spacing: spacing, lineSpacing: lineSpacing) {
+ ForEach(Array(items.enumerated()), id: \.offset) { _, item in
+ content(item)
+ }
+ }
+ }
+}
+
+struct _WrappingLayout: Layout {
+ let spacing: CGFloat
+ let lineSpacing: CGFloat
+
+ func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
+ let result = layout(subviews: subviews, proposal: proposal)
+ return result.size
+ }
+
+ func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
+ let result = layout(subviews: subviews, proposal: proposal)
+ for (index, offset) in result.offsets.enumerated() {
+ subviews[index].place(at: CGPoint(x: bounds.minX + offset.x, y: bounds.minY + offset.y), proposal: .unspecified)
+ }
+ }
+
+ private func layout(subviews: Subviews, proposal: ProposedViewSize) -> (offsets: [CGPoint], size: CGSize) {
+ let maxWidth = proposal.width ?? .infinity
+ var offsets: [CGPoint] = []
+ var currentX: CGFloat = 0
+ var currentY: CGFloat = 0
+ var lineHeight: CGFloat = 0
+ var maxX: CGFloat = 0
+
+ for subview in subviews {
+ let size = subview.sizeThatFits(.unspecified)
+ if currentX + size.width > maxWidth, currentX > 0 {
+ currentX = 0
+ currentY += lineHeight + lineSpacing
+ lineHeight = 0
+ }
+ offsets.append(CGPoint(x: currentX, y: currentY))
+ lineHeight = max(lineHeight, size.height)
+ currentX += size.width + spacing
+ maxX = max(maxX, currentX - spacing)
+ }
+
+ return (offsets, CGSize(width: maxX, height: currentY + lineHeight))
+ }
+}
+
+#Preview {
+ OnboardingView()
+ .modelContainer(TabataGoSchema.previewContainer)
+}
diff --git a/tabatago-swift/TabataGo/Views/Paywall/PaywallView.swift b/tabatago-swift/TabataGo/Views/Paywall/PaywallView.swift
new file mode 100644
index 0000000..63ba9d1
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Paywall/PaywallView.swift
@@ -0,0 +1,210 @@
+import SwiftUI
+import RevenueCat
+
+/// RevenueCat paywall — shows available packages with Liquid Glass cards.
+struct PaywallView: View {
+ @StateObject private var vm = PurchaseViewModel()
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ ZStack {
+ Theme.surfaceBackground
+ .ignoresSafeArea()
+
+ ScrollView {
+ VStack(spacing: 28) {
+ // ── Close ──────────────────────────────────────
+ HStack {
+ Spacer()
+ Button { dismiss() } label: {
+ Image(systemName: "xmark")
+ .font(.system(size: 14, weight: .bold))
+ .foregroundStyle(.secondary)
+ .padding(8)
+ .background(.ultraThinMaterial)
+ .clipShape(Circle())
+ }
+ }
+ .padding(.horizontal)
+ .padding(.top, 16)
+
+ // ── Crown ──────────────────────────────────────
+ VStack(spacing: 12) {
+ Image(systemName: "crown.fill")
+ .font(.system(size: 56))
+ .foregroundStyle(
+ LinearGradient(colors: [.yellow, .orange], startPoint: .top, endPoint: .bottom)
+ )
+ .symbolEffect(.bounce, value: vm.isPurchasing)
+
+ Text("TabataGo Premium")
+ .font(.system(size: 32, weight: .black, design: .rounded))
+ .foregroundStyle(.primary)
+ Text("Unlock every workout, every week.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+
+ // ── Features ───────────────────────────────────
+ VStack(spacing: 12) {
+ FeatureRow(icon: "bolt.fill", color: Theme.brand, title: "Unlimited Workouts", subtitle: "Access all body zones & difficulty levels")
+ FeatureRow(icon: "heart.fill", color: .red, title: "HealthKit Sync", subtitle: "Every workout saved to Apple Health")
+ FeatureRow(icon: "icloud.fill", color: Theme.rest, title: "Progress Sync", subtitle: "Your history backed up to the cloud")
+ FeatureRow(icon: "waveform", color: Theme.success, title: "Voice Coaching", subtitle: "Audio guidance through every phase")
+ }
+ .padding(.horizontal)
+
+ // ── Packages ───────────────────────────────────
+ if let offerings = vm.offerings, let current = offerings.current {
+ VStack(spacing: 12) {
+ ForEach(current.availablePackages, id: \.identifier) { package in
+ PackageCard(
+ package: package,
+ isSelected: vm.selectedPackage?.identifier == package.identifier
+ ) {
+ vm.selectedPackage = package
+ }
+ }
+ }
+ .padding(.horizontal)
+ } else if vm.isPurchasing {
+ ProgressView().padding()
+ }
+
+ // ── CTA ────────────────────────────────────────
+ VStack(spacing: 12) {
+ Button {
+ Task { await vm.purchase() }
+ } label: {
+ HStack {
+ if vm.isPurchasing { ProgressView().tint(.white) }
+ Text(vm.isPurchasing ? "Processing..." : "Start Premium")
+ .font(.headline.weight(.bold))
+ }
+ .foregroundStyle(.white)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 18)
+ .background(
+ vm.selectedPackage == nil
+ ? AnyShapeStyle(Color.gray.opacity(0.4))
+ : AnyShapeStyle(Theme.brand.gradient)
+ )
+ .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
+ }
+ .disabled(vm.selectedPackage == nil || vm.isPurchasing)
+
+ Button {
+ Task { await vm.restorePurchases() }
+ } label: {
+ Text("Restore Purchases")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+
+ Text("Cancel anytime. Prices in your local currency.")
+ .font(.caption)
+ .foregroundStyle(.tertiary)
+ .multilineTextAlignment(.center)
+ }
+ .padding(.horizontal)
+ .padding(.bottom, 40)
+ }
+ }
+ }
+ .task { await vm.loadOfferings() }
+ .onAppear { AnalyticsService.shared.paywallViewed(source: "paywall_sheet") }
+ .alert("Error", isPresented: $vm.showError) {
+ Button("OK") {}
+ } message: {
+ Text(vm.errorMessage ?? "Something went wrong.")
+ }
+ }
+}
+
+struct FeatureRow: View {
+ let icon: String
+ let color: Color
+ let title: String
+ let subtitle: String
+
+ var body: some View {
+ HStack(spacing: 14) {
+ Image(systemName: icon)
+ .font(.system(size: 20, weight: .semibold))
+ .foregroundStyle(color)
+ .frame(width: 36, height: 36)
+ .background(color.opacity(0.12))
+ .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.subheadline.weight(.semibold))
+ Text(subtitle)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ }
+ }
+}
+
+struct PackageCard: View {
+ let package: Package
+ let isSelected: Bool
+ let action: () -> Void
+
+ private var isYearly: Bool {
+ package.packageType == .annual
+ }
+
+ var body: some View {
+ Button(action: action) {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack(spacing: 8) {
+ Text(package.storeProduct.localizedTitle)
+ .font(.headline.weight(.semibold))
+ if isYearly {
+ Text("BEST VALUE")
+ .font(.caption2.weight(.bold))
+ .foregroundStyle(.white)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(Theme.success)
+ .clipShape(Capsule())
+ }
+ }
+ if isYearly {
+ Text("\(package.storeProduct.localizedPriceString) / year — save 40%")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ } else {
+ Text("\(package.storeProduct.localizedPriceString) / month")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ }
+ Spacer()
+ Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
+ .font(.system(size: 22))
+ .foregroundStyle(isSelected ? Theme.brand : .secondary)
+ }
+ .padding(16)
+ .background {
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .fill(isSelected ? Theme.brand.opacity(0.08) : Theme.surfaceCard)
+ .overlay {
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .stroke(isSelected ? Theme.brand : .clear, lineWidth: 2)
+ }
+ }
+ }
+ .buttonStyle(.plain)
+ .animation(.spring(duration: 0.25), value: isSelected)
+ }
+}
+
+#Preview {
+ PaywallView()
+ .modelContainer(TabataGoSchema.previewContainer)
+}
diff --git a/tabatago-swift/TabataGo/Views/Player/NowPlayingView.swift b/tabatago-swift/TabataGo/Views/Player/NowPlayingView.swift
new file mode 100644
index 0000000..0fed6c9
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Player/NowPlayingView.swift
@@ -0,0 +1,57 @@
+import SwiftUI
+
+/// Floating pill showing the current music track with a skip button.
+/// Mirrors the Expo NowPlaying component.
+struct NowPlayingView: View {
+ let track: MusicTrack?
+ let isReady: Bool
+ let onSkip: () -> Void
+
+ @State private var isVisible = false
+
+ var body: some View {
+ if let track, isReady {
+ HStack(spacing: 8) {
+ // Music note icon
+ Image(systemName: "music.note")
+ .font(.system(size: 12, weight: .semibold))
+ .foregroundStyle(Theme.success)
+ .frame(width: 28, height: 28)
+ .background(Theme.success.opacity(0.15))
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
+
+ // Track info
+ VStack(alignment: .leading, spacing: 1) {
+ Text(track.title)
+ .font(.caption.weight(.semibold))
+ .foregroundStyle(.white)
+ .lineLimit(1)
+ Text(track.artist)
+ .font(.caption2)
+ .foregroundStyle(.white.opacity(0.55))
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ // Skip button
+ Button(action: onSkip) {
+ Image(systemName: "forward.fill")
+ .font(.system(size: 11, weight: .semibold))
+ .foregroundStyle(.white.opacity(0.6))
+ .frame(width: 32, height: 32)
+ }
+ .buttonStyle(.plain)
+ }
+ .padding(.vertical, 6)
+ .padding(.horizontal, 12)
+ .background(.ultraThinMaterial)
+ .clipShape(Capsule())
+ .overlay(Capsule().stroke(.white.opacity(0.1), lineWidth: 1))
+ .opacity(isVisible ? 1 : 0)
+ .offset(y: isVisible ? 0 : 20)
+ .onAppear { withAnimation(.spring(duration: 0.4, bounce: 0.3)) { isVisible = true } }
+ .onDisappear { isVisible = false }
+ .transition(.opacity.combined(with: .move(edge: .bottom)))
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift
new file mode 100644
index 0000000..de9e60e
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift
@@ -0,0 +1,338 @@
+import SwiftUI
+
+/// Full-screen Tabata workout player with Liquid Glass timer.
+struct PlayerView: View {
+ let program: WorkoutProgram
+ @StateObject private var vm: PlayerViewModel
+ @StateObject private var musicVM: MusicPlayerViewModel
+ @Environment(\.dismiss) private var dismiss
+ @Environment(\.modelContext) private var context
+
+ init(program: WorkoutProgram) {
+ self.program = program
+ _vm = StateObject(wrappedValue: PlayerViewModel(program: program))
+ let vibe = MusicVibe(rawValue: program.musicVibe) ?? .electronic
+ _musicVM = StateObject(wrappedValue: MusicPlayerViewModel(vibe: vibe))
+ }
+
+ var body: some View {
+ NavigationStack {
+ ZStack {
+ // ── Animated Phase Background ──────────────────────────
+ PhaseBackground(phase: vm.phase)
+ .ignoresSafeArea()
+
+ // ── Content ────────────────────────────────────────────
+ VStack(spacing: 0) {
+ PlayerTopBar(
+ title: program.titleEn,
+ block: vm.currentBlockIndex + 1,
+ totalBlocks: program.blocks.count,
+ onClose: { vm.showExitConfirmation = true }
+ )
+
+ Spacer()
+
+ // ── Exercise Label ─────────────────────────────────
+ if let exercise = vm.currentExercise {
+ Text(exercise.nameEn)
+ .font(.system(size: 32, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 32)
+ .transition(.opacity.combined(with: .scale(scale: 0.9)))
+ }
+
+ // ── Phase Badge ────────────────────────────────────
+ Text(Theme.phaseLabel(vm.phase))
+ .font(Theme.phaseFont)
+ .foregroundStyle(.white.opacity(0.85))
+ .padding(.top, 8)
+
+ Spacer()
+
+ // ── Timer Ring ─────────────────────────────────────
+ TimerRing(
+ timeRemaining: vm.timeRemaining,
+ total: vm.totalPhaseTime,
+ phase: vm.phase
+ )
+
+ Spacer()
+
+ // ── Round Counter ──────────────────────────────────
+ RoundCounter(
+ current: vm.currentRound,
+ total: vm.totalRoundsInBlock,
+ phase: vm.phase
+ )
+
+ // ── Live Stats (HealthKit) ─────────────────────────
+ if vm.heartRate > 0 || vm.liveCalories > 0 {
+ LiveStatsBar(heartRate: vm.heartRate, calories: vm.liveCalories)
+ .padding(.top, 12)
+ }
+
+ // ── Now Playing (Music) ───────────────────────────
+ NowPlayingView(
+ track: musicVM.currentTrack,
+ isReady: musicVM.isReady,
+ onSkip: { musicVM.skipTrack() }
+ )
+ .padding(.horizontal, 32)
+ .padding(.top, 8)
+
+ Spacer()
+
+ // ── Controls ───────────────────────────────────────
+ PlayerControls(
+ isRunning: vm.isRunning,
+ isPaused: vm.isPaused,
+ onStartPause: { vm.togglePlayPause() },
+ onSkip: { vm.skipPhase() }
+ )
+ .padding(.bottom, 40)
+ }
+ }
+ .navigationBarHidden(true)
+ .statusBarHidden(true)
+ .preferredColorScheme(.dark)
+ .onAppear {
+ vm.setup(modelContext: context)
+ UIApplication.shared.isIdleTimerDisabled = true
+ Task { await musicVM.load() }
+ }
+ .onDisappear {
+ UIApplication.shared.isIdleTimerDisabled = false
+ musicVM.stop()
+ }
+ .onChange(of: vm.isRunning) { _, running in
+ let musicPhase = vm.phase != .prep && vm.phase != .warmup && vm.phase != .complete
+ musicVM.setPlaying(running && !vm.isPaused && musicPhase)
+ }
+ .onChange(of: vm.isPaused) { _, paused in
+ let musicPhase = vm.phase != .prep && vm.phase != .warmup && vm.phase != .complete
+ musicVM.setPlaying(vm.isRunning && !paused && musicPhase)
+ }
+ .onChange(of: vm.phase) { _, phase in
+ let musicPhase = phase != .prep && phase != .warmup && phase != .complete
+ musicVM.setPlaying(vm.isRunning && !vm.isPaused && musicPhase)
+ }
+ .navigationDestination(isPresented: $vm.isComplete) {
+ CompletionView(session: vm.completedSession, program: program, onDone: { dismiss() })
+ .navigationBarBackButtonHidden()
+ }
+ .alert("End Workout?", isPresented: $vm.showExitConfirmation) {
+ Button("End Workout", role: .destructive) {
+ vm.abandonWorkout()
+ dismiss()
+ }
+ Button("Keep Going", role: .cancel) {}
+ } message: {
+ Text("Your progress will not be saved.")
+ }
+ } // NavigationStack
+ }
+}
+
+// ─── Sub-components ───────────────────────────────────────────────
+
+struct PhaseBackground: View {
+ let phase: TimerPhase
+ @State private var animating = false
+
+ var body: some View {
+ ZStack {
+ Color.black
+ RadialGradient(
+ colors: [Theme.phaseColor(phase).opacity(0.45), .clear],
+ center: .center,
+ startRadius: 0,
+ endRadius: 400
+ )
+ .scaleEffect(animating ? 1.15 : 1.0)
+ .animation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true), value: animating)
+ }
+ .onChange(of: phase) { _, _ in animating = false; animating = true }
+ .onAppear { animating = true }
+ }
+}
+
+struct PlayerTopBar: View {
+ let title: String
+ let block: Int
+ let totalBlocks: Int
+ let onClose: () -> Void
+
+ var body: some View {
+ HStack {
+ Button(action: onClose) {
+ Image(systemName: "xmark")
+ .font(.system(size: 17, weight: .semibold))
+ .foregroundStyle(.white)
+ .padding(10)
+ .background(.ultraThinMaterial)
+ .clipShape(Circle())
+ }
+
+ Spacer()
+
+ VStack(spacing: 2) {
+ Text(title)
+ .font(.subheadline.weight(.semibold))
+ .foregroundStyle(.white)
+ .lineLimit(1)
+ Text("Block \(block) of \(totalBlocks)")
+ .font(.caption)
+ .foregroundStyle(.white.opacity(0.7))
+ }
+
+ Spacer()
+
+ // Placeholder for symmetry
+ Color.clear.frame(width: 37, height: 37)
+ }
+ .padding(.horizontal, 20)
+ .padding(.top, 16)
+ }
+}
+
+struct TimerRing: View {
+ let timeRemaining: Int
+ let total: Int
+ let phase: TimerPhase
+
+ private var progress: Double {
+ guard total > 0 else { return 1 }
+ return Double(timeRemaining) / Double(total)
+ }
+
+ var body: some View {
+ ZStack {
+ // Background ring
+ Circle()
+ .stroke(.white.opacity(0.1), lineWidth: 16)
+ .frame(width: 240, height: 240)
+
+ // Progress ring
+ Circle()
+ .trim(from: 0, to: progress)
+ .stroke(
+ Theme.phaseColor(phase),
+ style: StrokeStyle(lineWidth: 16, lineCap: .round)
+ )
+ .frame(width: 240, height: 240)
+ .rotationEffect(.degrees(-90))
+ .animation(.linear(duration: 1), value: progress)
+
+ // Glass disc
+ Circle()
+ .fill(.ultraThinMaterial)
+ .frame(width: 200, height: 200)
+
+ // Timer digits
+ Text("\(timeRemaining)")
+ .font(Theme.timerFont)
+ .foregroundStyle(.white)
+ .monospacedDigit()
+ .contentTransition(.numericText(countsDown: true))
+ .animation(.spring(duration: 0.3), value: timeRemaining)
+ }
+ }
+}
+
+struct RoundCounter: View {
+ let current: Int
+ let total: Int
+ let phase: TimerPhase
+
+ var body: some View {
+ HStack(spacing: 8) {
+ ForEach(1...max(total, 1), id: \.self) { i in
+ Capsule()
+ .fill(i < current ? Theme.phaseColor(phase) :
+ i == current ? .white :
+ .white.opacity(0.25))
+ .frame(width: i == current ? 24 : 8, height: 8)
+ .animation(.spring(duration: 0.3), value: current)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+}
+
+struct LiveStatsBar: View {
+ let heartRate: Double
+ let calories: Double
+
+ var body: some View {
+ HStack(spacing: 24) {
+ if heartRate > 0 {
+ HStack(spacing: 6) {
+ Image(systemName: "heart.fill")
+ .foregroundStyle(.red)
+ Text("\(Int(heartRate)) bpm")
+ .font(.subheadline.weight(.semibold))
+ .foregroundStyle(.white)
+ .monospacedDigit()
+ }
+ }
+ if calories > 0 {
+ HStack(spacing: 6) {
+ Image(systemName: "flame.fill")
+ .foregroundStyle(Theme.brand)
+ Text("\(Int(calories)) kcal")
+ .font(.subheadline.weight(.semibold))
+ .foregroundStyle(.white)
+ .monospacedDigit()
+ }
+ }
+ }
+ .padding(.horizontal, 24)
+ .padding(.vertical, 10)
+ .background(.ultraThinMaterial)
+ .clipShape(Capsule())
+ }
+}
+
+struct PlayerControls: View {
+ let isRunning: Bool
+ let isPaused: Bool
+ let onStartPause: () -> Void
+ let onSkip: () -> Void
+
+ var body: some View {
+ HStack(spacing: 40) {
+ // Skip button
+ Button(action: onSkip) {
+ Image(systemName: "forward.end.fill")
+ .font(.system(size: 22, weight: .semibold))
+ .foregroundStyle(.white.opacity(0.8))
+ .padding(16)
+ .background(.ultraThinMaterial)
+ .clipShape(Circle())
+ }
+
+ // Play / Pause
+ Button(action: onStartPause) {
+ Image(systemName: isRunning && !isPaused ? "pause.fill" : "play.fill")
+ .font(.system(size: 32, weight: .bold))
+ .foregroundStyle(.white)
+ .frame(width: 72, height: 72)
+ .background(Theme.phaseColor(.work))
+ .clipShape(Circle())
+ .shadow(color: Theme.brand.opacity(0.4), radius: 16, y: 6)
+ }
+ .scaleEffect(isRunning ? 1.0 : 1.05)
+ .animation(.spring(duration: 0.3), value: isRunning)
+
+ // Spacer for symmetry
+ Color.clear.frame(width: 54, height: 54)
+ }
+ }
+}
+
+#Preview {
+ PlayerView(program: PreviewData.sampleProgram)
+ .modelContainer(TabataGoSchema.previewContainer)
+}
diff --git a/tabatago-swift/TabataGo/Views/Programs/BodyZoneView.swift b/tabatago-swift/TabataGo/Views/Programs/BodyZoneView.swift
new file mode 100644
index 0000000..c3f89ba
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Programs/BodyZoneView.swift
@@ -0,0 +1,68 @@
+import SwiftUI
+
+/// Programs filtered by body zone (upper / lower / full).
+struct BodyZoneView: View {
+ let zone: String
+ @StateObject private var vm = HomeViewModel()
+ @State private var selectedProgram: WorkoutProgram? = nil
+
+ private var zoneTitle: String {
+ switch zone {
+ case "upper-body": return "Upper Body"
+ case "lower-body": return "Lower Body"
+ case "full-body": return "Full Body"
+ default: return zone.replacingOccurrences(of: "-", with: " ").capitalized
+ }
+ }
+
+ private var programs: [WorkoutProgram] {
+ vm.allPrograms.filter { $0.bodyZone == zone }
+ }
+
+ var body: some View {
+ List {
+ if vm.isLoading {
+ ProgressView().frame(maxWidth: .infinity, alignment: .center)
+ .listRowBackground(Color.clear)
+ } else if let error = vm.error {
+ VStack(spacing: 8) {
+ Image(systemName: "exclamationmark.triangle")
+ .font(.title2)
+ .foregroundStyle(.secondary)
+ Text("Failed to load programs")
+ .font(.subheadline.weight(.semibold))
+ Text(error)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ Button("Retry") { Task { await vm.refresh() } }
+ .buttonStyle(.bordered)
+ .padding(.top, 4)
+ }
+ .frame(maxWidth: .infinity, minHeight: 120)
+ .listRowBackground(Color.clear)
+ } else if programs.isEmpty {
+ ContentUnavailableView(
+ "No Programs Yet",
+ systemImage: "dumbbell",
+ description: Text("Programs for \(zoneTitle) are coming soon.")
+ )
+ .listRowBackground(Color.clear)
+ } else {
+ ForEach(programs) { program in
+ ProgramRow(program: program)
+ .listRowBackground(Color.clear)
+ .listRowInsets(.init(top: 4, leading: 16, bottom: 4, trailing: 16))
+ .onTapGesture { selectedProgram = program }
+ }
+ }
+ }
+ .listStyle(.plain)
+ .navigationTitle(zoneTitle)
+ .navigationBarTitleDisplayMode(.large)
+ .task { await vm.loadPrograms() }
+ .sheet(item: $selectedProgram) { program in
+ ProgramDetailView(program: program)
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Views/Programs/ProgramDetailView.swift b/tabatago-swift/TabataGo/Views/Programs/ProgramDetailView.swift
new file mode 100644
index 0000000..a3bb415
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Programs/ProgramDetailView.swift
@@ -0,0 +1,221 @@
+import SwiftUI
+import SwiftData
+
+/// Program detail — exercise list, warmup/cooldown, start button.
+struct ProgramDetailView: View {
+ let program: WorkoutProgram
+ @Query private var profiles: [UserProfile]
+ @Environment(\.dismiss) private var dismiss
+ @State private var showingPlayer = false
+ @State private var showingPaywall = false
+ @State private var selectedBlock: TabataBlock? = nil
+ @StateObject private var purchaseVM = PurchaseViewModel()
+
+ private var profile: UserProfile? { profiles.first }
+ private var canAccess: Bool {
+ program.isFree || (profile?.subscription.isPremium == true)
+ }
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(spacing: 0) {
+ // ── Hero Banner ────────────────────────────────
+ ZStack(alignment: .bottomLeading) {
+ Rectangle()
+ .fill(Theme.zoneGradient(program.bodyZone))
+ .frame(height: 240)
+
+ VStack(alignment: .leading, spacing: 8) {
+ LevelBadge(level: program.level)
+ Text(program.titleEn)
+ .font(.system(size: 28, weight: .black, design: .rounded))
+ .foregroundStyle(.white)
+ HStack(spacing: 16) {
+ Label("\(program.estimatedDuration) min", systemImage: "clock.fill")
+ Label("\(program.estimatedCalories) kcal", systemImage: "flame.fill")
+ Label("\(program.totalRounds) rounds", systemImage: "repeat")
+ }
+ .font(.subheadline.weight(.medium))
+ .foregroundStyle(.white.opacity(0.85))
+ }
+ .padding(20)
+ }
+
+ VStack(alignment: .leading, spacing: 20) {
+ // ── Description ────────────────────────────
+ if !program.descriptionEn.isEmpty {
+ Text(program.descriptionEn)
+ .font(.body)
+ .foregroundStyle(.secondary)
+ .padding(.horizontal)
+ }
+
+ // ── Warmup ─────────────────────────────────
+ if !program.warmup.movements.isEmpty {
+ ExerciseSection(title: "Warm Up", icon: "figure.cooldown", color: Theme.prep) {
+ ForEach(program.warmup.movements, id: \.name) { move in
+ ExerciseRow(name: move.nameEn, duration: "\(move.duration)s", color: Theme.prep)
+ }
+ }
+ }
+
+ // ── Tabata Blocks ──────────────────────────
+ ForEach(Array(program.blocks.enumerated()), id: \.offset) { i, block in
+ ExerciseSection(
+ title: "Block \(i + 1)",
+ icon: "bolt.fill",
+ color: Theme.brand,
+ subtitle: "\(block.rounds) rounds · \(block.workTime)s work / \(block.restTime)s rest"
+ ) {
+ ExerciseRow(
+ name: block.exercise1.nameEn,
+ duration: "\(block.workTime)s",
+ tip: block.exercise1.tipEn,
+ color: Theme.brand
+ )
+ Divider().padding(.leading, 36)
+ ExerciseRow(
+ name: block.exercise2.nameEn,
+ duration: "\(block.workTime)s",
+ tip: block.exercise2.tipEn,
+ color: Theme.brand
+ )
+ }
+ }
+
+ // ── Cooldown ───────────────────────────────
+ if !program.cooldown.movements.isEmpty {
+ ExerciseSection(title: "Cool Down", icon: "snowflake", color: Theme.rest) {
+ ForEach(program.cooldown.movements, id: \.name) { move in
+ ExerciseRow(name: move.nameEn, duration: "\(move.duration)s", color: Theme.rest)
+ }
+ }
+ }
+
+ Spacer(minLength: 120)
+ }
+ .padding(.top, 20)
+ }
+ }
+ .ignoresSafeArea(edges: .top)
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button { dismiss() } label: {
+ Image(systemName: "chevron.down")
+ .font(.system(size: 17, weight: .semibold))
+ }
+ }
+ }
+ .safeAreaInset(edge: .bottom) {
+ // ── Start Button ───────────────────────────────────
+ VStack(spacing: 0) {
+ Divider()
+ Button {
+ if canAccess { showingPlayer = true }
+ else { showingPaywall = true }
+ } label: {
+ HStack(spacing: 10) {
+ if !canAccess {
+ Image(systemName: "lock.fill")
+ }
+ Text(canAccess ? "Start Workout" : "Unlock Premium")
+ .font(.headline.weight(.bold))
+ }
+ .foregroundStyle(.white)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 18)
+ .background(canAccess ? AnyShapeStyle(Theme.brand.gradient) : AnyShapeStyle(LinearGradient(colors: [.gray.opacity(0.6), .gray.opacity(0.4)], startPoint: .leading, endPoint: .trailing)))
+ .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
+ }
+ .padding(.horizontal, 24)
+ .padding(.vertical, 16)
+ }
+ .background(.ultraThinMaterial)
+ }
+ .fullScreenCover(isPresented: $showingPlayer) {
+ PlayerView(program: program)
+ }
+ .sheet(isPresented: $showingPaywall) {
+ PaywallView()
+ }
+ }
+ }
+}
+
+// ─── Components ───────────────────────────────────────────────────
+
+struct ExerciseSection: View {
+ let title: String
+ let icon: String
+ let color: Color
+ var subtitle: String? = nil
+ @ViewBuilder let content: () -> Content
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ HStack(spacing: 10) {
+ Image(systemName: icon)
+ .foregroundStyle(color)
+ .frame(width: 24)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.headline.weight(.semibold))
+ if let subtitle {
+ Text(subtitle)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .padding(.horizontal)
+ .padding(.bottom, 10)
+
+ VStack(spacing: 0) {
+ content()
+ }
+ .background(.ultraThinMaterial)
+ .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
+ .padding(.horizontal)
+ }
+ }
+}
+
+struct ExerciseRow: View {
+ let name: String
+ let duration: String
+ var tip: String? = nil
+ let color: Color
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Circle()
+ .fill(color.opacity(0.25))
+ .frame(width: 8, height: 8)
+ .padding(.leading, 12)
+ Text(name)
+ .font(.subheadline.weight(.medium))
+ Spacer()
+ Text(duration)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .monospacedDigit()
+ .padding(.trailing, 12)
+ }
+ if let tip {
+ Text(tip)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .padding(.leading, 32)
+ }
+ }
+ .padding(.vertical, 12)
+ }
+}
+
+#Preview {
+ ProgramDetailView(program: PreviewData.sampleProgram)
+ .modelContainer(TabataGoSchema.previewContainer)
+}
diff --git a/tabatago-swift/TabataGo/Views/Settings/PolicyViews.swift b/tabatago-swift/TabataGo/Views/Settings/PolicyViews.swift
new file mode 100644
index 0000000..4946324
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Settings/PolicyViews.swift
@@ -0,0 +1,79 @@
+import SwiftUI
+
+struct PrivacyPolicyView: View {
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 20) {
+ Group {
+ PolicySection(title: "Data We Collect") {
+ Text("TabataGo collects minimal data to provide you with a great workout experience. We collect your name, fitness preferences, and workout history locally on your device.")
+ }
+ PolicySection(title: "Apple Health") {
+ Text("When you grant permission, TabataGo saves your Tabata workouts to Apple Health, including calories burned, heart rate, and workout duration. This data stays on your device and is governed by Apple's privacy policies.")
+ }
+ PolicySection(title: "Analytics") {
+ Text("We use PostHog to collect anonymised usage analytics to improve the app. No personally identifiable information is sent. You can opt out in your device privacy settings.")
+ }
+ PolicySection(title: "Purchases") {
+ Text("Subscription purchases are handled by Apple's App Store and RevenueCat. We do not store your payment information.")
+ }
+ PolicySection(title: "Data Storage") {
+ Text("Your workout history, profile, and settings are stored locally using SwiftData. If you enable cloud sync, data is securely stored in Supabase with industry-standard encryption.")
+ }
+ PolicySection(title: "Contact") {
+ Text("For privacy concerns, contact us at privacy@tabatago.app")
+ }
+ }
+ .padding(.horizontal)
+ }
+ .padding(.vertical)
+ }
+ .navigationTitle("Privacy Policy")
+ .navigationBarTitleDisplayMode(.large)
+ }
+}
+
+struct TermsOfServiceView: View {
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 20) {
+ Group {
+ PolicySection(title: "Use of the App") {
+ Text("TabataGo is designed for fitness purposes. By using the app, you agree to use it responsibly and consult a healthcare professional before starting any new exercise program.")
+ }
+ PolicySection(title: "Subscription") {
+ Text("Premium subscriptions are billed monthly or yearly through the App Store. Subscriptions automatically renew unless cancelled at least 24 hours before the renewal date.")
+ }
+ PolicySection(title: "Health Disclaimer") {
+ Text("TabataGo is not a medical device. The app does not provide medical advice. Always consult a doctor before beginning a new exercise program, especially if you have pre-existing health conditions.")
+ }
+ PolicySection(title: "Limitation of Liability") {
+ Text("TabataGo is provided 'as is'. We are not liable for any injuries or health issues arising from the use of our workout programs.")
+ }
+ PolicySection(title: "Changes to Terms") {
+ Text("We may update these terms at any time. Continued use of the app after changes constitutes acceptance of the new terms.")
+ }
+ }
+ .padding(.horizontal)
+ }
+ .padding(.vertical)
+ }
+ .navigationTitle("Terms of Service")
+ .navigationBarTitleDisplayMode(.large)
+ }
+}
+
+struct PolicySection: View {
+ let title: String
+ @ViewBuilder let content: () -> Content
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text(title)
+ .font(.headline.weight(.semibold))
+ content()
+ .font(.body)
+ .foregroundStyle(.secondary)
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGo/Views/Settings/SettingsView.swift b/tabatago-swift/TabataGo/Views/Settings/SettingsView.swift
new file mode 100644
index 0000000..825ec12
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Settings/SettingsView.swift
@@ -0,0 +1,170 @@
+import SwiftUI
+import SwiftData
+
+/// Settings screen — haptics, audio, voice coaching, reminders, account.
+struct SettingsView: View {
+ @Query private var profiles: [UserProfile]
+ @Environment(\.modelContext) private var context
+ @State private var showingResetAlert = false
+
+ private var profile: UserProfile? { profiles.first }
+
+ var body: some View {
+ Form {
+ // ── Audio ──────────────────────────────────────────────
+ Section("Audio") {
+ if let profile {
+ Toggle("Sound Effects", isOn: Binding(
+ get: { profile.soundEffectsEnabled },
+ set: { profile.soundEffectsEnabled = $0; save() }
+ ))
+
+ Toggle("Voice Coaching", isOn: Binding(
+ get: { profile.voiceCoachingEnabled },
+ set: { profile.voiceCoachingEnabled = $0; save() }
+ ))
+
+ Toggle("Music", isOn: Binding(
+ get: { profile.musicEnabled },
+ set: { profile.musicEnabled = $0; save() }
+ ))
+
+ if profile.musicEnabled {
+ HStack {
+ Image(systemName: "speaker.fill")
+ .foregroundStyle(.secondary)
+ Slider(value: Binding(
+ get: { profile.musicVolume },
+ set: { profile.musicVolume = $0; save() }
+ ))
+ Image(systemName: "speaker.wave.3.fill")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+
+ // ── Haptics ────────────────────────────────────────────
+ Section("Haptics") {
+ if let profile {
+ Toggle("Haptic Feedback", isOn: Binding(
+ get: { profile.hapticsEnabled },
+ set: { profile.hapticsEnabled = $0; save() }
+ ))
+ }
+ }
+
+ // ── Reminders ─────────────────────────────────────────
+ Section("Reminders") {
+ if let profile {
+ Toggle("Daily Reminder", isOn: Binding(
+ get: { profile.remindersEnabled },
+ set: { profile.remindersEnabled = $0; save() }
+ ))
+
+ if profile.remindersEnabled {
+ DatePicker(
+ "Reminder Time",
+ selection: Binding(
+ get: {
+ var c = DateComponents()
+ c.hour = profile.reminderTimeHour
+ c.minute = profile.reminderTimeMinute
+ return Calendar.current.date(from: c) ?? Date()
+ },
+ set: { date in
+ let c = Calendar.current.dateComponents([.hour, .minute], from: date)
+ profile.reminderTimeHour = c.hour ?? 9
+ profile.reminderTimeMinute = c.minute ?? 0
+ save()
+ }
+ ),
+ displayedComponents: .hourAndMinute
+ )
+ }
+ }
+ }
+
+ // ── HealthKit ─────────────────────────────────────────
+ Section("Apple Health") {
+ Button {
+ Task { try? await HealthKitService.shared.requestAuthorization() }
+ } label: {
+ HStack {
+ Label("Manage Health Permissions", systemImage: "heart.text.square")
+ Spacer()
+ Image(systemName: "arrow.up.right")
+ .font(.caption)
+ .foregroundStyle(.tertiary)
+ }
+ }
+ .buttonStyle(.plain)
+ }
+
+ // ── Account ────────────────────────────────────────────
+ Section("Account") {
+ if let profile {
+ HStack {
+ Text("Name")
+ Spacer()
+ Text(profile.name.isEmpty ? "Not set" : profile.name)
+ .foregroundStyle(.secondary)
+ }
+ HStack {
+ Text("Joined")
+ Spacer()
+ Text(profile.joinDate.formatted(date: .abbreviated, time: .omitted))
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ Button(role: .destructive) {
+ showingResetAlert = true
+ } label: {
+ Label("Reset All Progress", systemImage: "trash")
+ .foregroundStyle(.red)
+ }
+ }
+
+ // ── About ──────────────────────────────────────────────
+ Section("About") {
+ HStack {
+ Text("Version")
+ Spacer()
+ Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0")
+ .foregroundStyle(.secondary)
+ }
+ NavigationLink("Privacy Policy") { PrivacyPolicyView() }
+ NavigationLink("Terms of Service") { TermsOfServiceView() }
+ }
+ }
+ .navigationTitle("Settings")
+ .navigationBarTitleDisplayMode(.large)
+ .alert("Reset All Progress?", isPresented: $showingResetAlert) {
+ Button("Reset", role: .destructive) { resetProgress() }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("This will permanently delete your workout history and streak. This cannot be undone.")
+ }
+ }
+
+ private func save() {
+ try? context.save()
+ // Sync settings to AudioService
+ if let profile {
+ AudioService.shared.isSoundEffectsEnabled = profile.soundEffectsEnabled
+ AudioService.shared.isVoiceCoachingEnabled = profile.voiceCoachingEnabled
+ AudioService.shared.isMusicEnabled = profile.musicEnabled
+ AudioService.shared.musicVolume = Float(profile.musicVolume)
+ }
+ }
+
+ private func resetProgress() {
+ let descriptor = FetchDescriptor()
+ if let sessions = try? context.fetch(descriptor) {
+ sessions.forEach { context.delete($0) }
+ }
+ profile?.onboardingCompleted = false
+ try? context.save()
+ }
+}
diff --git a/tabatago-swift/TabataGo/Views/Tabs/ActivityTab.swift b/tabatago-swift/TabataGo/Views/Tabs/ActivityTab.swift
new file mode 100644
index 0000000..a3b73be
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Tabs/ActivityTab.swift
@@ -0,0 +1,317 @@
+import SwiftUI
+import SwiftData
+
+/// Activity tab — streak, workout history, HealthKit rings summary.
+struct ActivityTab: View {
+ @Query(sort: \WorkoutSession.completedAt, order: .reverse)
+ private var sessions: [WorkoutSession]
+
+ @Query private var snapshots: [HealthSnapshot]
+ @StateObject private var healthVM = HealthViewModel()
+
+ private var streak: (current: Int, longest: Int) { computeStreak(from: sessions) }
+ private var snapshot: HealthSnapshot? { snapshots.first }
+ private var weeklyCount: Int { countThisWeek(sessions) }
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(spacing: 20) {
+
+ // ── Streak Banner ──────────────────────────────
+ StreakBanner(current: streak.current, longest: streak.longest)
+ .padding(.horizontal)
+
+ // ── HealthKit Rings ────────────────────────────
+ if let snap = snapshot {
+ HealthRingsCard(snapshot: snap)
+ .padding(.horizontal)
+ }
+
+ // ── Weekly Summary ─────────────────────────────
+ VStack(alignment: .leading, spacing: 12) {
+ Text("This Week")
+ .font(.title3.weight(.bold))
+ .padding(.horizontal)
+
+ HStack(spacing: 12) {
+ StatBadge(label: "Workouts", value: "\(weeklyCount)", color: Theme.brand, icon: "flame.fill")
+ StatBadge(label: "Minutes", value: "\(weeklyMinutes)", color: Theme.rest, icon: "clock.fill")
+ StatBadge(label: "Calories", value: "\(Int(weeklyCalories))", color: Theme.success, icon: "bolt.fill")
+ }
+ .padding(.horizontal)
+ }
+
+ // ── Workout History ────────────────────────────
+ if !sessions.isEmpty {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("History")
+ .font(.title3.weight(.bold))
+ .padding(.horizontal)
+
+ LazyVStack(spacing: 10) {
+ ForEach(sessions.prefix(30)) { session in
+ SessionHistoryRow(session: session)
+ .padding(.horizontal)
+ }
+ }
+ }
+ } else {
+ EmptyActivityView()
+ .padding(.horizontal)
+ }
+
+ Spacer(minLength: 40)
+ }
+ .padding(.top, 8)
+ }
+ .navigationTitle("Activity")
+ .navigationBarTitleDisplayMode(.large)
+ .task { await healthVM.refresh() }
+ .refreshable { await healthVM.refresh() }
+ }
+ }
+
+ private var weeklyMinutes: Int {
+ countThisWeek(sessions, value: { $0.durationSeconds / 60 })
+ }
+
+ private var weeklyCalories: Double {
+ sessions.filter { isThisWeek($0.completedAt) }
+ .reduce(0) { $0 + $1.caloriesBurned }
+ }
+}
+
+// ─── Sub-components ───────────────────────────────────────────────
+
+struct StreakBanner: View {
+ let current: Int
+ let longest: Int
+
+ var body: some View {
+ HStack(spacing: 0) {
+ // Current streak
+ VStack(spacing: 4) {
+ HStack(alignment: .lastTextBaseline, spacing: 4) {
+ Text("\(current)")
+ .font(.system(size: 52, weight: .black, design: .rounded))
+ .monospacedDigit()
+ .foregroundStyle(Theme.brand)
+ Text("days")
+ .font(.headline)
+ .foregroundStyle(.secondary)
+ }
+ Text("Current Streak")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity)
+
+ Divider().frame(height: 50)
+
+ // Longest streak
+ VStack(spacing: 4) {
+ HStack(alignment: .lastTextBaseline, spacing: 4) {
+ Text("\(longest)")
+ .font(.system(size: 52, weight: .black, design: .rounded))
+ .monospacedDigit()
+ .foregroundStyle(Theme.success)
+ Text("days")
+ .font(.headline)
+ .foregroundStyle(.secondary)
+ }
+ Text("Best Streak")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity)
+ }
+ .padding(.vertical, 16)
+ .glassCard()
+ }
+}
+
+struct HealthRingsCard: View {
+ let snapshot: HealthSnapshot
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ HStack {
+ Image(systemName: "heart.fill")
+ .foregroundStyle(.red)
+ Text("Apple Health")
+ .font(.headline.weight(.semibold))
+ Spacer()
+ Text("Today")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ HStack(spacing: 12) {
+ HealthRingStat(
+ label: "Move",
+ value: "\(Int(snapshot.activeCaloricBurn))",
+ unit: "kcal",
+ color: .red
+ )
+ HealthRingStat(
+ label: "Exercise",
+ value: "\(Int(snapshot.exerciseMinutes))",
+ unit: "min",
+ color: .green
+ )
+ HealthRingStat(
+ label: "Stand",
+ value: "\(snapshot.standHours)",
+ unit: "hrs",
+ color: .cyan
+ )
+ }
+
+ if let hr = snapshot.restingHeartRate {
+ Divider()
+ HStack {
+ Label("\(Int(hr)) bpm", systemImage: "waveform.path.ecg")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ Spacer()
+ Text("Resting HR")
+ .font(.caption)
+ .foregroundStyle(.tertiary)
+ }
+ }
+ }
+ .padding(16)
+ .glassCard()
+ }
+}
+
+struct HealthRingStat: View {
+ let label: String
+ let value: String
+ let unit: String
+ let color: Color
+
+ var body: some View {
+ VStack(spacing: 4) {
+ Text(value)
+ .font(.system(size: 24, weight: .bold, design: .rounded))
+ .monospacedDigit()
+ .foregroundStyle(color)
+ Text(unit)
+ .font(.caption2.weight(.semibold))
+ .foregroundStyle(color.opacity(0.7))
+ Text(label)
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity)
+ }
+}
+
+struct SessionHistoryRow: View {
+ let session: WorkoutSession
+
+ var body: some View {
+ HStack(spacing: 14) {
+ // Zone indicator
+ Circle()
+ .fill(Theme.zoneColor(session.bodyZone))
+ .frame(width: 10, height: 10)
+
+ VStack(alignment: .leading, spacing: 3) {
+ Text(session.programTitle)
+ .font(.subheadline.weight(.semibold))
+ .lineLimit(1)
+ Text(session.completedAt.formatted(date: .abbreviated, time: .omitted))
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 3) {
+ Text("\(session.durationSeconds / 60)m")
+ .font(.subheadline.weight(.semibold))
+ .monospacedDigit()
+ if session.caloriesBurned > 0 {
+ Text("\(Int(session.caloriesBurned)) kcal")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .padding(14)
+ .glassCard()
+ }
+}
+
+struct EmptyActivityView: View {
+ var body: some View {
+ VStack(spacing: 16) {
+ Image(systemName: "figure.run.circle")
+ .font(.system(size: 56))
+ .foregroundStyle(Theme.brand.opacity(0.6))
+ Text("No workouts yet")
+ .font(.title3.weight(.semibold))
+ Text("Complete your first Tabata to see your activity here.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ }
+ .padding(32)
+ .frame(maxWidth: .infinity)
+ .glassCard()
+ }
+}
+
+// ─── Helpers ──────────────────────────────────────────────────────
+
+private func computeStreak(from sessions: [WorkoutSession]) -> (current: Int, longest: Int) {
+ guard !sessions.isEmpty else { return (0, 0) }
+ let calendar = Calendar.current
+ let uniqueDays = Set(sessions.map { calendar.startOfDay(for: $0.completedAt) })
+ .sorted(by: >)
+
+ let today = calendar.startOfDay(for: Date())
+ let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
+
+ guard uniqueDays[0] == today || uniqueDays[0] == yesterday else {
+ return (0, longestStreak(from: uniqueDays.sorted(by: >)))
+ }
+
+ var current = 1
+ for i in 1..)))
+}
+
+private func longestStreak(from sortedDays: [Date]) -> Int {
+ guard !sortedDays.isEmpty else { return 0 }
+ let calendar = Calendar.current
+ var longest = 1, run = 1
+ for i in 1.. Int {
+ sessions.filter { isThisWeek($0.completedAt) }.count
+}
+
+private func countThisWeek(_ sessions: [WorkoutSession], value: (WorkoutSession) -> Int) -> Int {
+ sessions.filter { isThisWeek($0.completedAt) }.reduce(0) { $0 + value($1) }
+}
+
+private func isThisWeek(_ date: Date) -> Bool {
+ Calendar.current.isDate(date, equalTo: Date(), toGranularity: .weekOfYear)
+}
+
+#Preview {
+ ActivityTab()
+ .modelContainer(TabataGoSchema.previewContainer)
+}
diff --git a/tabatago-swift/TabataGo/Views/Tabs/HomeTab.swift b/tabatago-swift/TabataGo/Views/Tabs/HomeTab.swift
new file mode 100644
index 0000000..0f797cb
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Tabs/HomeTab.swift
@@ -0,0 +1,329 @@
+import SwiftUI
+import SwiftData
+
+/// Home tab — featured programs, quick start, welcome back header.
+struct HomeTab: View {
+ @Query private var profiles: [UserProfile]
+ @Query(sort: \WorkoutSession.completedAt, order: .reverse) private var sessions: [WorkoutSession]
+ @StateObject private var vm: HomeViewModel
+ @State private var selectedProgram: WorkoutProgram? = nil
+ @State private var showingPlayer = false
+
+ /// Production init — ViewModel fetches programs from Supabase.
+ init() {
+ _vm = StateObject(wrappedValue: HomeViewModel())
+ }
+
+ /// Preview/test init — injects a pre-populated ViewModel, no network calls.
+ init(previewVM: HomeViewModel) {
+ _vm = StateObject(wrappedValue: previewVM)
+ }
+
+ private var profile: UserProfile? { profiles.first }
+
+ // ── Stats derived from SwiftData session history ───────────────────
+ private var currentStreak: Int {
+ let calendar = Calendar.current
+ var streak = 0
+ var checkDate = calendar.startOfDay(for: Date())
+ let workoutDays = Set(sessions.map { calendar.startOfDay(for: $0.completedAt) })
+ while workoutDays.contains(checkDate) {
+ streak += 1
+ checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate)!
+ }
+ return streak
+ }
+
+ private var weeklyCount: Int {
+ let start = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
+ return sessions.filter { $0.completedAt >= start }.count
+ }
+
+ private var totalCount: Int { sessions.count }
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 24) {
+ // ── Quick Stats Row ──
+ HStack(spacing: 12) {
+ StatBadge(label: "Streak", value: "\(currentStreak)d", color: Theme.brand, icon: "flame.fill")
+ StatBadge(label: "This Week", value: "\(weeklyCount)", color: Theme.success, icon: "checkmark.circle.fill")
+ StatBadge(label: "All Time", value: "\(totalCount)", color: Theme.rest, icon: "trophy.fill")
+ }
+ .padding(.horizontal)
+
+ // ── Featured Workouts ──
+ if !vm.featuredPrograms.isEmpty {
+ VStack(alignment: .leading, spacing: 12) {
+ SectionHeader(title: "Featured", subtitle: "Handpicked for you")
+ .padding(.horizontal)
+
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 16) {
+ ForEach(vm.featuredPrograms) { program in
+ FeaturedProgramCard(program: program)
+ .onTapGesture { selectedProgram = program }
+ }
+ }
+ .padding(.horizontal)
+ }
+ }
+ }
+
+ // ── Body Zone Grid ──
+ VStack(alignment: .leading, spacing: 12) {
+ SectionHeader(title: "Browse by Zone", subtitle: "Target specific muscle groups")
+ .padding(.horizontal)
+
+ VStack(spacing: 12) {
+ ForEach(vm.availableZones, id: \.self) { zone in
+ NavigationLink(destination: BodyZoneView(zone: zone)) {
+ ZoneCard(zone: zone)
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(.horizontal)
+ }
+
+ // ── All Programs ──
+ if !vm.allPrograms.isEmpty {
+ VStack(alignment: .leading, spacing: 12) {
+ SectionHeader(title: "All Workouts")
+ .padding(.horizontal)
+
+ LazyVStack(spacing: 12) {
+ ForEach(vm.allPrograms) { program in
+ ProgramRow(program: program)
+ .onTapGesture { selectedProgram = program }
+ .padding(.horizontal)
+ }
+ }
+ }
+ }
+
+ // ── Loading / Error State ──
+ if vm.isLoading {
+ ProgressView()
+ .frame(maxWidth: .infinity, minHeight: 120)
+ } else if let error = vm.error {
+ VStack(spacing: 8) {
+ Image(systemName: "exclamationmark.triangle")
+ .font(.title2)
+ .foregroundStyle(.secondary)
+ Text("Failed to load programs")
+ .font(.subheadline.weight(.semibold))
+ Text(error)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ Button("Retry") { Task { await vm.refresh() } }
+ .buttonStyle(.bordered)
+ .padding(.top, 4)
+ }
+ .frame(maxWidth: .infinity, minHeight: 120)
+ .padding(.horizontal)
+ }
+
+ Spacer(minLength: 32)
+ }
+ .padding(.top, 8)
+ }
+ .navigationTitle(profile?.name.isEmpty == false ? "Hey, \(profile!.name.split(separator: " ").first ?? "there") 👋" : "TabataGo")
+ .navigationBarTitleDisplayMode(.large)
+ .refreshable { await vm.refresh() }
+ .sheet(item: $selectedProgram) { program in
+ ProgramDetailView(program: program)
+ }
+ .task { await vm.loadPrograms() }
+ }
+ }
+}
+
+// ─── Sub-components ───────────────────────────────────────────────
+
+struct SectionHeader: View {
+ let title: String
+ var subtitle: String? = nil
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.title2.weight(.bold))
+ if let subtitle {
+ Text(subtitle)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+}
+
+struct FeaturedProgramCard: View {
+ let program: WorkoutProgram
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ // Header gradient area
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
+ .fill(Theme.zoneGradient(program.bodyZone))
+ .frame(width: 220, height: 110)
+ .overlay(alignment: .bottomLeading) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(program.titleEn)
+ .font(.headline.weight(.bold))
+ .foregroundStyle(.white)
+ .lineLimit(2)
+ HStack(spacing: 6) {
+ Label("\(program.estimatedDuration)m", systemImage: "clock")
+ Label("\(program.estimatedCalories) kcal", systemImage: "flame")
+ }
+ .font(.caption.weight(.medium))
+ .foregroundStyle(.white.opacity(0.85))
+ }
+ .padding(12)
+ }
+
+ HStack {
+ LevelBadge(level: program.level)
+ Spacer()
+ if program.isFree {
+ Text("FREE")
+ .font(.caption2.weight(.bold))
+ .foregroundStyle(Theme.success)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(Theme.success.opacity(0.15))
+ .clipShape(Capsule())
+ }
+ }
+ }
+ .frame(width: 220)
+ }
+}
+
+struct ZoneCard: View {
+ let zone: String
+
+ var body: some View {
+ ZStack(alignment: .leading) {
+ RoundedRectangle(cornerRadius: 20, style: .continuous)
+ .fill(Theme.zoneGradient(zone))
+
+ HStack {
+ VStack(alignment: .leading, spacing: 6) {
+ Text(zoneLabel)
+ .font(.title3.weight(.bold))
+ .foregroundStyle(.white)
+ Text(zoneDescription)
+ .font(.subheadline)
+ .foregroundStyle(.white.opacity(0.8))
+ .lineLimit(2)
+ Spacer(minLength: 0)
+ HStack(spacing: 4) {
+ Text("Explore")
+ .font(.caption.weight(.semibold))
+ Image(systemName: "arrow.right")
+ .font(.caption.weight(.semibold))
+ }
+ .foregroundStyle(.white.opacity(0.7))
+ }
+
+ Spacer()
+
+ Image(systemName: zoneIcon)
+ .font(.system(size: 44, weight: .semibold))
+ .foregroundStyle(.white.opacity(0.25))
+ }
+ .padding(20)
+ }
+ .frame(height: 140)
+ }
+
+ private var zoneLabel: String {
+ switch zone {
+ case "upper-body": return "Upper Body"
+ case "lower-body": return "Lower Body"
+ case "full-body": return "Full Body"
+ default: return zone.replacingOccurrences(of: "-", with: " ").capitalized
+ }
+ }
+
+ private var zoneDescription: String {
+ switch zone {
+ case "upper-body": return "Arms, chest, shoulders & back"
+ case "lower-body": return "Legs, glutes & core stability"
+ case "full-body": return "Total body burn, head to toe"
+ default: return "Targeted workouts"
+ }
+ }
+
+ private var zoneIcon: String {
+ switch zone {
+ case "upper-body": return "figure.arms.open"
+ case "lower-body": return "figure.walk"
+ case "full-body": return "figure.highintensity.intervaltraining"
+ default: return "figure.run"
+ }
+ }
+}
+
+struct LevelBadge: View {
+ let level: String
+
+ var body: some View {
+ Text(level)
+ .font(.caption2.weight(.bold))
+ .foregroundStyle(Theme.levelColor(level))
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(Theme.levelColor(level).opacity(0.15))
+ .clipShape(Capsule())
+ }
+}
+
+struct ProgramRow: View {
+ let program: WorkoutProgram
+
+ var body: some View {
+ HStack(spacing: 14) {
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .fill(Theme.zoneGradient(program.bodyZone))
+ .frame(width: 56, height: 56)
+ .overlay {
+ Image(systemName: "bolt.fill")
+ .font(.system(size: 22, weight: .bold))
+ .foregroundStyle(.white)
+ }
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(program.titleEn)
+ .font(.subheadline.weight(.semibold))
+ .lineLimit(1)
+ HStack(spacing: 8) {
+ LevelBadge(level: program.level)
+ Label("\(program.estimatedDuration)m", systemImage: "clock")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Label("\(program.estimatedCalories) kcal", systemImage: "flame")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ Spacer()
+ Image(systemName: "chevron.right")
+ .font(.caption.weight(.semibold))
+ .foregroundStyle(.tertiary)
+ }
+ .padding(14)
+ .glassCard()
+ }
+}
+
+#Preview {
+ HomeTab(previewVM: HomeViewModel(previewPrograms: [PreviewData.sampleProgram]))
+ .modelContainer(TabataGoSchema.previewContainer)
+ .environment(AppState())
+}
diff --git a/tabatago-swift/TabataGo/Views/Tabs/MainTabView.swift b/tabatago-swift/TabataGo/Views/Tabs/MainTabView.swift
new file mode 100644
index 0000000..fd20ba1
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Tabs/MainTabView.swift
@@ -0,0 +1,52 @@
+import SwiftUI
+
+/// Root tab bar — Liquid Glass tab bar (iOS 26).
+struct MainTabView: View {
+ @State private var selectedTab: AppTab = .home
+
+ enum AppTab: String, CaseIterable {
+ case home, programs, activity, profile
+
+ var icon: String {
+ switch self {
+ case .home: return "house.fill"
+ case .programs: return "rectangle.grid.2x2.fill"
+ case .activity: return "chart.bar.fill"
+ case .profile: return "person.fill"
+ }
+ }
+
+ var label: String {
+ switch self {
+ case .home: return "Home"
+ case .programs: return "Programs"
+ case .activity: return "Activity"
+ case .profile: return "Profile"
+ }
+ }
+ }
+
+ var body: some View {
+ TabView(selection: $selectedTab) {
+ Tab(AppTab.home.label, systemImage: AppTab.home.icon, value: AppTab.home) {
+ HomeTab()
+ }
+ Tab(AppTab.programs.label, systemImage: AppTab.programs.icon, value: AppTab.programs) {
+ ProgramsTab()
+ }
+ Tab(AppTab.activity.label, systemImage: AppTab.activity.icon, value: AppTab.activity) {
+ ActivityTab()
+ }
+ Tab(AppTab.profile.label, systemImage: AppTab.profile.icon, value: AppTab.profile) {
+ ProfileTab()
+ }
+ }
+ .tabViewStyle(.sidebarAdaptable)
+ }
+}
+
+#Preview {
+ MainTabView()
+ .modelContainer(TabataGoSchema.previewContainer)
+ .environment(AppState())
+}
diff --git a/tabatago-swift/TabataGo/Views/Tabs/ProfileTab.swift b/tabatago-swift/TabataGo/Views/Tabs/ProfileTab.swift
new file mode 100644
index 0000000..9fd9d1f
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Tabs/ProfileTab.swift
@@ -0,0 +1,130 @@
+import SwiftUI
+import SwiftData
+
+/// Profile tab — user info, settings, subscription, saved workouts.
+struct ProfileTab: View {
+ @Query private var profiles: [UserProfile]
+ @State private var showingSettings = false
+ @State private var showingPaywall = false
+ @Environment(\.modelContext) private var context
+ @StateObject private var purchaseVM = PurchaseViewModel()
+
+ private var profile: UserProfile? { profiles.first }
+
+ var body: some View {
+ NavigationStack {
+ List {
+ // ── Profile Header ────────────────────────────────
+ Section {
+ HStack(spacing: 16) {
+ Circle()
+ .fill(Theme.brand.gradient)
+ .frame(width: 64, height: 64)
+ .overlay {
+ Text(String(profile?.name.prefix(1).uppercased() ?? "?"))
+ .font(.title2.weight(.bold))
+ .foregroundStyle(.white)
+ }
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(profile?.name ?? "Athlete")
+ .font(.title3.weight(.bold))
+ Text(profile?.goal.label ?? "")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ Text("Joined \(profile?.joinDate.formatted(date: .abbreviated, time: .omitted) ?? "")")
+ .font(.caption)
+ .foregroundStyle(.tertiary)
+ }
+ Spacer()
+ }
+ .padding(.vertical, 8)
+ }
+
+ // ── Subscription ──────────────────────────────────
+ Section("Subscription") {
+ if profile?.subscription.isPremium == true {
+ HStack {
+ Label("Premium Active", systemImage: "crown.fill")
+ .foregroundStyle(Theme.brand)
+ Spacer()
+ Text(profile?.subscription == .premiumYearly ? "Yearly" : "Monthly")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ } else {
+ Button {
+ showingPaywall = true
+ } label: {
+ HStack {
+ Label("Upgrade to Premium", systemImage: "crown")
+ .foregroundStyle(Theme.brand)
+ Spacer()
+ Image(systemName: "chevron.right")
+ .font(.caption.weight(.semibold))
+ .foregroundStyle(.tertiary)
+ }
+ }
+ .buttonStyle(.plain)
+ }
+ }
+
+ // ── Fitness Profile ───────────────────────────────
+ Section("Fitness Profile") {
+ ProfileRow(label: "Level", value: profile?.fitnessLevel.label ?? "—", icon: "chart.bar")
+ ProfileRow(label: "Goal", value: profile?.goal.label ?? "—", icon: "target")
+ ProfileRow(label: "Weekly Goal", value: "\(profile?.weeklyFrequency ?? 3)x / week", icon: "calendar")
+ }
+
+ // ── Settings ──────────────────────────────────────
+ Section {
+ NavigationLink(destination: SettingsView()) {
+ Label("Settings", systemImage: "gearshape")
+ }
+ NavigationLink(destination: PrivacyPolicyView()) {
+ Label("Privacy Policy", systemImage: "hand.raised")
+ }
+ NavigationLink(destination: TermsOfServiceView()) {
+ Label("Terms of Service", systemImage: "doc.text")
+ }
+ }
+
+ // ── App Info ──────────────────────────────────────
+ Section {
+ HStack {
+ Text("Version")
+ Spacer()
+ Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .navigationTitle("Profile")
+ .navigationBarTitleDisplayMode(.large)
+ .sheet(isPresented: $showingPaywall) {
+ PaywallView()
+ }
+ }
+ }
+}
+
+struct ProfileRow: View {
+ let label: String
+ let value: String
+ let icon: String
+
+ var body: some View {
+ HStack {
+ Label(label, systemImage: icon)
+ Spacer()
+ Text(value)
+ .foregroundStyle(.secondary)
+ }
+ }
+}
+
+#Preview {
+ ProfileTab()
+ .modelContainer(TabataGoSchema.previewContainer)
+ .environment(AppState())
+}
diff --git a/tabatago-swift/TabataGo/Views/Tabs/ProgramsTab.swift b/tabatago-swift/TabataGo/Views/Tabs/ProgramsTab.swift
new file mode 100644
index 0000000..b3dd805
--- /dev/null
+++ b/tabatago-swift/TabataGo/Views/Tabs/ProgramsTab.swift
@@ -0,0 +1,134 @@
+import SwiftUI
+import SwiftData
+
+/// Programs tab — browse all workouts, filter by zone/level.
+struct ProgramsTab: View {
+ @StateObject private var vm = HomeViewModel()
+ @State private var selectedZone: String? = nil
+ @State private var selectedLevel: String? = nil
+ @State private var selectedProgram: WorkoutProgram? = nil
+ @State private var searchText = ""
+
+ private var zones = ["upper", "lower", "full"]
+ private var levels = ["Beginner", "Intermediate", "Advanced"]
+
+ private var filtered: [WorkoutProgram] {
+ vm.allPrograms.filter { program in
+ let zoneMatch = selectedZone == nil || program.bodyZone == selectedZone
+ let levelMatch = selectedLevel == nil || program.level == selectedLevel
+ let searchMatch = searchText.isEmpty ||
+ program.titleEn.localizedCaseInsensitiveContains(searchText) ||
+ program.bodyZone.localizedCaseInsensitiveContains(searchText)
+ return zoneMatch && levelMatch && searchMatch
+ }
+ }
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(spacing: 16) {
+ // ── Zone Filter ───────────────────────────────
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 10) {
+ FilterChip(label: "All", isSelected: selectedZone == nil) {
+ selectedZone = nil
+ }
+ ForEach(zones, id: \.self) { zone in
+ FilterChip(
+ label: zone.capitalized == "Full" ? "Full Body" : zone.capitalized,
+ isSelected: selectedZone == zone,
+ color: Theme.zoneColor(zone)
+ ) {
+ selectedZone = selectedZone == zone ? nil : zone
+ }
+ }
+ }
+ .padding(.horizontal)
+ }
+
+ // ── Level Filter ──────────────────────────────
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 10) {
+ FilterChip(label: "All Levels", isSelected: selectedLevel == nil) {
+ selectedLevel = nil
+ }
+ ForEach(levels, id: \.self) { level in
+ FilterChip(
+ label: level,
+ isSelected: selectedLevel == level,
+ color: Theme.levelColor(level)
+ ) {
+ selectedLevel = selectedLevel == level ? nil : level
+ }
+ }
+ }
+ .padding(.horizontal)
+ }
+
+ // ── Program Grid ──────────────────────────────
+ if vm.isLoading {
+ ProgressView().frame(minHeight: 120)
+ } else if filtered.isEmpty {
+ ContentUnavailableView(
+ "No Programs Found",
+ systemImage: "magnifyingglass",
+ description: Text("Try changing your filters.")
+ )
+ .padding(.top, 40)
+ } else {
+ LazyVStack(spacing: 12) {
+ ForEach(filtered) { program in
+ ProgramRow(program: program)
+ .onTapGesture { selectedProgram = program }
+ .padding(.horizontal)
+ }
+ }
+ }
+
+ Spacer(minLength: 40)
+ }
+ .padding(.top, 8)
+ }
+ .navigationTitle("Programs")
+ .navigationBarTitleDisplayMode(.large)
+ .searchable(text: $searchText, prompt: "Search workouts...")
+ .task { await vm.loadPrograms() }
+ .refreshable { await vm.refresh() }
+ .sheet(item: $selectedProgram) { program in
+ ProgramDetailView(program: program)
+ }
+ }
+ }
+}
+
+struct FilterChip: View {
+ let label: String
+ let isSelected: Bool
+ var color: Color = .primary
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(label)
+ .font(.subheadline.weight(isSelected ? .semibold : .regular))
+ .foregroundStyle(isSelected ? .white : .primary)
+ .padding(.horizontal, 14)
+ .padding(.vertical, 8)
+ .background {
+ if isSelected {
+ Capsule().fill(color == .primary ? Theme.brand : color)
+ } else {
+ Capsule().fill(.ultraThinMaterial)
+ }
+ }
+ }
+ .buttonStyle(.plain)
+ .animation(.spring(duration: 0.25), value: isSelected)
+ }
+}
+
+#Preview {
+ ProgramsTab()
+ .modelContainer(TabataGoSchema.previewContainer)
+ .environment(AppState())
+}
diff --git a/tabatago-swift/TabataGoTests/TabataGoTests.swift b/tabatago-swift/TabataGoTests/TabataGoTests.swift
new file mode 100644
index 0000000..55d4da7
--- /dev/null
+++ b/tabatago-swift/TabataGoTests/TabataGoTests.swift
@@ -0,0 +1,62 @@
+import XCTest
+@testable import TabataGo
+
+final class TabataGoTests: XCTestCase {
+
+ func testStreakComputationConsecutiveDays() {
+ // Consecutive 3 days should yield streak of 3
+ let calendar = Calendar.current
+ let today = calendar.startOfDay(for: Date())
+ let sessions: [Date] = [
+ today,
+ calendar.date(byAdding: .day, value: -1, to: today)!,
+ calendar.date(byAdding: .day, value: -2, to: today)!,
+ ]
+ // Validate unique day count
+ XCTAssertEqual(sessions.count, 3)
+ }
+
+ func testWorkoutSessionCompletionRate() {
+ let session = WorkoutSession(
+ programId: "test",
+ programTitle: "Test",
+ bodyZone: "full",
+ level: "Beginner",
+ startedAt: Date(),
+ completedAt: Date(),
+ durationSeconds: 1440,
+ caloriesBurned: 180,
+ roundsCompleted: 8,
+ totalRounds: 8
+ )
+ XCTAssertEqual(session.completionRate, 1.0)
+ }
+
+ func testWorkoutSessionPartialCompletionRate() {
+ let session = WorkoutSession(
+ programId: "test",
+ programTitle: "Test",
+ bodyZone: "full",
+ level: "Beginner",
+ startedAt: Date(),
+ completedAt: Date(),
+ durationSeconds: 720,
+ caloriesBurned: 90,
+ roundsCompleted: 4,
+ totalRounds: 8
+ )
+ XCTAssertEqual(session.completionRate, 0.5)
+ }
+
+ func testSubscriptionPlanIsPremium() {
+ XCTAssertFalse(SubscriptionPlan.free.isPremium)
+ XCTAssertTrue(SubscriptionPlan.premiumMonthly.isPremium)
+ XCTAssertTrue(SubscriptionPlan.premiumYearly.isPremium)
+ }
+
+ func testTimerPhaseColors() {
+ // Phase colors should differ
+ XCTAssertNotEqual(Theme.phaseColor(.work), Theme.phaseColor(.rest))
+ XCTAssertNotEqual(Theme.phaseColor(.work), Theme.phaseColor(.complete))
+ }
+}
diff --git a/tabatago-swift/TabataGoUITests/TabataGoUITests.swift b/tabatago-swift/TabataGoUITests/TabataGoUITests.swift
new file mode 100644
index 0000000..9f69407
--- /dev/null
+++ b/tabatago-swift/TabataGoUITests/TabataGoUITests.swift
@@ -0,0 +1,19 @@
+import XCTest
+
+final class TabataGoUITests: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ app = XCUIApplication()
+ app.launchArguments = ["--uitesting"]
+ app.launch()
+ }
+
+ func testOnboardingFlowCompletes() {
+ // App should show onboarding for fresh installs
+ // In a real test, we'd interact with onboarding steps
+ XCTAssertTrue(app.exists)
+ }
+}
diff --git a/tabatago-swift/TabataGoWatch/App/TabataGoWatchApp.swift b/tabatago-swift/TabataGoWatch/App/TabataGoWatchApp.swift
new file mode 100644
index 0000000..f596f8e
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/App/TabataGoWatchApp.swift
@@ -0,0 +1,17 @@
+import SwiftUI
+import HealthKit
+
+@main
+struct TabataGoWatchApp: App {
+
+ @StateObject private var connectivityManager = WatchConnectivityManager.shared
+ @StateObject private var playerEngine = WatchPlayerEngine()
+
+ var body: some Scene {
+ WindowGroup {
+ WatchRootView()
+ .environmentObject(connectivityManager)
+ .environmentObject(playerEngine)
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGoWatch/Complications/Info.plist b/tabatago-swift/TabataGoWatch/Complications/Info.plist
new file mode 100644
index 0000000..e556090
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Complications/Info.plist
@@ -0,0 +1,29 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ TabataGoWidget
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ XPC!
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/tabatago-swift/TabataGoWatch/Complications/TabataGoComplication.swift b/tabatago-swift/TabataGoWatch/Complications/TabataGoComplication.swift
new file mode 100644
index 0000000..098713b
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Complications/TabataGoComplication.swift
@@ -0,0 +1,165 @@
+import WidgetKit
+import SwiftUI
+
+// ─── Timeline entry ───────────────────────────────────────────────────────────
+
+struct TabataEntry: TimelineEntry {
+ let date: Date
+ let streak: Int
+ let lastWorkoutLabel: String // e.g. "Yesterday" or "2 days ago"
+}
+
+// ─── Provider ─────────────────────────────────────────────────────────────────
+
+struct TabataProvider: TimelineProvider {
+
+ private let sharedDefaults = UserDefaults(suiteName: "group.com.tabatago.app")
+
+ func placeholder(in context: Context) -> TabataEntry {
+ TabataEntry(date: Date(), streak: 7, lastWorkoutLabel: "Today")
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (TabataEntry) -> Void) {
+ completion(makeEntry())
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
+ let entry = makeEntry()
+ // Refresh at midnight so the streak date label stays accurate
+ let midnight = Calendar.current.startOfDay(for: Date().addingTimeInterval(86_400))
+ completion(Timeline(entries: [entry], policy: .after(midnight)))
+ }
+
+ private func makeEntry() -> TabataEntry {
+ let streak = sharedDefaults?.integer(forKey: "streak") ?? 0
+ let lastDate = sharedDefaults?.object(forKey: "lastWorkoutDate") as? Date
+ let label = relativeLabel(for: lastDate)
+ return TabataEntry(date: Date(), streak: streak, lastWorkoutLabel: label)
+ }
+
+ private func relativeLabel(for date: Date?) -> String {
+ guard let date else { return "Not started" }
+ let days = Calendar.current.dateComponents([.day],
+ from: Calendar.current.startOfDay(for: date),
+ to: Calendar.current.startOfDay(for: Date())).day ?? 0
+ switch days {
+ case 0: return "Today"
+ case 1: return "Yesterday"
+ default: return "\(days) days ago"
+ }
+ }
+}
+
+// ─── Views ────────────────────────────────────────────────────────────────────
+
+/// `.accessoryCircular` — streak count inside an orange ring
+struct CircularComplicationView: View {
+ let entry: TabataEntry
+
+ var body: some View {
+ ZStack {
+ AccessoryWidgetBackground()
+ VStack(spacing: 0) {
+ Image(systemName: "bolt.fill")
+ .font(.system(size: 9, weight: .bold))
+ .foregroundStyle(.orange)
+ Text("\(entry.streak)")
+ .font(.system(size: 18, weight: .black, design: .rounded))
+ .minimumScaleFactor(0.5)
+ }
+ }
+ .widgetAccentable()
+ }
+}
+
+/// `.accessoryRectangular` — streak + last workout date
+struct RectangularComplicationView: View {
+ let entry: TabataEntry
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 4) {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ .font(.system(size: 10, weight: .bold))
+ Text("\(entry.streak) day streak")
+ .font(.system(size: 12, weight: .bold, design: .rounded))
+ }
+ Text(entry.lastWorkoutLabel)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ Text("Open TabataGo →")
+ .font(.system(size: 10, weight: .medium))
+ .foregroundStyle(.orange)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .widgetAccentable()
+ }
+}
+
+/// `.accessoryCorner` — tiny bolt + streak digit in the corner
+struct CornerComplicationView: View {
+ let entry: TabataEntry
+
+ var body: some View {
+ ZStack {
+ AccessoryWidgetBackground()
+ Image(systemName: "bolt.fill")
+ .font(.system(size: 14, weight: .bold))
+ .foregroundStyle(.orange)
+ }
+ .widgetLabel("\(entry.streak) day streak")
+ }
+}
+
+// ─── Widget definition ────────────────────────────────────────────────────────
+
+struct TabataGoComplication: Widget {
+ static let kind = "TabataGoComplication"
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: Self.kind, provider: TabataProvider()) { entry in
+ TabataComplicationEntryView(entry: entry)
+ .containerBackground(.black, for: .widget)
+ }
+ .configurationDisplayName("TabataGo")
+ .description("Your current workout streak.")
+ .supportedFamilies([
+ .accessoryCircular,
+ .accessoryRectangular,
+ .accessoryCorner
+ ])
+ }
+}
+
+struct TabataComplicationEntryView: View {
+ @Environment(\.widgetFamily) var family
+ let entry: TabataEntry
+
+ var body: some View {
+ switch family {
+ case .accessoryCircular:
+ CircularComplicationView(entry: entry)
+ case .accessoryRectangular:
+ RectangularComplicationView(entry: entry)
+ case .accessoryCorner:
+ CornerComplicationView(entry: entry)
+ default:
+ CircularComplicationView(entry: entry)
+ }
+ }
+}
+
+// ─── Previews ─────────────────────────────────────────────────────────────────
+
+#Preview("Circular", as: .accessoryCircular) {
+ TabataGoComplication()
+} timeline: {
+ TabataEntry(date: .now, streak: 7, lastWorkoutLabel: "Today")
+}
+
+#Preview("Rectangular", as: .accessoryRectangular) {
+ TabataGoComplication()
+} timeline: {
+ TabataEntry(date: .now, streak: 7, lastWorkoutLabel: "Today")
+}
diff --git a/tabatago-swift/TabataGoWatch/Resources/Info.plist b/tabatago-swift/TabataGoWatch/Resources/Info.plist
new file mode 100644
index 0000000..66947f8
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Resources/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ TabataGo
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSHealthShareUsageDescription
+ TabataGo reads your heart rate and calories during workouts.
+ NSHealthUpdateUsageDescription
+ TabataGo saves workout data to Apple Health directly from your Watch.
+ WKApplication
+
+ WKCompanionAppBundleIdentifier
+ com.tabatago.app
+
+
diff --git a/tabatago-swift/TabataGoWatch/Resources/TabataGoWatch.entitlements b/tabatago-swift/TabataGoWatch/Resources/TabataGoWatch.entitlements
new file mode 100644
index 0000000..f2ea579
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Resources/TabataGoWatch.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.developer.healthkit
+
+ com.apple.security.application-groups
+
+ group.com.tabatago.app
+
+
+
diff --git a/tabatago-swift/TabataGoWatch/Services/WatchConnectivityManager.swift b/tabatago-swift/TabataGoWatch/Services/WatchConnectivityManager.swift
new file mode 100644
index 0000000..02cd433
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Services/WatchConnectivityManager.swift
@@ -0,0 +1,88 @@
+import Foundation
+import WatchConnectivity
+
+/// Watch-side WatchConnectivity manager.
+/// Receives workout payloads from the phone, sends HR/calorie results back.
+@MainActor
+final class WatchConnectivityManager: NSObject, ObservableObject, WCSessionDelegate {
+
+ static let shared = WatchConnectivityManager()
+
+ @Published private(set) var isPhoneReachable = false
+
+ var onStartWorkout: ((WatchWorkoutPayload) -> Void)?
+ var onTimerTick: ((TimerTickPayload) -> Void)?
+ var onPause: (() -> Void)?
+ var onResume: (() -> Void)?
+ var onEnd: (() -> Void)?
+
+ private var session: WCSession?
+
+ private override init() {
+ super.init()
+ guard WCSession.isSupported() else { return }
+ session = WCSession.default
+ session?.delegate = self
+ session?.activate()
+ }
+
+ // ─── Send data to phone ───────────────────────────────────────
+
+ func sendHeartRate(_ bpm: Double) {
+ send([WCMessageKey.type: WCMessageType.heartRateUpdate.rawValue,
+ WCMessageKey.heartRate: bpm])
+ }
+
+ func sendSessionResult(_ result: WatchSessionResult) {
+ guard let data = try? JSONEncoder().encode(result) else { return }
+ send([WCMessageKey.type: WCMessageType.sessionCompleted.rawValue,
+ WCMessageKey.sessionResult: data])
+ }
+
+ // ─── WCSessionDelegate ────────────────────────────────────────
+
+ nonisolated func session(_ session: WCSession, activationDidCompleteWith state: WCSessionActivationState, error: Error?) {
+ let reachable = session.isReachable
+ Task { @MainActor in self.isPhoneReachable = reachable }
+ }
+
+ nonisolated func sessionReachabilityDidChange(_ session: WCSession) {
+ let reachable = session.isReachable
+ Task { @MainActor in self.isPhoneReachable = reachable }
+ }
+
+ nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
+ guard let typeRaw = message[WCMessageKey.type] as? String,
+ let type = WCMessageType(rawValue: typeRaw) else { return }
+
+ // Extract Data payloads before crossing actor boundary — [String: Any] is not Sendable.
+ let workoutData = message[WCMessageKey.workoutPayload] as? Data
+ let tickData = message["tick"] as? Data
+
+ Task { @MainActor in
+ switch type {
+ case .startWorkout:
+ guard let data = workoutData,
+ let payload = try? JSONDecoder().decode(WatchWorkoutPayload.self, from: data) else { return }
+ onStartWorkout?(payload)
+
+ case .timerTick:
+ guard let data = tickData,
+ let tick = try? JSONDecoder().decode(TimerTickPayload.self, from: data) else { return }
+ onTimerTick?(tick)
+
+ case .pauseWorkout: onPause?()
+ case .resumeWorkout: onResume?()
+ case .endWorkout: onEnd?()
+ default: break
+ }
+ }
+ }
+
+ // ─── Private ─────────────────────────────────────────────────
+
+ private func send(_ message: [String: Any]) {
+ guard let session, session.isReachable else { return }
+ session.sendMessage(message, replyHandler: nil)
+ }
+}
diff --git a/tabatago-swift/TabataGoWatch/Services/WatchPlayerEngine.swift b/tabatago-swift/TabataGoWatch/Services/WatchPlayerEngine.swift
new file mode 100644
index 0000000..f5cfa73
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Services/WatchPlayerEngine.swift
@@ -0,0 +1,297 @@
+import Foundation
+import HealthKit
+import WatchKit
+
+// ─── Watch-local phase enum (mirrors iOS TimerPhase without cross-target dep) ──
+
+enum WatchPhase: String, Codable, CaseIterable {
+ case prep = "PREP"
+ case warmup = "WARMUP"
+ case work = "WORK"
+ case rest = "REST"
+ case interBlockRest = "INTER_BLOCK_REST"
+ case cooldown = "COOLDOWN"
+ case complete = "COMPLETE"
+}
+
+// ─── Engine ───────────────────────────────────────────────────────────────────
+
+/// Drives the watch-side workout timer.
+///
+/// Design: The phone is the source-of-truth; the Watch engine *also* runs
+/// its own 1-second countdown so the display stays smooth even if WC
+/// messages are delayed. On every `timerTick` from the phone the Watch
+/// snaps its state to match, preventing drift.
+///
+/// HealthKit: The engine owns an `HKWorkoutSession` + `HKLiveWorkoutBuilder`
+/// so that calorie / heart-rate data is written directly from the Watch,
+/// which has the most accurate wrist sensors.
+@MainActor
+final class WatchPlayerEngine: NSObject, ObservableObject {
+
+ // ── Published state ───────────────────────────────────────────────
+ @Published private(set) var isActive = false
+ @Published private(set) var isPaused = false
+ @Published private(set) var phase = WatchPhase.prep
+ @Published private(set) var timeRemaining = 0
+ @Published private(set) var currentRound = 1
+ @Published private(set) var totalRoundsInBlock = 8
+ @Published private(set) var currentExerciseName: String? = nil
+ @Published private(set) var heartRate = 0.0
+ @Published private(set) var activeCalories = 0.0
+
+ // ── Private state ─────────────────────────────────────────────────
+ private var payload: WatchWorkoutPayload?
+ private var startedAt: Date?
+ private var timer: Timer?
+
+ // ── HealthKit ─────────────────────────────────────────────────────
+ private let healthStore = HKHealthStore()
+ private var workoutSession: HKWorkoutSession?
+ private var liveBuilder: HKLiveWorkoutBuilder?
+
+ // ── Connectivity back-ref ─────────────────────────────────────────
+ private let wc = WatchConnectivityManager.shared
+
+ override init() {
+ super.init()
+ wireConnectivityCallbacks()
+ }
+
+ // ─── Public API ───────────────────────────────────────────────────
+
+ func togglePause() {
+ if isPaused { resume() } else { pause() }
+ }
+
+ func endWorkout() {
+ stopTimer()
+ Task { await finalizeHealthKit() }
+ }
+
+ // ─── Connectivity callbacks ───────────────────────────────────────
+
+ private func wireConnectivityCallbacks() {
+ wc.onStartWorkout = { [weak self] payload in
+ Task { @MainActor in self?.handleStartWorkout(payload) }
+ }
+ wc.onTimerTick = { [weak self] tick in
+ Task { @MainActor in self?.handleTick(tick) }
+ }
+ wc.onPause = { [weak self] in Task { @MainActor in self?.pause() } }
+ wc.onResume = { [weak self] in Task { @MainActor in self?.resume() } }
+ wc.onEnd = { [weak self] in Task { @MainActor in self?.endWorkout() } }
+ }
+
+ private func handleStartWorkout(_ p: WatchWorkoutPayload) {
+ payload = p
+ startedAt = Date()
+
+ // Seed from first block
+ if let first = p.blocks.first {
+ totalRoundsInBlock = first.rounds
+ currentExerciseName = first.exercise1Name
+ }
+ phase = p.warmupDuration > 0 ? .warmup : .prep
+ timeRemaining = p.warmupDuration > 0 ? p.warmupDuration : 10
+ currentRound = 1
+ heartRate = 0
+ activeCalories = 0
+ isPaused = false
+ isActive = true
+
+ WKInterfaceDevice.current().play(.start)
+
+ startTimer()
+ Task { await startHealthKit() }
+ }
+
+ /// Snap watch state to phone's authoritative tick.
+ private func handleTick(_ tick: TimerTickPayload) {
+ phase = WatchPhase(rawValue: tick.phase) ?? phase
+ timeRemaining = tick.timeRemaining
+ currentRound = tick.currentRound
+ totalRoundsInBlock = tick.totalRoundsInBlock
+ currentExerciseName = tick.exerciseName
+ }
+
+ // ─── Timer ────────────────────────────────────────────────────────
+
+ private func startTimer() {
+ stopTimer()
+ timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ Task { @MainActor in self?.tick() }
+ }
+ }
+
+ private func stopTimer() {
+ timer?.invalidate()
+ timer = nil
+ }
+
+ private func tick() {
+ guard !isPaused else { return }
+ if timeRemaining > 0 {
+ timeRemaining -= 1
+ }
+ // Phase transitions are driven by the phone's timerTick messages;
+ // the local countdown is purely for smooth display.
+ }
+
+ private func pause() {
+ isPaused = true
+ stopTimer()
+ workoutSession?.pause()
+ WKInterfaceDevice.current().play(.stop)
+ }
+
+ private func resume() {
+ isPaused = false
+ startTimer()
+ workoutSession?.resume()
+ WKInterfaceDevice.current().play(.start)
+ }
+
+ // ─── HealthKit ────────────────────────────────────────────────────
+
+ private func startHealthKit() async {
+ guard HKHealthStore.isHealthDataAvailable() else { return }
+
+ let typesToShare: Set = [
+ HKQuantityType(.activeEnergyBurned),
+ HKQuantityType(.heartRate),
+ HKObjectType.workoutType()
+ ]
+ let typesToRead: Set = [
+ HKQuantityType(.heartRate),
+ HKQuantityType(.activeEnergyBurned)
+ ]
+
+ do {
+ try await healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead)
+ } catch {
+ return // HealthKit optional — proceed without it
+ }
+
+ let config = HKWorkoutConfiguration()
+ config.activityType = .highIntensityIntervalTraining
+ config.locationType = .indoor
+
+ do {
+ let session = try HKWorkoutSession(healthStore: healthStore, configuration: config)
+ let builder = session.associatedWorkoutBuilder()
+ builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
+ workoutConfiguration: config)
+ session.delegate = self
+ builder.delegate = self
+
+ workoutSession = session
+ liveBuilder = builder
+
+ session.startActivity(with: startedAt ?? Date())
+ try await builder.beginCollection(at: startedAt ?? Date())
+ } catch {
+ // Non-fatal — timer still works without HealthKit
+ }
+ }
+
+ private func finalizeHealthKit() async {
+ let endDate = Date()
+ guard let session = workoutSession, let builder = liveBuilder else {
+ sendResultToPhone(endDate: endDate)
+ isActive = false
+ return
+ }
+
+ do {
+ try await builder.endCollection(at: endDate)
+ let workout = try await builder.finishWorkout()
+ session.end()
+
+ guard let workout else {
+ sendResultToPhone(endDate: endDate)
+ return
+ }
+
+ let avgHR = workout.statistics(for: HKQuantityType(.heartRate))?
+ .averageQuantity()?.doubleValue(for: HKUnit(from: "count/min"))
+ let peakHR = workout.statistics(for: HKQuantityType(.heartRate))?
+ .maximumQuantity()?.doubleValue(for: HKUnit(from: "count/min"))
+ let cal = workout.statistics(for: HKQuantityType(.activeEnergyBurned))?
+ .sumQuantity()?.doubleValue(for: .kilocalorie())
+
+ if let cal { activeCalories = cal }
+ sendResultToPhone(endDate: endDate, avgHR: avgHR, peakHR: peakHR, cal: cal)
+ } catch {
+ sendResultToPhone(endDate: endDate)
+ }
+
+ isActive = false
+ }
+
+ private func sendResultToPhone(endDate: Date,
+ avgHR: Double? = nil,
+ peakHR: Double? = nil,
+ cal: Double? = nil) {
+ let result = WatchSessionResult(
+ programId: payload?.programId ?? "",
+ startedAt: startedAt ?? endDate,
+ completedAt: endDate,
+ durationSeconds: Int(endDate.timeIntervalSince(startedAt ?? endDate)),
+ activeCalories: cal ?? activeCalories,
+ averageHeartRate: avgHR,
+ peakHeartRate: peakHR
+ )
+ wc.sendSessionResult(result)
+ WKInterfaceDevice.current().play(.success)
+ }
+}
+
+// ─── HKWorkoutSessionDelegate ─────────────────────────────────────────────────
+
+extension WatchPlayerEngine: HKWorkoutSessionDelegate {
+ nonisolated func workoutSession(_ workoutSession: HKWorkoutSession,
+ didChangeTo toState: HKWorkoutSessionState,
+ from fromState: HKWorkoutSessionState,
+ date: Date) {}
+
+ nonisolated func workoutSession(_ workoutSession: HKWorkoutSession,
+ didFailWithError error: Error) {}
+}
+
+// ─── HKLiveWorkoutBuilderDelegate ────────────────────────────────────────────
+
+extension WatchPlayerEngine: HKLiveWorkoutBuilderDelegate {
+ nonisolated func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {}
+
+ nonisolated func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder,
+ didCollectDataOf collectedTypes: Set) {
+ for type in collectedTypes {
+ guard let quantityType = type as? HKQuantityType else { continue }
+
+ switch quantityType {
+ case HKQuantityType(.heartRate):
+ let hr = workoutBuilder
+ .statistics(for: quantityType)?
+ .mostRecentQuantity()?
+ .doubleValue(for: HKUnit(from: "count/min")) ?? 0
+ Task { @MainActor [weak self] in
+ guard let self else { return }
+ self.heartRate = hr
+ self.wc.sendHeartRate(hr)
+ }
+
+ case HKQuantityType(.activeEnergyBurned):
+ let cal = workoutBuilder
+ .statistics(for: quantityType)?
+ .sumQuantity()?
+ .doubleValue(for: .kilocalorie()) ?? 0
+ Task { @MainActor [weak self] in
+ self?.activeCalories = cal
+ }
+
+ default: break
+ }
+ }
+ }
+}
diff --git a/tabatago-swift/TabataGoWatch/Views/WatchActivityView.swift b/tabatago-swift/TabataGoWatch/Views/WatchActivityView.swift
new file mode 100644
index 0000000..5f2f6b5
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Views/WatchActivityView.swift
@@ -0,0 +1,156 @@
+import SwiftUI
+import HealthKit
+
+/// Mini activity/streak summary shown on the Watch when idle.
+/// Displays today's Move/Exercise/Stand ring progress from HealthKit
+/// and a streak count stored via App Group UserDefaults (shared with phone).
+struct WatchActivityView: View {
+
+ @State private var moveProgress: Double = 0 // 0-1
+ @State private var exerciseProgress: Double = 0
+ @State private var standProgress: Double = 0
+ @State private var streak: Int = 0
+ @State private var isLoading = true
+
+ private let healthStore = HKHealthStore()
+ private let sharedDefaults = UserDefaults(suiteName: "group.com.tabatago.app")
+
+ var body: some View {
+ ScrollView {
+ VStack(spacing: 14) {
+
+ // ── Rings ──────────────────────────────────────────
+ Text("Today")
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.secondary)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ HStack(spacing: 12) {
+ RingView(progress: moveProgress,
+ color: Color(red: 1.0, green: 0.23, blue: 0.19),
+ icon: "flame.fill",
+ label: "Move")
+ RingView(progress: exerciseProgress,
+ color: .green,
+ icon: "figure.run",
+ label: "Exercise")
+ RingView(progress: standProgress,
+ color: Color(red: 0.04, green: 0.80, blue: 0.97),
+ icon: "figure.stand",
+ label: "Stand")
+ }
+
+ Divider()
+
+ // ── Streak ─────────────────────────────────────────
+ HStack {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ .font(.system(size: 14))
+ Text("\(streak) day\(streak == 1 ? "" : "s")")
+ .font(.system(size: 14, weight: .bold, design: .rounded))
+ Spacer()
+ Text("streak")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 4)
+ .padding(.vertical, 8)
+ .redacted(reason: isLoading ? .placeholder : [])
+ }
+ .task { await loadData() }
+ }
+
+ // ─── Data loading ─────────────────────────────────────────────────
+
+ private func loadData() async {
+ streak = sharedDefaults?.integer(forKey: "streak") ?? 0
+
+ guard HKHealthStore.isHealthDataAvailable() else {
+ isLoading = false
+ return
+ }
+
+ let typesToRead: Set = [
+ HKObjectType.activitySummaryType()
+ ]
+
+ guard (try? await healthStore.requestAuthorization(toShare: [], read: typesToRead)) != nil else {
+ isLoading = false
+ return
+ }
+
+ let calendar = Calendar.current
+ let today = calendar.startOfDay(for: Date())
+ let predicate = HKQuery.predicateForActivitySummary(with: calendar.dateComponents(
+ [.era, .year, .month, .day], from: today))
+
+ let summaries = try? await withCheckedThrowingContinuation { (cont: CheckedContinuation<[HKActivitySummary], Error>) in
+ let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, error in
+ if let error { cont.resume(throwing: error) }
+ else { cont.resume(returning: summaries ?? []) }
+ }
+ healthStore.execute(query)
+ }
+
+ if let summary = summaries?.first {
+ let moveGoal = summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())
+ let exerciseGoal = summary.appleExerciseTimeGoal.doubleValue(for: .minute())
+ let standGoal = summary.appleStandHoursGoal.doubleValue(for: .count())
+
+ await MainActor.run {
+ moveProgress = moveGoal > 0
+ ? summary.activeEnergyBurned.doubleValue(for: .kilocalorie()) / moveGoal
+ : 0
+ exerciseProgress = exerciseGoal > 0
+ ? summary.appleExerciseTime.doubleValue(for: .minute()) / exerciseGoal
+ : 0
+ standProgress = standGoal > 0
+ ? summary.appleStandHours.doubleValue(for: .count()) / standGoal
+ : 0
+ isLoading = false
+ }
+ } else {
+ await MainActor.run { isLoading = false }
+ }
+ }
+}
+
+// ─── Sub-components ───────────────────────────────────────────────────────────
+
+private struct RingView: View {
+ let progress: Double
+ let color: Color
+ let icon: String
+ let label: String
+
+ var body: some View {
+ VStack(spacing: 4) {
+ ZStack {
+ // Track
+ Circle()
+ .stroke(color.opacity(0.2), lineWidth: 5)
+ // Fill
+ Circle()
+ .trim(from: 0, to: min(progress, 1.0))
+ .stroke(color, style: StrokeStyle(lineWidth: 5, lineCap: .round))
+ .rotationEffect(.degrees(-90))
+ .animation(.easeOut(duration: 0.6), value: progress)
+ // Icon
+ Image(systemName: icon)
+ .font(.system(size: 9, weight: .bold))
+ .foregroundStyle(color)
+ }
+ .frame(width: 36, height: 36)
+
+ Text(label)
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ }
+}
+
+#Preview {
+ WatchActivityView()
+}
diff --git a/tabatago-swift/TabataGoWatch/Views/WatchIdleView.swift b/tabatago-swift/TabataGoWatch/Views/WatchIdleView.swift
new file mode 100644
index 0000000..57e92eb
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Views/WatchIdleView.swift
@@ -0,0 +1,43 @@
+import SwiftUI
+
+/// Idle state — waiting for a workout to start from the phone.
+struct WatchIdleView: View {
+ @EnvironmentObject private var connectivity: WatchConnectivityManager
+
+ var body: some View {
+ VStack(spacing: 12) {
+ Image(systemName: "bolt.fill")
+ .font(.system(size: 36, weight: .bold))
+ .foregroundStyle(.orange)
+
+ Text("TabataGo")
+ .font(.system(size: 18, weight: .bold, design: .rounded))
+
+ Text("Start a workout\non your iPhone")
+ .font(.system(size: 13))
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+
+ if connectivity.isPhoneReachable {
+ HStack(spacing: 4) {
+ Circle()
+ .fill(.green)
+ .frame(width: 6, height: 6)
+ Text("Connected")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ } else {
+ HStack(spacing: 4) {
+ Circle()
+ .fill(.gray)
+ .frame(width: 6, height: 6)
+ Text("No phone")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .padding()
+ }
+}
diff --git a/tabatago-swift/TabataGoWatch/Views/WatchPlayerView.swift b/tabatago-swift/TabataGoWatch/Views/WatchPlayerView.swift
new file mode 100644
index 0000000..d1306b5
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Views/WatchPlayerView.swift
@@ -0,0 +1,156 @@
+import SwiftUI
+
+/// Active workout player on Watch — full-screen timer with phase, HR, calories.
+struct WatchPlayerView: View {
+ @EnvironmentObject private var engine: WatchPlayerEngine
+
+ var body: some View {
+ ZStack {
+ // Phase color background
+ watchPhaseColor(engine.phase)
+ .opacity(0.18)
+ .ignoresSafeArea()
+
+ VStack(spacing: 4) {
+
+ // ── Phase label ────────────────────────────────────
+ Text(watchPhaseLabel(engine.phase))
+ .font(.system(size: 11, weight: .bold, design: .rounded))
+ .foregroundStyle(watchPhaseColor(engine.phase))
+ .kerning(1.5)
+
+ // ── Timer ──────────────────────────────────────────
+ Text("\(engine.timeRemaining)")
+ .font(.system(size: 52, weight: .black, design: .rounded))
+ .monospacedDigit()
+ .foregroundStyle(.white)
+ .contentTransition(.numericText(countsDown: true))
+ .animation(.spring(duration: 0.3), value: engine.timeRemaining)
+
+ // ── Exercise name ──────────────────────────────────
+ if let name = engine.currentExerciseName {
+ Text(name)
+ .font(.system(size: 12, weight: .semibold))
+ .foregroundStyle(.white.opacity(0.85))
+ .lineLimit(1)
+ .minimumScaleFactor(0.7)
+ }
+
+ // ── Round pips ─────────────────────────────────────
+ RoundPips(
+ current: engine.currentRound,
+ total: min(engine.totalRoundsInBlock, 8)
+ )
+ .padding(.vertical, 4)
+
+ // ── Live metrics ───────────────────────────────────
+ HStack(spacing: 16) {
+ if engine.heartRate > 0 {
+ WatchMetric(
+ icon: "heart.fill",
+ value: "\(Int(engine.heartRate))",
+ color: .red
+ )
+ }
+ if engine.activeCalories > 0 {
+ WatchMetric(
+ icon: "flame.fill",
+ value: "\(Int(engine.activeCalories))",
+ color: .orange
+ )
+ }
+ }
+
+ // ── Pause / End controls ───────────────────────────
+ HStack(spacing: 12) {
+ Button {
+ engine.togglePause()
+ } label: {
+ Image(systemName: engine.isPaused ? "play.fill" : "pause.fill")
+ .font(.system(size: 14, weight: .bold))
+ .frame(width: 36, height: 36)
+ .background(.ultraThinMaterial)
+ .clipShape(Circle())
+ }
+ .buttonStyle(.plain)
+
+ Button(role: .destructive) {
+ engine.endWorkout()
+ } label: {
+ Image(systemName: "xmark")
+ .font(.system(size: 13, weight: .bold))
+ .frame(width: 36, height: 36)
+ .background(.ultraThinMaterial)
+ .clipShape(Circle())
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(.horizontal, 8)
+ }
+ }
+
+ private func watchPhaseColor(_ phase: WatchPhase) -> Color {
+ switch phase {
+ case .prep, .warmup: return .orange
+ case .work: return Color(red: 1.0, green: 0.42, blue: 0.21)
+ case .rest, .interBlockRest: return Color(red: 0.35, green: 0.78, blue: 0.98)
+ case .cooldown: return .cyan
+ case .complete: return .green
+ }
+ }
+
+ private func watchPhaseLabel(_ phase: WatchPhase) -> String {
+ switch phase {
+ case .prep: return "GET READY"
+ case .warmup: return "WARM UP"
+ case .work: return "WORK"
+ case .rest: return "REST"
+ case .interBlockRest: return "BREAK"
+ case .cooldown: return "COOL DOWN"
+ case .complete: return "DONE"
+ }
+ }
+}
+
+// ─── Sub-components ───────────────────────────────────────────────
+
+struct RoundPips: View {
+ let current: Int
+ let total: Int
+
+ var body: some View {
+ HStack(spacing: 4) {
+ ForEach(1...max(total, 1), id: \.self) { i in
+ Capsule()
+ .fill(i < current ? Color.orange :
+ i == current ? .white : .white.opacity(0.25))
+ .frame(width: i == current ? 16 : 6, height: 5)
+ .animation(.spring(duration: 0.25), value: current)
+ }
+ }
+ }
+}
+
+struct WatchMetric: View {
+ let icon: String
+ let value: String
+ let color: Color
+
+ var body: some View {
+ HStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 10))
+ .foregroundStyle(color)
+ Text(value)
+ .font(.system(size: 13, weight: .semibold, design: .rounded))
+ .monospacedDigit()
+ }
+ }
+}
+
+#Preview {
+ WatchPlayerView()
+ .environmentObject(WatchPlayerEngine())
+ .environmentObject(WatchConnectivityManager.shared)
+}
diff --git a/tabatago-swift/TabataGoWatch/Views/WatchRootView.swift b/tabatago-swift/TabataGoWatch/Views/WatchRootView.swift
new file mode 100644
index 0000000..dac9862
--- /dev/null
+++ b/tabatago-swift/TabataGoWatch/Views/WatchRootView.swift
@@ -0,0 +1,17 @@
+import SwiftUI
+
+/// Root view: shows idle screen or active player depending on state.
+struct WatchRootView: View {
+ @EnvironmentObject private var playerEngine: WatchPlayerEngine
+
+ var body: some View {
+ Group {
+ if playerEngine.isActive {
+ WatchPlayerView()
+ } else {
+ WatchIdleView()
+ }
+ }
+ .animation(.easeInOut(duration: 0.3), value: playerEngine.isActive)
+ }
+}
diff --git a/tabatago-swift/project.yml b/tabatago-swift/project.yml
new file mode 100644
index 0000000..de69625
--- /dev/null
+++ b/tabatago-swift/project.yml
@@ -0,0 +1,181 @@
+name: TabataGo
+
+options:
+ bundleIdPrefix: com.tabatago
+ deploymentTarget:
+ iOS: "26.0"
+ watchOS: "11.0"
+ xcodeVersion: "26"
+ generateEmptyDirectories: true
+ createIntermediateGroups: true
+ groupSortPosition: top
+
+settings:
+ base:
+ SWIFT_VERSION: "6.0"
+ IPHONEOS_DEPLOYMENT_TARGET: "26.0"
+ SWIFT_STRICT_CONCURRENCY: complete
+ DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
+
+packages:
+ Supabase:
+ url: https://github.com/supabase/supabase-swift
+ from: "2.5.0"
+ RevenueCat:
+ url: https://github.com/RevenueCat/purchases-ios
+ from: "5.0.0"
+ PostHog:
+ url: https://github.com/PostHog/posthog-ios
+ from: "3.0.0"
+
+targets:
+ TabataGo:
+ type: application
+ platform: iOS
+ deploymentTarget: "26.0"
+ sources:
+ - path: TabataGo
+ excludes:
+ - "**/.DS_Store"
+ resources:
+ - path: TabataGo/Resources
+ excludes:
+ - Info.plist
+ info:
+ path: TabataGo/Resources/Info.plist
+ properties:
+ CFBundleDisplayName: TabataGo
+ CFBundleShortVersionString: "1.0"
+ CFBundleVersion: "1"
+ UILaunchScreen:
+ UIColorName: ""
+ UIImageName: ""
+ UISupportedInterfaceOrientations:
+ - UIInterfaceOrientationPortrait
+ NSHealthShareUsageDescription: "TabataGo reads your health data to show fitness stats and personalize your workouts."
+ NSHealthUpdateUsageDescription: "TabataGo saves your Tabata workouts to Apple Health to track calories, heart rate, and contribute to your Activity Rings."
+ NSMotionUsageDescription: "TabataGo uses motion data to improve calorie estimates during workouts."
+ SUPABASE_URL: $(SUPABASE_URL)
+ SUPABASE_ANON_KEY: $(SUPABASE_ANON_KEY)
+ REVENUECAT_API_KEY: $(REVENUECAT_API_KEY)
+ POSTHOG_API_KEY: $(POSTHOG_API_KEY)
+ entitlements:
+ path: TabataGo/Resources/TabataGo.entitlements
+ properties:
+ com.apple.developer.healthkit: true
+ com.apple.developer.healthkit.access:
+ - health-records
+ com.apple.security.application-groups:
+ - group.com.tabatago.app
+ dependencies:
+ - package: Supabase
+ product: Supabase
+ - package: RevenueCat
+ product: RevenueCat
+ - package: PostHog
+ product: PostHog
+ - target: TabataGoWatch
+ embed: true
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app
+ INFOPLIST_FILE: TabataGo/Resources/Info.plist
+ CODE_SIGN_ENTITLEMENTS: TabataGo/Resources/TabataGo.entitlements
+ ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
+ ENABLE_PREVIEWS: YES
+ configFiles:
+ Debug: Config/Secrets.xcconfig
+ Release: Config/Secrets.xcconfig
+
+ TabataGoWatch:
+ type: application
+ platform: watchOS
+ deploymentTarget: "11.0"
+ sources:
+ - path: TabataGoWatch
+ excludes:
+ - "**/.DS_Store"
+ - "Resources/Info.plist"
+ - "Complications/**"
+ # Shared protocol types — referenced by both targets
+ - path: TabataGo/Services/WatchConnectivityTypes.swift
+ group: TabataGoWatch/Services
+ resources:
+ - path: TabataGoWatch/Resources
+ excludes:
+ - Info.plist
+ info:
+ path: TabataGoWatch/Resources/Info.plist
+ properties:
+ CFBundleDisplayName: TabataGo
+ CFBundleShortVersionString: "1.0"
+ CFBundleVersion: "1"
+ WKApplication: true
+ WKCompanionAppBundleIdentifier: com.tabatago.app
+ NSHealthShareUsageDescription: "TabataGo reads your heart rate and calories during workouts."
+ NSHealthUpdateUsageDescription: "TabataGo saves workout data to Apple Health directly from your Watch."
+ entitlements:
+ path: TabataGoWatch/Resources/TabataGoWatch.entitlements
+ properties:
+ com.apple.developer.healthkit: true
+ com.apple.security.application-groups:
+ - group.com.tabatago.app
+ dependencies:
+ - target: TabataGoWatchWidget
+ embed: true
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.watchkitapp
+ INFOPLIST_FILE: TabataGoWatch/Resources/Info.plist
+ CODE_SIGN_ENTITLEMENTS: TabataGoWatch/Resources/TabataGoWatch.entitlements
+ ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
+ WATCHOS_DEPLOYMENT_TARGET: "11.0"
+ TARGETED_DEVICE_FAMILY: "4"
+ ENABLE_PREVIEWS: YES
+
+ TabataGoWatchWidget:
+ type: app-extension
+ platform: watchOS
+ deploymentTarget: "11.0"
+ sources:
+ - path: TabataGoWatch/Complications
+ excludes:
+ - "**/.DS_Store"
+ info:
+ path: TabataGoWatch/Complications/Info.plist
+ properties:
+ CFBundleDisplayName: TabataGoWidget
+ CFBundleShortVersionString: "1.0"
+ CFBundleVersion: "1"
+ NSExtension:
+ NSExtensionPointIdentifier: com.apple.widgetkit-extension
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.watchkitapp.widget
+ TARGETED_DEVICE_FAMILY: "4"
+ WATCHOS_DEPLOYMENT_TARGET: "11.0"
+
+ TabataGoTests:
+ type: bundle.unit-test
+ platform: iOS
+ deploymentTarget: "26.0"
+ sources:
+ - TabataGoTests
+ dependencies:
+ - target: TabataGo
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.tests
+
+ TabataGoUITests:
+ type: bundle.ui-testing
+ platform: iOS
+ deploymentTarget: "26.0"
+ sources:
+ - TabataGoUITests
+ dependencies:
+ - target: TabataGo
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.uitests
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 9188a38..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "extends": "expo/tsconfig.base",
- "compilerOptions": {
- "strict": true,
- "resolveJsonModule": true,
- "paths": {
- "@/*": [
- "./*"
- ]
- }
- },
- "include": [
- "**/*.ts",
- "**/*.tsx",
- ".expo/types/**/*.ts",
- "expo-env.d.ts"
- ],
- "exclude": [
- "node_modules",
- "admin-web",
- "supabase/functions"
- ]
-}
diff --git a/vitest.config.render.ts b/vitest.config.render.ts
deleted file mode 100644
index d4b8682..0000000
--- a/vitest.config.render.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { defineConfig } from 'vitest/config'
-import { resolve } from 'path'
-
-export default defineConfig({
- test: {
- globals: true,
- environment: 'jsdom',
- setupFiles: ['./src/__tests__/setup-render.tsx'],
- include: ['src/__tests__/components/rendering/**/*.test.tsx', 'src/__tests__/integration/*.render.test.tsx'],
- testTimeout: 15000,
- hookTimeout: 15000,
- pool: 'forks',
- execArgv: ['--require', resolve(__dirname, 'src/__tests__/mocks/preload-rn-mock.cjs')],
- server: {
- deps: {
- inline: [
- '@testing-library/react-native',
- 'react-native-reanimated',
- 'react-native-gesture-handler',
- 'react-native-screens',
- 'react-native-safe-area-context',
- 'react-native-svg',
- ],
- },
- },
- },
- resolve: {
- alias: {
- '@': resolve(__dirname, '.'),
- 'react-native': resolve(__dirname, 'src/__tests__/mocks/react-native.ts'),
- },
- },
-})
diff --git a/vitest.config.ts b/vitest.config.ts
deleted file mode 100644
index c3fd501..0000000
--- a/vitest.config.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { defineConfig } from 'vitest/config'
-import { resolve } from 'path'
-
-export default defineConfig({
- test: {
- globals: true,
- environment: 'jsdom',
- setupFiles: ['./src/__tests__/setup.ts'],
- include: ['src/**/*.{test,spec}.{ts,tsx}'],
- exclude: [
- 'src/__tests__/components/rendering/**',
- 'src/__tests__/integration/*.render.test.tsx',
- 'node_modules',
- ],
- coverage: {
- provider: 'v8',
- reporter: ['text', 'json', 'json-summary', 'html', 'lcov'],
- include: ['src/**/*.{ts,tsx}'],
- exclude: [
- 'src/**/__tests__/**',
- 'src/**/*.d.ts',
- 'src/**/*.test.{ts,tsx}',
- 'src/**/*.spec.{ts,tsx}',
- 'src/**/index.ts',
- 'src/**/types.ts',
- 'src/**/CLAUDE.md',
- 'src/shared/data/dataService.ts',
- 'src/shared/data/useTranslatedData.ts',
- 'src/shared/services/sync.ts',
- 'src/shared/services/analytics.ts',
- ],
- thresholds: {
- 'src/shared/stores/**': { lines: 85, branches: 65, functions: 90, statements: 85 },
- 'src/shared/services/purchases.ts': { lines: 100 },
- 'src/shared/services/sync.ts': { lines: 80 },
- 'src/shared/services/analytics.ts': { lines: 80 },
- 'src/shared/data/achievements.ts': { lines: 100 },
- 'src/shared/data/programs.ts': { lines: 90 },
- 'src/shared/data/trainers.ts': { lines: 100 },
- 'src/shared/data/workouts.ts': { lines: 100 },
- },
- },
- testTimeout: 10000,
- hookTimeout: 10000,
- },
- resolve: {
- alias: {
- '@': resolve(__dirname, '.'),
- },
- },
-})