diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..c791527
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,88 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+### Start Everything
+```bash
+./develop.sh # starts PostgreSQL (Docker), backend (uvicorn), and frontend (vite) together
+```
+
+### Backend
+```bash
+cd backend
+source .venv/bin/activate
+uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
+ruff check . # lint
+ruff format . # format
+pytest # run tests
+```
+
+### Frontend
+```bash
+cd frontend
+npm run dev # dev server on :5173
+npm run build # production build
+npm run lint # ESLint
+```
+
+### Database
+```bash
+docker compose up -d # start PostgreSQL with pgvector on :5432
+```
+
+## Architecture
+
+Full-stack health/fitness tracker with AI-powered nutrition analysis and plan generation.
+
+**Stack:** FastAPI (Python) + React 19 (TypeScript) + PostgreSQL/pgvector + DSPy + OpenAI GPT-4o
+
+### Backend (`/backend`)
+- `app/main.py` — FastAPI app, CORS config, router mounting
+- `app/api/v1/endpoints/` — Route handlers: `login`, `users`, `nutrition`, `health`, `plans`
+- `app/models/` — SQLModel ORM models (User, HealthMetric, HealthGoal, FoodLog, FoodItem, Plan)
+- `app/ai/` — DSPy modules: `nutrition.py` (food analysis + image analysis), `plans.py` (personalized plan generation)
+- `app/core/ai_config.py` — DSPy LM configuration (GPT-4o mini)
+- `app/api/deps.py` — FastAPI dependencies for auth and DB session injection
+
+### Frontend (`/src`)
+- `App.tsx` — React Router routes; all routes except `/login` are wrapped in `ProtectedRoute`
+- `api/client.ts` — Axios instance with base URL from `VITE_API_URL`; Bearer token auto-attached via interceptor from localStorage
+- `context/AuthContext.tsx` — Auth state (token, user); token persisted to localStorage
+- `pages/` — One page component per route (Dashboard, Nutrition, Health, Plans, Profile)
+- `components/catalyst/` — Reusable UI component library (buttons, inputs, tables, dialogs, etc.)
+
+### Auth Flow
+OAuth2 password flow → JWT (HS256, 8-day expiry) → stored in localStorage → Axios interceptor injects header on every request.
+
+### AI Modules (DSPy)
+Both AI modules use chain-of-thought (`dspy.ChainOfThought`) via GPT-4o mini:
+- **Nutrition:** Analyzes food text/images → macro breakdown accounting for hidden calories (oils, sauces)
+- **Plans:** Generates personalized diet + exercise plans from user profile data
+
+### Environment Variables
+See `.env.example`. Required: `DATABASE_URL`, `OPENAI_API_KEY`, `SECRET_KEY`, `POSTGRES_*` vars. Frontend uses `VITE_API_URL` (defaults to `http://localhost:8000/api/v1`).
+
+## Push Notifications
+
+### Testing push notifications
+**Always use the production build** — never test push notifications against `npm run dev`. The dev server's hot-reloading reinstalls the service worker on every reload, which invalidates push subscriptions (FCM returns 410 Gone).
+
+```bash
+cd frontend
+npm run build
+npx serve dist -p 5173 -s # -s = SPA mode (falls back to index.html)
+```
+
+Then in the browser:
+1. DevTools → Application → Service Workers → Unregister any old SW
+2. Hard-refresh (`Cmd+Shift+R`) to install the fresh `sw.js`
+3. Profile → Notifications → toggle ON to create a fresh subscription
+4. Use "Send test" button or trigger via backend
+
+### If push notifications stop working
+- Check `chrome://gcm-internals/` — Connection State must be **CONNECTED** (not CONNECTING)
+- If disconnected, fully quit and restart Chrome (`Cmd+Q`)
+- Clear stale DB subscriptions: `docker compose exec db psql -U user -d healthyfit -c "DELETE FROM pushsubscription;"`
+- Re-subscribe after restarting
diff --git a/backend/app/ai/kettlebell.py b/backend/app/ai/kettlebell.py
new file mode 100644
index 0000000..3ec215c
--- /dev/null
+++ b/backend/app/ai/kettlebell.py
@@ -0,0 +1,57 @@
+import dspy
+from pydantic import BaseModel, Field
+
+
+class ExerciseBlock(BaseModel):
+ order: int = Field(description="Position in workout sequence")
+ name: str = Field(description="Exercise name")
+ description: str = Field(description="Brief description of the movement")
+ sets: int = Field(description="Number of sets")
+ reps: int = Field(description="Number of reps per set (0 if timed)")
+ duration_seconds: int = Field(description="Duration in seconds per set (0 if rep-based)")
+ weight_kg: float = Field(description="Prescribed weight in kg")
+ rest_seconds: int = Field(description="Rest time between sets in seconds")
+ coaching_tip: str = Field(description="Key coaching cue for this exercise")
+
+
+class KettlebellSessionOutput(BaseModel):
+ reasoning: str = Field(description="Step-by-step reasoning for session design choices")
+ title: str = Field(description="Session title")
+ focus: str = Field(description="Session focus e.g. strength, conditioning, mobility")
+ total_duration_min: int = Field(description="Estimated total workout duration in minutes")
+ difficulty: str = Field(description="Difficulty level: beginner, intermediate, advanced")
+ exercises: list[ExerciseBlock] = Field(description="Ordered list of exercises in the session")
+ notes: str = Field(description="Coaching notes and any special instructions for the session")
+
+
+class GenerateKettlebellSession(dspy.Signature):
+ """Generate a personalized kettlebell workout session based on user profile and preferences.
+
+ Think step-by-step: assess user fitness level, pick movements appropriate to the focus and
+ difficulty, assign weights respecting progressive overload principles from available weights,
+ sequence exercises for proper warm-up and fatigue management, and ensure total work time
+ (sets × reps/duration + rest periods) fits within the requested duration.
+ """
+
+ user_profile: str = dspy.InputField(desc="User details including age, weight, fitness level, and goals")
+ available_weights_kg: str = dspy.InputField(desc="Comma-separated list of available kettlebell weights in kg")
+ focus: str = dspy.InputField(desc="Session focus: strength, conditioning, mobility, fat loss, etc.")
+ duration_minutes: int = dspy.InputField(desc="Target session duration in minutes")
+ session: KettlebellSessionOutput = dspy.OutputField(desc="Complete structured kettlebell session")
+
+
+class KettlebellModule(dspy.Module):
+ def __init__(self):
+ super().__init__()
+ self.generate = dspy.ChainOfThought(GenerateKettlebellSession)
+
+ def forward(self, user_profile: str, available_weights_kg: str, focus: str, duration_minutes: int):
+ return self.generate(
+ user_profile=user_profile,
+ available_weights_kg=available_weights_kg,
+ focus=focus,
+ duration_minutes=duration_minutes,
+ )
+
+
+kettlebell_module = KettlebellModule()
diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py
index 81a9953..7eb7d38 100644
--- a/backend/app/api/v1/api.py
+++ b/backend/app/api/v1/api.py
@@ -1,6 +1,6 @@
from fastapi import APIRouter
-from app.api.v1.endpoints import health, login, nutrition, plans, users
+from app.api.v1.endpoints import calendar, health, kettlebell, login, nutrition, plans, push, supplements, users
api_router = APIRouter()
api_router.include_router(login.router, tags=["login"])
@@ -8,3 +8,7 @@ api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(nutrition.router, prefix="/nutrition", tags=["nutrition"])
api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(plans.router, prefix="/plans", tags=["plans"])
+api_router.include_router(kettlebell.router, prefix="/kettlebell", tags=["kettlebell"])
+api_router.include_router(supplements.router, prefix="/supplements", tags=["supplements"])
+api_router.include_router(calendar.router, prefix="/calendar", tags=["calendar"])
+api_router.include_router(push.router, prefix="/push", tags=["push"])
diff --git a/backend/app/api/v1/endpoints/calendar.py b/backend/app/api/v1/endpoints/calendar.py
new file mode 100644
index 0000000..64890b5
--- /dev/null
+++ b/backend/app/api/v1/endpoints/calendar.py
@@ -0,0 +1,462 @@
+from calendar import monthrange
+from datetime import date, datetime
+from typing import Any, List, Optional
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from pydantic import BaseModel
+from sqlmodel import Session, select
+
+from app.api import deps
+from app.models.calendar import CalendarEvent, DailyNote
+from app.models.kettlebell import KettlebellSession
+from app.models.supplement import Supplement, SupplementLog
+
+router = APIRouter()
+
+
+# ── Pydantic schemas ─────────────────────────────────────────────────────────
+
+
+class DayMeta(BaseModel):
+ date: str
+ has_note: bool
+ event_count: int
+ supplement_compliance: Optional[float]
+ has_workout: bool
+ calorie_total: Optional[float]
+
+
+class DailyNoteUpsert(BaseModel):
+ content: str = ""
+ mood: Optional[str] = None
+ energy_level: Optional[int] = None
+
+
+class DailyNoteOut(BaseModel):
+ id: Optional[int]
+ date: str
+ content: str
+ mood: Optional[str]
+ energy_level: Optional[int]
+ updated_at: Optional[str]
+
+
+class CalendarEventCreate(BaseModel):
+ date: str # YYYY-MM-DD
+ title: str
+ description: Optional[str] = None
+ event_type: str = "general"
+ color: Optional[str] = None
+ start_time: Optional[str] = None
+ is_completed: bool = False
+
+
+class CalendarEventUpdate(BaseModel):
+ title: Optional[str] = None
+ description: Optional[str] = None
+ event_type: Optional[str] = None
+ color: Optional[str] = None
+ start_time: Optional[str] = None
+ is_completed: Optional[bool] = None
+
+
+class CalendarEventOut(BaseModel):
+ id: int
+ date: str
+ title: str
+ description: Optional[str]
+ event_type: str
+ color: Optional[str]
+ start_time: Optional[str]
+ is_completed: bool
+ created_at: Optional[str]
+
+
+class SupplementWithStatus(BaseModel):
+ id: int
+ name: str
+ dosage: float
+ unit: str
+ frequency: str
+ scheduled_times: List[str]
+ notes: Optional[str]
+ is_active: bool
+ created_at: str
+ taken_today: bool
+ streak: int
+
+
+class KettlebellSessionSummary(BaseModel):
+ id: int
+ title: str
+ focus: str
+ total_duration_min: int
+ difficulty: str
+ status: str
+ completed_at: Optional[str]
+
+
+class DayDetail(BaseModel):
+ date: str
+ note: Optional[DailyNoteOut]
+ events: List[CalendarEventOut]
+ supplements: List[SupplementWithStatus]
+ kettlebell_sessions: List[KettlebellSessionSummary]
+
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+
+
+def _day_window(d: date):
+ """Return (start_dt, end_dt) for a full day."""
+ start = datetime(d.year, d.month, d.day, 0, 0, 0)
+ end = datetime(d.year, d.month, d.day, 23, 59, 59)
+ return start, end
+
+
+def _note_out(note: DailyNote) -> DailyNoteOut:
+ return DailyNoteOut(
+ id=note.id,
+ date=note.date.isoformat(),
+ content=note.content,
+ mood=note.mood,
+ energy_level=note.energy_level,
+ updated_at=note.updated_at.isoformat() if note.updated_at else None,
+ )
+
+
+def _event_out(ev: CalendarEvent) -> CalendarEventOut:
+ return CalendarEventOut(
+ id=ev.id,
+ date=ev.date.isoformat(),
+ title=ev.title,
+ description=ev.description,
+ event_type=ev.event_type,
+ color=ev.color,
+ start_time=ev.start_time,
+ is_completed=ev.is_completed,
+ created_at=ev.created_at.isoformat() if ev.created_at else None,
+ )
+
+
+# ── Endpoints ─────────────────────────────────────────────────────────────────
+
+
+@router.get("/month", response_model=List[DayMeta])
+def get_month_summary(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+ year: int = Query(...),
+ month: int = Query(...),
+) -> Any:
+ _, last_day = monthrange(year, month)
+ month_start = date(year, month, 1)
+ month_end = date(year, month, last_day)
+ month_start_dt = datetime(year, month, 1, 0, 0, 0)
+ month_end_dt = datetime(year, month, last_day, 23, 59, 59)
+
+ uid = current_user.id
+
+ # DailyNotes
+ notes = session.exec(
+ select(DailyNote)
+ .where(DailyNote.user_id == uid)
+ .where(DailyNote.date >= month_start)
+ .where(DailyNote.date <= month_end)
+ ).all()
+ notes_by_date = {n.date: n for n in notes}
+
+ # CalendarEvents
+ events = session.exec(
+ select(CalendarEvent)
+ .where(CalendarEvent.user_id == uid)
+ .where(CalendarEvent.date >= month_start)
+ .where(CalendarEvent.date <= month_end)
+ ).all()
+ event_counts: dict[date, int] = {}
+ for ev in events:
+ event_counts[ev.date] = event_counts.get(ev.date, 0) + 1
+
+ # SupplementLogs
+ active_supp_count = session.exec(
+ select(Supplement).where(Supplement.user_id == uid).where(Supplement.is_active)
+ ).all()
+ total_supplements = len(active_supp_count)
+ supp_logs = session.exec(
+ select(SupplementLog)
+ .where(SupplementLog.user_id == uid)
+ .where(SupplementLog.taken_at >= month_start_dt)
+ .where(SupplementLog.taken_at <= month_end_dt)
+ ).all()
+ supp_by_date: dict[date, set] = {}
+ for log in supp_logs:
+ d = log.taken_at.date()
+ supp_by_date.setdefault(d, set()).add(log.supplement_id)
+
+ # KettlebellSessions
+ kb_sessions = session.exec(
+ select(KettlebellSession)
+ .where(KettlebellSession.user_id == uid)
+ .where(KettlebellSession.status == "completed")
+ .where(KettlebellSession.completed_at >= month_start_dt)
+ .where(KettlebellSession.completed_at <= month_end_dt)
+ ).all()
+ workout_dates: set[date] = set()
+ for kb in kb_sessions:
+ if kb.completed_at:
+ workout_dates.add(kb.completed_at.date())
+
+ # FoodLogs — import lazily to avoid circular imports
+ from app.models.food import FoodLog # noqa: PLC0415
+
+ food_logs = session.exec(
+ select(FoodLog)
+ .where(FoodLog.user_id == uid)
+ .where(FoodLog.timestamp >= month_start_dt)
+ .where(FoodLog.timestamp <= month_end_dt)
+ ).all()
+ calories_by_date: dict[date, float] = {}
+ for fl in food_logs:
+ d = fl.timestamp.date()
+ calories_by_date[d] = calories_by_date.get(d, 0.0) + (fl.calories or 0.0)
+
+ result = []
+ for day_num in range(1, last_day + 1):
+ d = date(year, month, day_num)
+ taken = len(supp_by_date.get(d, set()))
+ compliance = (taken / total_supplements) if total_supplements > 0 else None
+ result.append(
+ DayMeta(
+ date=d.isoformat(),
+ has_note=bool(notes_by_date.get(d) and notes_by_date[d].content),
+ event_count=event_counts.get(d, 0),
+ supplement_compliance=compliance,
+ has_workout=d in workout_dates,
+ calorie_total=calories_by_date.get(d) or None,
+ )
+ )
+ return result
+
+
+@router.get("/day", response_model=DayDetail)
+def get_day_detail(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+ date_str: str = Query(..., alias="date"),
+) -> Any:
+ try:
+ d = date.fromisoformat(date_str)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD.")
+
+ uid = current_user.id
+ start, end = _day_window(d)
+
+ note = session.exec(select(DailyNote).where(DailyNote.user_id == uid).where(DailyNote.date == d)).first()
+
+ events = session.exec(
+ select(CalendarEvent)
+ .where(CalendarEvent.user_id == uid)
+ .where(CalendarEvent.date == d)
+ .order_by(CalendarEvent.start_time)
+ ).all()
+
+ # Supplements with taken status for this date
+ active_supps = session.exec(
+ select(Supplement).where(Supplement.user_id == uid).where(Supplement.is_active).order_by(Supplement.name)
+ ).all()
+ day_logs = session.exec(
+ select(SupplementLog)
+ .where(SupplementLog.user_id == uid)
+ .where(SupplementLog.taken_at >= start)
+ .where(SupplementLog.taken_at <= end)
+ ).all()
+ taken_ids = {log.supplement_id for log in day_logs}
+ supplements = [
+ SupplementWithStatus(
+ id=s.id,
+ name=s.name,
+ dosage=s.dosage,
+ unit=s.unit,
+ frequency=s.frequency,
+ scheduled_times=s.scheduled_times or [],
+ notes=s.notes,
+ is_active=s.is_active,
+ created_at=s.created_at.isoformat(),
+ taken_today=s.id in taken_ids,
+ streak=0,
+ )
+ for s in active_supps
+ ]
+
+ kb_sessions = session.exec(
+ select(KettlebellSession)
+ .where(KettlebellSession.user_id == uid)
+ .where(KettlebellSession.created_at >= start)
+ .where(KettlebellSession.created_at <= end)
+ ).all()
+ kb_out = [
+ KettlebellSessionSummary(
+ id=kb.id,
+ title=kb.title,
+ focus=kb.focus,
+ total_duration_min=kb.total_duration_min,
+ difficulty=kb.difficulty,
+ status=kb.status,
+ completed_at=kb.completed_at.isoformat() if kb.completed_at else None,
+ )
+ for kb in kb_sessions
+ ]
+
+ return DayDetail(
+ date=d.isoformat(),
+ note=_note_out(note) if note else None,
+ events=[_event_out(ev) for ev in events],
+ supplements=supplements,
+ kettlebell_sessions=kb_out,
+ )
+
+
+@router.get("/notes/{date_str}", response_model=DailyNoteOut)
+def get_note(
+ date_str: str,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ try:
+ d = date.fromisoformat(date_str)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="Invalid date format.")
+
+ note = session.exec(
+ select(DailyNote).where(DailyNote.user_id == current_user.id).where(DailyNote.date == d)
+ ).first()
+ if not note:
+ raise HTTPException(status_code=404, detail="Note not found")
+ return _note_out(note)
+
+
+@router.put("/notes/{date_str}", response_model=DailyNoteOut)
+def upsert_note(
+ date_str: str,
+ data: DailyNoteUpsert,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ try:
+ d = date.fromisoformat(date_str)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="Invalid date format.")
+
+ note = session.exec(
+ select(DailyNote).where(DailyNote.user_id == current_user.id).where(DailyNote.date == d)
+ ).first()
+
+ if note:
+ note.content = data.content
+ if data.mood is not None:
+ note.mood = data.mood
+ if data.energy_level is not None:
+ note.energy_level = data.energy_level
+ note.updated_at = datetime.utcnow()
+ else:
+ note = DailyNote(
+ user_id=current_user.id,
+ date=d,
+ content=data.content,
+ mood=data.mood,
+ energy_level=data.energy_level,
+ updated_at=datetime.utcnow(),
+ )
+
+ session.add(note)
+ session.commit()
+ session.refresh(note)
+ return _note_out(note)
+
+
+@router.get("/events", response_model=List[CalendarEventOut])
+def list_events(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+ start: str = Query(...),
+ end: str = Query(...),
+) -> Any:
+ try:
+ start_date = date.fromisoformat(start)
+ end_date = date.fromisoformat(end)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="Invalid date format.")
+
+ events = session.exec(
+ select(CalendarEvent)
+ .where(CalendarEvent.user_id == current_user.id)
+ .where(CalendarEvent.date >= start_date)
+ .where(CalendarEvent.date <= end_date)
+ .order_by(CalendarEvent.date, CalendarEvent.start_time)
+ ).all()
+ return [_event_out(ev) for ev in events]
+
+
+@router.post("/events", response_model=CalendarEventOut, status_code=201)
+def create_event(
+ data: CalendarEventCreate,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ try:
+ d = date.fromisoformat(data.date)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="Invalid date format.")
+
+ ev = CalendarEvent(
+ user_id=current_user.id,
+ date=d,
+ title=data.title,
+ description=data.description,
+ event_type=data.event_type,
+ color=data.color,
+ start_time=data.start_time,
+ is_completed=data.is_completed,
+ )
+ session.add(ev)
+ session.commit()
+ session.refresh(ev)
+ return _event_out(ev)
+
+
+@router.put("/events/{event_id}", response_model=CalendarEventOut)
+def update_event(
+ event_id: int,
+ data: CalendarEventUpdate,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ ev = session.get(CalendarEvent, event_id)
+ if not ev:
+ raise HTTPException(status_code=404, detail="Event not found")
+ if ev.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ update_data = data.model_dump(exclude_unset=True)
+ for key, value in update_data.items():
+ setattr(ev, key, value)
+ session.add(ev)
+ session.commit()
+ session.refresh(ev)
+ return _event_out(ev)
+
+
+@router.delete("/events/{event_id}", status_code=204)
+def delete_event(
+ event_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> None:
+ ev = session.get(CalendarEvent, event_id)
+ if not ev:
+ raise HTTPException(status_code=404, detail="Event not found")
+ if ev.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ session.delete(ev)
+ session.commit()
diff --git a/backend/app/api/v1/endpoints/kettlebell.py b/backend/app/api/v1/endpoints/kettlebell.py
new file mode 100644
index 0000000..6256845
--- /dev/null
+++ b/backend/app/api/v1/endpoints/kettlebell.py
@@ -0,0 +1,404 @@
+from collections import defaultdict
+from datetime import datetime, timedelta
+from typing import Any, Dict, List, Optional
+
+from fastapi import APIRouter, Depends, HTTPException
+from pydantic import BaseModel
+from sqlmodel import Session, select
+
+from app.ai.kettlebell import kettlebell_module
+from app.api import deps
+from app.models.kettlebell import KettlebellSession, KettlebellSetLog
+
+router = APIRouter()
+
+
+class KettlebellGenerateRequest(BaseModel):
+ focus: str
+ duration_minutes: int
+ available_weights: list[float]
+
+
+class LogSetRequest(BaseModel):
+ exercise_order: int
+ set_number: int
+ actual_reps: int
+ actual_weight_kg: float
+ actual_duration_seconds: int
+ perceived_effort: int
+
+
+class CompleteSessionRequest(BaseModel):
+ notes: Optional[str] = None
+
+
+class ExerciseProgression(BaseModel):
+ date: str
+ max_weight: float
+ avg_rpe: float
+ total_volume: float
+
+
+class PersonalRecord(BaseModel):
+ exercise_name: str
+ max_weight: float
+ date: str
+
+
+class WeeklyVolume(BaseModel):
+ week: str
+ sessions: int
+ total_volume: float
+
+
+class AnalyticsResponse(BaseModel):
+ weekly_sessions: List[WeeklyVolume]
+ exercise_progressions: Dict[str, List[ExerciseProgression]]
+ personal_records: List[PersonalRecord]
+ avg_rpe_trend: List[Dict]
+
+
+@router.get("/analytics", response_model=AnalyticsResponse)
+def get_analytics(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Get aggregated analytics across all completed sessions."""
+ completed_sessions = session.exec(
+ select(KettlebellSession)
+ .where(KettlebellSession.user_id == current_user.id)
+ .where(KettlebellSession.status == "completed")
+ .order_by(KettlebellSession.completed_at)
+ ).all()
+
+ if not completed_sessions:
+ return AnalyticsResponse(
+ weekly_sessions=[],
+ exercise_progressions={},
+ personal_records=[],
+ avg_rpe_trend=[],
+ )
+
+ session_ids = [s.id for s in completed_sessions]
+ all_sets = session.exec(select(KettlebellSetLog).where(KettlebellSetLog.session_id.in_(session_ids))).all()
+
+ # Map session_id -> session
+ session_map = {s.id: s for s in completed_sessions}
+ # Map session_id -> exercise_order -> exercise_name
+ exercise_name_map: Dict[int, Dict[int, str]] = {}
+ for s in completed_sessions:
+ exercises = s.exercises.get("exercises", []) if s.exercises else []
+ exercise_name_map[s.id] = {ex["order"]: ex["name"] for ex in exercises if "order" in ex and "name" in ex}
+
+ # Weekly sessions + volume
+ weeks: Dict[str, Dict] = defaultdict(lambda: {"sessions": set(), "volume": 0.0})
+ for set_log in all_sets:
+ s = session_map.get(set_log.session_id)
+ if not s or not s.completed_at:
+ continue
+ week_start = s.completed_at - timedelta(days=s.completed_at.weekday())
+ week_key = week_start.strftime("%Y-%m-%d")
+ weeks[week_key]["sessions"].add(set_log.session_id)
+ volume = set_log.actual_reps * set_log.actual_weight_kg
+ weeks[week_key]["volume"] += volume
+
+ weekly_sessions = [
+ WeeklyVolume(week=k, sessions=len(v["sessions"]), total_volume=round(v["volume"], 1))
+ for k, v in sorted(weeks.items())
+ ]
+
+ # Exercise progressions + PRs
+ exercise_data: Dict[str, List[Dict]] = defaultdict(list)
+ for set_log in all_sets:
+ s = session_map.get(set_log.session_id)
+ if not s or not s.completed_at:
+ continue
+ ex_name = exercise_name_map.get(set_log.session_id, {}).get(set_log.exercise_order)
+ if not ex_name:
+ continue
+ exercise_data[ex_name].append(
+ {
+ "date": s.completed_at.strftime("%Y-%m-%d"),
+ "weight": set_log.actual_weight_kg,
+ "rpe": set_log.perceived_effort,
+ "volume": set_log.actual_reps * set_log.actual_weight_kg,
+ }
+ )
+
+ exercise_progressions: Dict[str, List[ExerciseProgression]] = {}
+ personal_records: List[PersonalRecord] = []
+
+ for ex_name, sets in exercise_data.items():
+ by_date: Dict[str, Dict] = defaultdict(lambda: {"weights": [], "rpes": [], "volume": 0.0})
+ for s in sets:
+ by_date[s["date"]]["weights"].append(s["weight"])
+ by_date[s["date"]]["rpes"].append(s["rpe"])
+ by_date[s["date"]]["volume"] += s["volume"]
+
+ progressions = [
+ ExerciseProgression(
+ date=d,
+ max_weight=max(v["weights"]),
+ avg_rpe=round(sum(v["rpes"]) / len(v["rpes"]), 1),
+ total_volume=round(v["volume"], 1),
+ )
+ for d, v in sorted(by_date.items())
+ ]
+ exercise_progressions[ex_name] = progressions
+
+ all_weights = [s["weight"] for s in sets]
+ max_w = max(all_weights)
+ pr_date = next(s["date"] for s in sorted(sets, key=lambda x: x["date"]) if s["weight"] == max_w)
+ personal_records.append(PersonalRecord(exercise_name=ex_name, max_weight=max_w, date=pr_date))
+
+ personal_records.sort(key=lambda x: x.max_weight, reverse=True)
+
+ # Avg RPE trend by session
+ avg_rpe_trend = []
+ sets_by_session: Dict[int, List] = defaultdict(list)
+ for set_log in all_sets:
+ sets_by_session[set_log.session_id].append(set_log.perceived_effort)
+
+ for s in completed_sessions:
+ rpes = sets_by_session.get(s.id, [])
+ if rpes and s.completed_at:
+ avg_rpe_trend.append(
+ {
+ "date": s.completed_at.strftime("%Y-%m-%d"),
+ "avg_rpe": round(sum(rpes) / len(rpes), 1),
+ "session_title": s.title,
+ }
+ )
+
+ return AnalyticsResponse(
+ weekly_sessions=weekly_sessions,
+ exercise_progressions=exercise_progressions,
+ personal_records=personal_records,
+ avg_rpe_trend=avg_rpe_trend,
+ )
+
+
+@router.post("/generate", response_model=KettlebellSession)
+def generate_session(
+ *,
+ current_user: deps.CurrentUser,
+ request: KettlebellGenerateRequest,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Generate a new kettlebell session using AI."""
+ try:
+ user_profile = (
+ f"Age: {current_user.age or 'unknown'}, "
+ f"Gender: {current_user.gender or 'unknown'}, "
+ f"Weight: {current_user.weight or 'unknown'} kg, "
+ f"Height: {current_user.height or 'unknown'} cm"
+ )
+ available_weights_kg = ", ".join(str(w) for w in sorted(request.available_weights))
+
+ generated = kettlebell_module(
+ user_profile=user_profile,
+ available_weights_kg=available_weights_kg,
+ focus=request.focus,
+ duration_minutes=request.duration_minutes,
+ )
+
+ s = generated.session
+ kb_session = KettlebellSession(
+ user_id=current_user.id,
+ title=s.title,
+ focus=s.focus,
+ exercises={"exercises": [ex.model_dump() for ex in s.exercises]},
+ total_duration_min=s.total_duration_min,
+ difficulty=s.difficulty,
+ notes=s.notes,
+ status="generated",
+ )
+ session.add(kb_session)
+ session.commit()
+ session.refresh(kb_session)
+ return kb_session
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/", response_model=List[KettlebellSession])
+def list_sessions(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Get all kettlebell sessions for the current user."""
+ statement = (
+ select(KettlebellSession)
+ .where(KettlebellSession.user_id == current_user.id)
+ .order_by(KettlebellSession.created_at.desc())
+ )
+ return session.exec(statement).all()
+
+
+@router.get("/{session_id}", response_model=KettlebellSession)
+def get_session(
+ session_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Get a single kettlebell session."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ return kb_session
+
+
+@router.patch("/{session_id}/start", response_model=KettlebellSession)
+def start_session(
+ session_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Mark a session as in progress."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ kb_session.status = "in_progress"
+ kb_session.started_at = datetime.utcnow()
+ session.add(kb_session)
+ session.commit()
+ session.refresh(kb_session)
+ return kb_session
+
+
+@router.post("/{session_id}/sets", response_model=KettlebellSetLog)
+def log_set(
+ session_id: int,
+ request: LogSetRequest,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Log a completed set."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ set_log = KettlebellSetLog(
+ session_id=session_id,
+ exercise_order=request.exercise_order,
+ set_number=request.set_number,
+ actual_reps=request.actual_reps,
+ actual_weight_kg=request.actual_weight_kg,
+ actual_duration_seconds=request.actual_duration_seconds,
+ perceived_effort=request.perceived_effort,
+ )
+ session.add(set_log)
+ session.commit()
+ session.refresh(set_log)
+ return set_log
+
+
+@router.get("/{session_id}/sets", response_model=List[KettlebellSetLog])
+def get_sets(
+ session_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Get all logged sets for a session."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ statement = select(KettlebellSetLog).where(KettlebellSetLog.session_id == session_id)
+ return session.exec(statement).all()
+
+
+@router.post("/{session_id}/retry", response_model=KettlebellSession)
+def retry_session(
+ session_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Clone an abandoned session as a fresh generated session."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ new_session = KettlebellSession(
+ user_id=current_user.id,
+ title=kb_session.title,
+ focus=kb_session.focus,
+ exercises=kb_session.exercises,
+ total_duration_min=kb_session.total_duration_min,
+ difficulty=kb_session.difficulty,
+ notes="",
+ status="generated",
+ )
+ session.add(new_session)
+ session.commit()
+ session.refresh(new_session)
+ return new_session
+
+
+@router.patch("/{session_id}/abandon", response_model=KettlebellSession)
+def abandon_session(
+ session_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Mark a session as abandoned."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ kb_session.status = "abandoned"
+ session.add(kb_session)
+ session.commit()
+ session.refresh(kb_session)
+ return kb_session
+
+
+@router.delete("/{session_id}", status_code=204)
+def delete_session(
+ session_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> None:
+ """Delete a session and all its set logs."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ statement = select(KettlebellSetLog).where(KettlebellSetLog.session_id == session_id)
+ for log in session.exec(statement).all():
+ session.delete(log)
+ session.flush()
+ session.delete(kb_session)
+ session.commit()
+
+
+@router.patch("/{session_id}/complete", response_model=KettlebellSession)
+def complete_session(
+ session_id: int,
+ request: CompleteSessionRequest,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ """Mark a session as completed."""
+ kb_session = session.get(KettlebellSession, session_id)
+ if not kb_session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if kb_session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ kb_session.status = "completed"
+ kb_session.completed_at = datetime.utcnow()
+ if request.notes is not None:
+ kb_session.notes = request.notes
+ session.add(kb_session)
+ session.commit()
+ session.refresh(kb_session)
+ return kb_session
diff --git a/backend/app/api/v1/endpoints/nutrition.py b/backend/app/api/v1/endpoints/nutrition.py
index 61aa4da..26f3e5d 100644
--- a/backend/app/api/v1/endpoints/nutrition.py
+++ b/backend/app/api/v1/endpoints/nutrition.py
@@ -1,9 +1,11 @@
+from datetime import date as date_type
+from datetime import datetime
from typing import Any
import litellm
-from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
+from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from pydantic import BaseModel
-from sqlmodel import Session
+from sqlmodel import Session, select
from app.ai.nutrition import NutritionalInfo, analyze_nutrition_from_image, nutrition_module
from app.api import deps
@@ -16,9 +18,18 @@ class AnalyzeRequest(BaseModel):
description: str
+class FoodLogCreate(BaseModel):
+ name: str
+ calories: float
+ protein: float
+ carbs: float
+ fats: float
+
+
@router.post("/analyze", response_model=NutritionalInfo)
def analyze_food(
request: AnalyzeRequest,
+ current_user: deps.CurrentUser,
) -> Any:
"""
Analyze food description and return nutritional info using DSPy.
@@ -32,6 +43,7 @@ def analyze_food(
@router.post("/analyze/image", response_model=NutritionalInfo)
async def analyze_food_image(
+ current_user: deps.CurrentUser,
file: UploadFile = File(...),
description: str = Form(""),
) -> Any:
@@ -51,7 +63,7 @@ async def analyze_food_image(
def log_food(
*,
session: Session = Depends(deps.get_session),
- nutrition_info: NutritionalInfo,
+ nutrition_info: FoodLogCreate,
current_user: deps.CurrentUser,
) -> Any:
"""
@@ -81,8 +93,6 @@ def read_logs(
"""
Get food logs for current user.
"""
- from sqlmodel import select
-
statement = (
select(FoodLog)
.where(FoodLog.user_id == current_user.id)
@@ -91,3 +101,55 @@ def read_logs(
.limit(limit)
)
return session.exec(statement).all()
+
+
+class NutritionSummary(BaseModel):
+ date: str
+ total_calories: float
+ total_protein: float
+ total_carbs: float
+ total_fats: float
+ log_count: int
+ target_calories: float | None
+ target_protein: float | None
+ target_carbs: float | None
+ target_fat: float | None
+
+
+@router.get("/summary", response_model=NutritionSummary)
+def get_nutrition_summary(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+ date: str = Query(default=None, description="Date in YYYY-MM-DD format, defaults to today"),
+) -> Any:
+ """
+ Get aggregated macro totals for a given day.
+ """
+ if date:
+ target_date = datetime.strptime(date, "%Y-%m-%d").date()
+ else:
+ target_date = date_type.today()
+
+ start = datetime(target_date.year, target_date.month, target_date.day, 0, 0, 0)
+ end = datetime(target_date.year, target_date.month, target_date.day, 23, 59, 59)
+
+ statement = (
+ select(FoodLog)
+ .where(FoodLog.user_id == current_user.id)
+ .where(FoodLog.timestamp >= start)
+ .where(FoodLog.timestamp <= end)
+ )
+ logs = session.exec(statement).all()
+
+ return NutritionSummary(
+ date=target_date.isoformat(),
+ total_calories=sum(log.calories for log in logs),
+ total_protein=sum(log.protein for log in logs),
+ total_carbs=sum(log.carbs for log in logs),
+ total_fats=sum(log.fats for log in logs),
+ log_count=len(logs),
+ target_calories=getattr(current_user, "target_calories", None),
+ target_protein=getattr(current_user, "target_protein", None),
+ target_carbs=getattr(current_user, "target_carbs", None),
+ target_fat=getattr(current_user, "target_fat", None),
+ )
diff --git a/backend/app/api/v1/endpoints/push.py b/backend/app/api/v1/endpoints/push.py
new file mode 100644
index 0000000..6cf8020
--- /dev/null
+++ b/backend/app/api/v1/endpoints/push.py
@@ -0,0 +1,126 @@
+import json
+
+from fastapi import APIRouter, Depends, HTTPException
+from pydantic import BaseModel
+from pywebpush import WebPushException, webpush
+from sqlmodel import Session, select
+
+from app.api import deps
+from app.config import settings
+from app.models.push_subscription import PushSubscription
+
+router = APIRouter()
+
+
+class SubscriptionKeys(BaseModel):
+ p256dh: str
+ auth: str
+
+
+class SubscribeRequest(BaseModel):
+ endpoint: str
+ keys: SubscriptionKeys
+ reminder_hour: int = 9
+ reminder_minute: int = 0
+ timezone: str = "UTC"
+
+
+class UnsubscribeRequest(BaseModel):
+ endpoint: str
+
+
+def send_push(
+ subscriptions: list[PushSubscription],
+ title: str,
+ body: str,
+ url: str = "/supplements",
+ session: Session | None = None,
+) -> None:
+ payload = json.dumps({"title": title, "body": body, "url": url})
+ for sub in subscriptions:
+ try:
+ webpush(
+ subscription_info={
+ "endpoint": sub.endpoint,
+ "keys": {"p256dh": sub.p256dh, "auth": sub.auth},
+ },
+ data=payload,
+ vapid_private_key=settings.VAPID_PRIVATE_KEY,
+ vapid_claims={"sub": settings.VAPID_MAILTO},
+ )
+ except WebPushException as ex:
+ if ex.response and ex.response.status_code in (404, 410):
+ sub.is_active = False
+ if session:
+ session.add(sub)
+
+
+@router.get("/vapid-public-key")
+def get_vapid_public_key() -> dict:
+ return {"public_key": settings.VAPID_PUBLIC_KEY}
+
+
+@router.post("/subscribe", status_code=201)
+def subscribe(
+ data: SubscribeRequest,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> dict:
+ existing = session.exec(select(PushSubscription).where(PushSubscription.endpoint == data.endpoint)).first()
+ if existing:
+ existing.user_id = current_user.id
+ existing.p256dh = data.keys.p256dh
+ existing.auth = data.keys.auth
+ existing.reminder_hour = data.reminder_hour
+ existing.reminder_minute = data.reminder_minute
+ existing.timezone = data.timezone
+ existing.is_active = True
+ session.add(existing)
+ else:
+ sub = PushSubscription(
+ user_id=current_user.id,
+ endpoint=data.endpoint,
+ p256dh=data.keys.p256dh,
+ auth=data.keys.auth,
+ reminder_hour=data.reminder_hour,
+ reminder_minute=data.reminder_minute,
+ timezone=data.timezone,
+ )
+ session.add(sub)
+ session.commit()
+ return {"status": "subscribed"}
+
+
+@router.delete("/unsubscribe")
+def unsubscribe(
+ data: UnsubscribeRequest,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> dict:
+ sub = session.exec(
+ select(PushSubscription)
+ .where(PushSubscription.endpoint == data.endpoint)
+ .where(PushSubscription.user_id == current_user.id)
+ ).first()
+ if sub:
+ sub.is_active = False
+ session.add(sub)
+ session.commit()
+ return {"status": "unsubscribed"}
+
+
+@router.post("/test")
+def send_test_notification(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> dict:
+ subs = session.exec(
+ select(PushSubscription)
+ .where(PushSubscription.user_id == current_user.id)
+ .where(PushSubscription.is_active == True) # noqa: E712
+ ).all()
+ if not subs:
+ raise HTTPException(status_code=404, detail="No active subscriptions")
+ send_push(list(subs), title="Test Notification", body="Push notifications are working!", session=session)
+ session.commit()
+ return {"sent": len(subs)}
diff --git a/backend/app/api/v1/endpoints/supplements.py b/backend/app/api/v1/endpoints/supplements.py
new file mode 100644
index 0000000..4c260da
--- /dev/null
+++ b/backend/app/api/v1/endpoints/supplements.py
@@ -0,0 +1,238 @@
+from datetime import date, datetime
+from typing import Any, List, Optional
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from pydantic import BaseModel
+from sqlmodel import Session, select
+
+from app.api import deps
+from app.models.supplement import Supplement, SupplementLog
+
+router = APIRouter()
+
+
+class SupplementCreate(BaseModel):
+ name: str
+ dosage: float
+ unit: str
+ frequency: str = "daily"
+ scheduled_times: List[str] = []
+ notes: Optional[str] = None
+
+
+class SupplementUpdate(BaseModel):
+ name: Optional[str] = None
+ dosage: Optional[float] = None
+ unit: Optional[str] = None
+ frequency: Optional[str] = None
+ scheduled_times: Optional[List[str]] = None
+ notes: Optional[str] = None
+ is_active: Optional[bool] = None
+
+
+class SupplementLogCreate(BaseModel):
+ dose_taken: Optional[float] = None
+ notes: Optional[str] = None
+ taken_at: Optional[datetime] = None
+
+
+class SupplementWithStatus(BaseModel):
+ id: int
+ name: str
+ dosage: float
+ unit: str
+ frequency: str
+ scheduled_times: List[str]
+ notes: Optional[str]
+ is_active: bool
+ created_at: datetime
+ taken_today: bool
+ streak: int
+
+
+@router.get("/", response_model=List[Supplement])
+def list_supplements(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ statement = (
+ select(Supplement)
+ .where(Supplement.user_id == current_user.id)
+ .where(Supplement.is_active)
+ .order_by(Supplement.name)
+ )
+ return session.exec(statement).all()
+
+
+@router.post("/", response_model=Supplement)
+def create_supplement(
+ *,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+ data: SupplementCreate,
+) -> Any:
+ supplement = Supplement(
+ user_id=current_user.id,
+ name=data.name,
+ dosage=data.dosage,
+ unit=data.unit,
+ frequency=data.frequency,
+ scheduled_times=data.scheduled_times,
+ notes=data.notes,
+ )
+ session.add(supplement)
+ session.commit()
+ session.refresh(supplement)
+ return supplement
+
+
+@router.put("/{supplement_id}", response_model=Supplement)
+def update_supplement(
+ supplement_id: int,
+ data: SupplementUpdate,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ supplement = session.get(Supplement, supplement_id)
+ if not supplement:
+ raise HTTPException(status_code=404, detail="Supplement not found")
+ if supplement.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ update_data = data.model_dump(exclude_unset=True)
+ for key, value in update_data.items():
+ setattr(supplement, key, value)
+ session.add(supplement)
+ session.commit()
+ session.refresh(supplement)
+ return supplement
+
+
+@router.delete("/{supplement_id}", status_code=204)
+def delete_supplement(
+ supplement_id: int,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> None:
+ supplement = session.get(Supplement, supplement_id)
+ if not supplement:
+ raise HTTPException(status_code=404, detail="Supplement not found")
+ if supplement.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ # Soft delete
+ supplement.is_active = False
+ session.add(supplement)
+ session.commit()
+
+
+@router.post("/{supplement_id}/log", response_model=SupplementLog)
+def log_supplement(
+ supplement_id: int,
+ data: SupplementLogCreate,
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ supplement = session.get(Supplement, supplement_id)
+ if not supplement:
+ raise HTTPException(status_code=404, detail="Supplement not found")
+ if supplement.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ log = SupplementLog(
+ user_id=current_user.id,
+ supplement_id=supplement_id,
+ taken_at=data.taken_at or datetime.utcnow(),
+ dose_taken=data.dose_taken,
+ notes=data.notes,
+ )
+ session.add(log)
+ session.commit()
+ session.refresh(log)
+ return log
+
+
+@router.get("/logs", response_model=List[SupplementLog])
+def get_supplement_logs(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+ supplement_id: Optional[int] = Query(default=None),
+ start_date: Optional[str] = Query(default=None, description="YYYY-MM-DD"),
+ end_date: Optional[str] = Query(default=None, description="YYYY-MM-DD"),
+) -> Any:
+ statement = select(SupplementLog).where(SupplementLog.user_id == current_user.id)
+ if supplement_id:
+ statement = statement.where(SupplementLog.supplement_id == supplement_id)
+ if start_date:
+ dt = datetime.strptime(start_date, "%Y-%m-%d")
+ statement = statement.where(SupplementLog.taken_at >= dt)
+ if end_date:
+ dt = datetime.strptime(end_date, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
+ statement = statement.where(SupplementLog.taken_at <= dt)
+ statement = statement.order_by(SupplementLog.taken_at.desc())
+ return session.exec(statement).all()
+
+
+@router.get("/today", response_model=List[SupplementWithStatus])
+def get_today_supplements(
+ current_user: deps.CurrentUser,
+ session: Session = Depends(deps.get_session),
+) -> Any:
+ today = date.today()
+ start = datetime(today.year, today.month, today.day, 0, 0, 0)
+ end = datetime(today.year, today.month, today.day, 23, 59, 59)
+
+ supplements = session.exec(
+ select(Supplement)
+ .where(Supplement.user_id == current_user.id)
+ .where(Supplement.is_active)
+ .order_by(Supplement.name)
+ ).all()
+
+ today_logs = session.exec(
+ select(SupplementLog)
+ .where(SupplementLog.user_id == current_user.id)
+ .where(SupplementLog.taken_at >= start)
+ .where(SupplementLog.taken_at <= end)
+ ).all()
+
+ taken_ids = {log.supplement_id for log in today_logs}
+
+ result = []
+ for s in supplements:
+ # Calculate streak: consecutive days taken
+ streak = 0
+ check_date = today
+ while True:
+ d_start = datetime(check_date.year, check_date.month, check_date.day, 0, 0, 0)
+ d_end = datetime(check_date.year, check_date.month, check_date.day, 23, 59, 59)
+ taken = session.exec(
+ select(SupplementLog)
+ .where(SupplementLog.supplement_id == s.id)
+ .where(SupplementLog.taken_at >= d_start)
+ .where(SupplementLog.taken_at <= d_end)
+ ).first()
+ if taken:
+ streak += 1
+ from datetime import timedelta
+
+ check_date = check_date - timedelta(days=1)
+ else:
+ break
+ if streak > 365:
+ break
+
+ result.append(
+ SupplementWithStatus(
+ id=s.id,
+ name=s.name,
+ dosage=s.dosage,
+ unit=s.unit,
+ frequency=s.frequency,
+ scheduled_times=s.scheduled_times or [],
+ notes=s.notes,
+ is_active=s.is_active,
+ created_at=s.created_at,
+ taken_today=s.id in taken_ids,
+ streak=streak,
+ )
+ )
+
+ return result
diff --git a/backend/app/api/v1/endpoints/users.py b/backend/app/api/v1/endpoints/users.py
index 5d58219..d70339e 100644
--- a/backend/app/api/v1/endpoints/users.py
+++ b/backend/app/api/v1/endpoints/users.py
@@ -59,6 +59,8 @@ def update_user_me(
Update own user.
"""
user_data = user_in.model_dump(exclude_unset=True)
+ if "password" in user_data:
+ user_data["password_hash"] = security.get_password_hash(user_data.pop("password"))
current_user.sqlmodel_update(user_data)
session.add(current_user)
session.commit()
diff --git a/backend/app/models/calendar.py b/backend/app/models/calendar.py
new file mode 100644
index 0000000..e6feb30
--- /dev/null
+++ b/backend/app/models/calendar.py
@@ -0,0 +1,27 @@
+import datetime as dt
+from typing import Optional
+
+from sqlmodel import Field, SQLModel
+
+
+class DailyNote(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ user_id: int = Field(foreign_key="user.id", index=True)
+ date: dt.date = Field(index=True)
+ content: str = Field(default="", max_length=10000)
+ mood: Optional[str] = None # "great"/"good"/"okay"/"bad"/"awful"
+ energy_level: Optional[int] = None # 1–10
+ updated_at: dt.datetime = Field(default_factory=dt.datetime.utcnow)
+
+
+class CalendarEvent(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ user_id: int = Field(foreign_key="user.id", index=True)
+ date: dt.date = Field(index=True)
+ title: str
+ description: Optional[str] = None
+ event_type: str = "general" # "workout" | "supplement" | "general"
+ color: Optional[str] = None
+ start_time: Optional[str] = None # "HH:MM"
+ is_completed: bool = False
+ created_at: dt.datetime = Field(default_factory=dt.datetime.utcnow)
diff --git a/backend/app/models/kettlebell.py b/backend/app/models/kettlebell.py
new file mode 100644
index 0000000..a0de47c
--- /dev/null
+++ b/backend/app/models/kettlebell.py
@@ -0,0 +1,44 @@
+from datetime import datetime
+from typing import Optional
+
+from pydantic import BaseModel
+from sqlmodel import JSON, Field, SQLModel
+
+
+class ExerciseBlock(BaseModel):
+ order: int
+ name: str
+ description: str
+ sets: int
+ reps: int
+ duration_seconds: int # reps=0 → timed, duration_seconds=0 → rep-based
+ weight_kg: float
+ rest_seconds: int
+ coaching_tip: str
+
+
+class KettlebellSession(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ user_id: int = Field(foreign_key="user.id", index=True)
+ title: str
+ focus: str # e.g. "strength", "conditioning"
+ exercises: dict = Field(default={}, sa_type=JSON) # full AI-prescribed exercise list
+ total_duration_min: int
+ difficulty: str
+ notes: str = Field(default="")
+ status: str = Field(default="generated") # "generated" | "in_progress" | "completed" | "abandoned"
+ started_at: Optional[datetime] = Field(default=None)
+ completed_at: Optional[datetime] = Field(default=None)
+ created_at: datetime = Field(default_factory=datetime.utcnow)
+
+
+class KettlebellSetLog(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ session_id: int = Field(foreign_key="kettlebellsession.id", index=True)
+ exercise_order: int
+ set_number: int
+ actual_reps: int
+ actual_weight_kg: float
+ actual_duration_seconds: int
+ perceived_effort: int # 1–10 RPE
+ completed_at: datetime = Field(default_factory=datetime.utcnow)
diff --git a/backend/app/models/push_subscription.py b/backend/app/models/push_subscription.py
new file mode 100644
index 0000000..febeef2
--- /dev/null
+++ b/backend/app/models/push_subscription.py
@@ -0,0 +1,17 @@
+from datetime import datetime
+from typing import Optional
+
+from sqlmodel import Field, SQLModel
+
+
+class PushSubscription(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ user_id: int = Field(foreign_key="user.id", index=True)
+ endpoint: str = Field(unique=True, index=True)
+ p256dh: str
+ auth: str
+ reminder_hour: int = Field(default=9)
+ reminder_minute: int = Field(default=0)
+ timezone: str = Field(default="UTC")
+ is_active: bool = Field(default=True)
+ created_at: datetime = Field(default_factory=datetime.utcnow)
diff --git a/backend/app/models/supplement.py b/backend/app/models/supplement.py
new file mode 100644
index 0000000..c982393
--- /dev/null
+++ b/backend/app/models/supplement.py
@@ -0,0 +1,27 @@
+from datetime import datetime
+from typing import List, Optional
+
+from sqlalchemy import Column
+from sqlmodel import JSON, Field, SQLModel
+
+
+class Supplement(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ user_id: int = Field(foreign_key="user.id", index=True)
+ name: str = Field(index=True)
+ dosage: float
+ unit: str # mg / mcg / IU / g
+ frequency: str = Field(default="daily") # daily / weekly / as_needed
+ scheduled_times: List[str] = Field(default=[], sa_column=Column(JSON)) # ["08:00", "20:00"]
+ notes: Optional[str] = None
+ is_active: bool = Field(default=True)
+ created_at: datetime = Field(default_factory=datetime.utcnow)
+
+
+class SupplementLog(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ user_id: int = Field(foreign_key="user.id", index=True)
+ supplement_id: int = Field(foreign_key="supplement.id", index=True)
+ taken_at: datetime = Field(default_factory=datetime.utcnow)
+ dose_taken: Optional[float] = None
+ notes: Optional[str] = None
diff --git a/backend/app/models/user.py b/backend/app/models/user.py
index 2205236..7c47550 100644
--- a/backend/app/models/user.py
+++ b/backend/app/models/user.py
@@ -19,4 +19,10 @@ class User(SQLModel, table=True):
weight: Optional[float] = None
unit_preference: str = Field(default="metric") # "metric" or "imperial"
+ # Nutrition Targets
+ target_calories: Optional[float] = None
+ target_protein: Optional[float] = None
+ target_carbs: Optional[float] = None
+ target_fat: Optional[float] = None
+
created_at: datetime = Field(default_factory=datetime.utcnow)
diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py
new file mode 100644
index 0000000..fdb2526
--- /dev/null
+++ b/backend/app/scheduler.py
@@ -0,0 +1,47 @@
+from datetime import datetime, timezone
+
+import pytz
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.cron import CronTrigger
+from sqlmodel import Session, select
+
+from app.db import engine
+from app.models.push_subscription import PushSubscription
+
+scheduler = BackgroundScheduler(timezone="UTC")
+
+
+def send_supplement_reminders() -> None:
+ from app.api.v1.endpoints.push import send_push
+
+ now_utc = datetime.now(timezone.utc)
+ with Session(engine) as session:
+ subs = session.exec(
+ select(PushSubscription).where(PushSubscription.is_active == True) # noqa: E712
+ ).all()
+ due = []
+ for sub in subs:
+ try:
+ tz = pytz.timezone(sub.timezone)
+ local_now = now_utc.astimezone(tz)
+ if local_now.hour == sub.reminder_hour and local_now.minute == sub.reminder_minute:
+ due.append(sub)
+ except Exception:
+ pass
+ if due:
+ send_push(due, title="Supplement Reminder", body="Time to log your supplements for today!", session=session)
+ session.commit()
+
+
+def start_scheduler() -> None:
+ scheduler.add_job(
+ send_supplement_reminders,
+ CronTrigger(minute="*"),
+ id="supplement_reminders",
+ replace_existing=True,
+ )
+ scheduler.start()
+
+
+def stop_scheduler() -> None:
+ scheduler.shutdown(wait=False)
diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py
index b8f1ddf..5e9ba39 100644
--- a/backend/app/schemas/user.py
+++ b/backend/app/schemas/user.py
@@ -21,6 +21,10 @@ class UserRead(UserBase):
height: Optional[float] = None
weight: Optional[float] = None
unit_preference: str = "metric"
+ target_calories: Optional[float] = None
+ target_protein: Optional[float] = None
+ target_carbs: Optional[float] = None
+ target_fat: Optional[float] = None
class UserUpdate(SQLModel):
@@ -34,3 +38,7 @@ class UserUpdate(SQLModel):
height: Optional[float] = None
weight: Optional[float] = None
unit_preference: Optional[str] = None
+ target_calories: Optional[float] = None
+ target_protein: Optional[float] = None
+ target_carbs: Optional[float] = None
+ target_fat: Optional[float] = None
diff --git a/backend/requirements.txt b/backend/requirements.txt
index ff1cb0c..ce7b04e 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -13,4 +13,7 @@ pytest
httpx
python-dotenv
ruff
+pywebpush
+apscheduler
+pytz
diff --git a/frontend/dev-dist/registerSW.js b/frontend/dev-dist/registerSW.js
new file mode 100644
index 0000000..1d5625f
--- /dev/null
+++ b/frontend/dev-dist/registerSW.js
@@ -0,0 +1 @@
+if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
index 072a57e..0717174 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,9 @@
-
frontend
+
+
+ HealthyFit
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e42914b..7765dad 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -12,10 +12,12 @@
"lucide-react": "^0.562.0",
"motion": "^12.27.0",
"react": "^19.2.0",
+ "react-day-picker": "^9.14.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.12.0",
- "recharts": "^3.6.0"
+ "recharts": "^3.6.0",
+ "sonner": "^2.0.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -33,13 +35,14 @@
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
- "vite": "^7.2.4"
+ "vite": "^7.2.4",
+ "vite-plugin-pwa": "^1.2.0"
}
},
"node_modules/@babel/code-frame": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
- "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -52,9 +55,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
- "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -93,14 +96,14 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
- "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+ "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==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "@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"
@@ -109,6 +112,19 @@
"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==",
+ "dev": true,
+ "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",
@@ -126,6 +142,63 @@
"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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
+ "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
+ "dev": true,
+ "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",
@@ -136,6 +209,20 @@
"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==",
+ "dev": true,
+ "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",
@@ -168,6 +255,79 @@
"@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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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",
@@ -198,6 +358,21 @@
"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==",
+ "dev": true,
+ "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",
@@ -213,13 +388,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
- "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.6"
+ "@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -228,6 +403,1118 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
+ "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
+ "dev": true,
+ "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"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
+ "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
+ "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
+ "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
+ "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-scoped-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
+ "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-dotall-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
+ "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
+ "dev": true,
+ "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-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
+ "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
+ "dev": true,
+ "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-duplicate-named-capturing-groups-regex": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz",
+ "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==",
+ "dev": true,
+ "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-dynamic-import": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
+ "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
+ "dev": true,
+ "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-explicit-resource-management": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
+ "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
+ "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-json-strings": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
+ "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-member-expression-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
+ "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
+ "dev": true,
+ "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-modules-amd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
+ "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^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-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==",
+ "dev": true,
+ "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-modules-systemjs": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
+ "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
+ "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^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-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==",
+ "dev": true,
+ "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-new-target": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
+ "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-object-super": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
+ "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1"
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-property-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
+ "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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-regexp-modifiers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
+ "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
+ "dev": true,
+ "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-reserved-words": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
+ "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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-typeof-symbol": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
+ "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
+ "dev": true,
+ "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-unicode-escapes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
+ "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
+ "dev": true,
+ "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-unicode-property-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
+ "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
+ "dev": true,
+ "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-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==",
+ "dev": true,
+ "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/plugin-transform-unicode-sets-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
+ "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
+ "dev": true,
+ "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/preset-env": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz",
+ "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.28.6",
+ "@babel/plugin-syntax-import-attributes": "^7.28.6",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-async-generator-functions": "^7.29.0",
+ "@babel/plugin-transform-async-to-generator": "^7.28.6",
+ "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
+ "@babel/plugin-transform-block-scoping": "^7.28.6",
+ "@babel/plugin-transform-class-properties": "^7.28.6",
+ "@babel/plugin-transform-class-static-block": "^7.28.6",
+ "@babel/plugin-transform-classes": "^7.28.6",
+ "@babel/plugin-transform-computed-properties": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5",
+ "@babel/plugin-transform-dotall-regex": "^7.28.6",
+ "@babel/plugin-transform-duplicate-keys": "^7.27.1",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0",
+ "@babel/plugin-transform-dynamic-import": "^7.27.1",
+ "@babel/plugin-transform-explicit-resource-management": "^7.28.6",
+ "@babel/plugin-transform-exponentiation-operator": "^7.28.6",
+ "@babel/plugin-transform-export-namespace-from": "^7.27.1",
+ "@babel/plugin-transform-for-of": "^7.27.1",
+ "@babel/plugin-transform-function-name": "^7.27.1",
+ "@babel/plugin-transform-json-strings": "^7.28.6",
+ "@babel/plugin-transform-literals": "^7.27.1",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.28.6",
+ "@babel/plugin-transform-member-expression-literals": "^7.27.1",
+ "@babel/plugin-transform-modules-amd": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.28.6",
+ "@babel/plugin-transform-modules-systemjs": "^7.29.0",
+ "@babel/plugin-transform-modules-umd": "^7.27.1",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
+ "@babel/plugin-transform-new-target": "^7.27.1",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
+ "@babel/plugin-transform-numeric-separator": "^7.28.6",
+ "@babel/plugin-transform-object-rest-spread": "^7.28.6",
+ "@babel/plugin-transform-object-super": "^7.27.1",
+ "@babel/plugin-transform-optional-catch-binding": "^7.28.6",
+ "@babel/plugin-transform-optional-chaining": "^7.28.6",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/plugin-transform-private-methods": "^7.28.6",
+ "@babel/plugin-transform-private-property-in-object": "^7.28.6",
+ "@babel/plugin-transform-property-literals": "^7.27.1",
+ "@babel/plugin-transform-regenerator": "^7.29.0",
+ "@babel/plugin-transform-regexp-modifiers": "^7.28.6",
+ "@babel/plugin-transform-reserved-words": "^7.27.1",
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
+ "@babel/plugin-transform-spread": "^7.28.6",
+ "@babel/plugin-transform-sticky-regex": "^7.27.1",
+ "@babel/plugin-transform-template-literals": "^7.27.1",
+ "@babel/plugin-transform-typeof-symbol": "^7.27.1",
+ "@babel/plugin-transform-unicode-escapes": "^7.27.1",
+ "@babel/plugin-transform-unicode-property-regex": "^7.28.6",
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.15",
+ "babel-plugin-polyfill-corejs3": "^0.14.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.6",
+ "core-js-compat": "^3.48.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "dev": true,
+ "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",
@@ -244,18 +1531,18 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
- "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/generator": "^7.28.6",
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.6",
+ "@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
@@ -263,9 +1550,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
- "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -276,6 +1563,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@date-fns/tz": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
+ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
+ "license": "MIT"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
@@ -1006,6 +2299,16 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "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==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1038,6 +2341,17 @@
"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==",
+ "dev": true,
+ "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",
@@ -1203,6 +2517,77 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@rollup/plugin-node-resolve": {
+ "version": "15.3.1",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
+ "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "@types/resolve": "1.20.2",
+ "deepmerge": "^4.2.2",
+ "is-module": "^1.0.0",
+ "resolve": "^1.22.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.78.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-terser": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
+ "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "serialize-javascript": "^6.0.1",
+ "smob": "^1.0.0",
+ "terser": "^5.17.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+ "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
@@ -1565,6 +2950,29 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
+ "node_modules/@surma/rollup-plugin-off-main-thread": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
+ "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ejs": "^3.1.6",
+ "json5": "^2.2.0",
+ "magic-string": "^0.25.0",
+ "string.prototype.matchall": "^4.0.6"
+ }
+ },
+ "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
"node_modules/@swc/core": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.8.tgz",
@@ -1801,6 +3209,15 @@
"@swc/counter": "^0.1.3"
}
},
+ "node_modules/@tabby_ai/hijri-converter": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz",
+ "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/@tailwindcss/node": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
@@ -2249,6 +3666,20 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/resolve": {
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
+ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2347,12 +3778,94 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/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/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "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/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
@@ -2364,6 +3877,48 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.17",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
+ "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
+ "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "core-js-compat": "^3.48.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
+ "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -2436,6 +3991,32 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "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",
@@ -2449,6 +4030,23 @@
"node": ">= 0.4"
}
},
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2598,6 +4196,23 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/common-tags": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2625,6 +4240,20 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/core-js-compat": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
+ "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2640,6 +4269,16 @@
"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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2767,6 +4406,76 @@
"node": ">=12"
}
},
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/date-fns-jalali": {
+ "version": "4.1.0-0",
+ "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
+ "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2810,6 +4519,52 @@
"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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "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-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "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/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2865,6 +4620,22 @@
"node": ">= 0.4"
}
},
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
@@ -2886,6 +4657,75 @@
"node": ">=10.13.0"
}
},
+ "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",
@@ -2931,6 +4771,24 @@
"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/es-toolkit": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
@@ -3190,6 +5048,13 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -3233,6 +5098,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3246,6 +5128,39 @@
"node": ">=16.0.0"
}
},
+ "node_modules/filelist": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
+ "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/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/filelist/node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3304,6 +5219,39 @@
}
}
},
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
@@ -3347,6 +5295,22 @@
}
}
},
+ "node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3371,6 +5335,47 @@
"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==",
+ "dev": true,
+ "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",
@@ -3405,6 +5410,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-own-enumerable-property-symbols": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
+ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -3418,6 +5430,49 @@
"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/glob": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
+ "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "foreground-child": "^3.3.1",
+ "jackspeak": "^4.1.1",
+ "minimatch": "^10.1.1",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "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",
@@ -3431,6 +5486,45 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/globals": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
@@ -3444,6 +5538,23 @@
"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",
@@ -3463,6 +5574,19 @@
"dev": true,
"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",
@@ -3473,6 +5597,35 @@
"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==",
+ "dev": true,
+ "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",
@@ -3579,6 +5732,13 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3632,6 +5792,21 @@
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
"license": "MIT"
},
+ "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/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -3665,6 +5840,141 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "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-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-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "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-decimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
@@ -3685,6 +5995,42 @@
"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-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-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",
@@ -3708,6 +6054,66 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "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-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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-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-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+ "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -3720,6 +6126,181 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "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-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+ "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "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/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",
@@ -3727,6 +6308,40 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/jackspeak": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
+ "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^9.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -3777,6 +6392,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "dev": true,
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3804,6 +6426,29 @@
"node": ">=6"
}
},
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonpointer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
+ "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3814,6 +6459,16 @@
"json-buffer": "3.0.1"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -4105,6 +6760,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4112,6 +6781,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -4789,6 +7465,16 @@
"node": "*"
}
},
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/motion": {
"version": "12.27.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.27.0.tgz",
@@ -4869,6 +7555,50 @@
"dev": true,
"license": "MIT"
},
+ "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-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "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==",
+ "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",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -4887,6 +7617,24 @@
"node": ">= 0.8.0"
}
},
+ "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",
@@ -4919,6 +7667,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4977,6 +7732,40 @@
"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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/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/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -4984,6 +7773,29 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -5023,6 +7835,19 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/pretty-bytes": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
+ "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@@ -5049,6 +7874,16 @@
"node": ">=6"
}
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/react": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
@@ -5058,6 +7893,28 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-day-picker": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz",
+ "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@date-fns/tz": "^1.4.1",
+ "@tabby_ai/hijri-converter": "1.0.5",
+ "date-fns": "^4.1.0",
+ "date-fns-jalali": "4.1.0-0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/gpbl"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
@@ -5210,6 +8067,108 @@
"redux": "^5.0.0"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
+ "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.1.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -5243,12 +8202,43 @@
"url": "https://opencollective.com/unified"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "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": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -5304,6 +8294,82 @@
"fsevents": "~2.3.2"
}
},
+ "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==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/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==",
+ "dev": true,
+ "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/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -5320,12 +8386,71 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"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==",
+ "dev": true,
+ "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/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -5349,6 +8474,129 @@
"node": ">=8"
}
},
+ "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/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/smob": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz",
+ "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/sonner": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
+ "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.8.0-beta.0",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
+ "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
+ "deprecated": "The work that was done in this beta branch won't be included in future versions",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "whatwg-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5359,6 +8607,35 @@
"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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "deprecated": "Please use @jridgewell/sourcemap-codec instead",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
@@ -5369,6 +8646,107 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "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/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.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/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -5383,6 +8761,31 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/stringify-object": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
+ "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "get-own-enumerable-property-symbols": "^3.0.0",
+ "is-obj": "^1.0.1",
+ "is-regexp": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
+ "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5427,6 +8830,19 @@
"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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/tabbable": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
@@ -5466,6 +8882,54 @@
"url": "https://opencollective.com/webpack"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tempy": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz",
+ "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-stream": "^2.0.0",
+ "temp-dir": "^2.0.0",
+ "type-fest": "^0.16.0",
+ "unique-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.46.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
+ "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
+ "dev": true,
+ "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/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -5507,17 +8971,14 @@
}
}
},
- "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==",
+ "node_modules/tr46": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
+ "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "dependencies": {
+ "punycode": "^2.1.0"
}
},
"node_modules/trim-lines": {
@@ -5559,6 +9020,97 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-fest": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
+ "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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",
@@ -5573,6 +9125,25 @@
"node": ">=14.17"
}
},
+ "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-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -5580,6 +9151,50 @@
"dev": true,
"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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -5599,6 +9214,19 @@
"url": "https://opencollective.com/unified"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "crypto-random-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
@@ -5667,6 +9295,27 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4",
+ "yarn": "*"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -5842,6 +9491,37 @@
}
}
},
+ "node_modules/vite-plugin-pwa": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz",
+ "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.6",
+ "pretty-bytes": "^6.1.1",
+ "tinyglobby": "^0.2.10",
+ "workbox-build": "^7.4.0",
+ "workbox-window": "^7.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vite-pwa/assets-generator": "^1.0.0",
+ "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
+ "workbox-build": "^7.4.0",
+ "workbox-window": "^7.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@vite-pwa/assets-generator": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -5860,17 +9540,23 @@
}
}
},
- "node_modules/vite/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "node_modules/webidl-conversions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
+ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
+ "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "dependencies": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
}
},
"node_modules/which": {
@@ -5889,6 +9575,95 @@
"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==",
+ "dev": true,
+ "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/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -5899,6 +9674,377 @@
"node": ">=0.10.0"
}
},
+ "node_modules/workbox-background-sync": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz",
+ "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "idb": "^7.0.1",
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-broadcast-update": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz",
+ "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-build": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz",
+ "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@apideck/better-ajv-errors": "^0.3.1",
+ "@babel/core": "^7.24.4",
+ "@babel/preset-env": "^7.11.0",
+ "@babel/runtime": "^7.11.2",
+ "@rollup/plugin-babel": "^5.2.0",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-replace": "^2.4.1",
+ "@rollup/plugin-terser": "^0.4.3",
+ "@surma/rollup-plugin-off-main-thread": "^2.2.3",
+ "ajv": "^8.6.0",
+ "common-tags": "^1.8.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "fs-extra": "^9.0.1",
+ "glob": "^11.0.1",
+ "lodash": "^4.17.20",
+ "pretty-bytes": "^5.3.0",
+ "rollup": "^2.79.2",
+ "source-map": "^0.8.0-beta.0",
+ "stringify-object": "^3.3.0",
+ "strip-comments": "^2.0.1",
+ "tempy": "^0.6.0",
+ "upath": "^1.2.0",
+ "workbox-background-sync": "7.4.0",
+ "workbox-broadcast-update": "7.4.0",
+ "workbox-cacheable-response": "7.4.0",
+ "workbox-core": "7.4.0",
+ "workbox-expiration": "7.4.0",
+ "workbox-google-analytics": "7.4.0",
+ "workbox-navigation-preload": "7.4.0",
+ "workbox-precaching": "7.4.0",
+ "workbox-range-requests": "7.4.0",
+ "workbox-recipes": "7.4.0",
+ "workbox-routing": "7.4.0",
+ "workbox-strategies": "7.4.0",
+ "workbox-streams": "7.4.0",
+ "workbox-sw": "7.4.0",
+ "workbox-window": "7.4.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz",
+ "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-schema": "^0.4.0",
+ "jsonpointer": "^5.0.0",
+ "leven": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "ajv": ">=8"
+ }
+ },
+ "node_modules/workbox-build/node_modules/@rollup/plugin-babel": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
+ "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.10.4",
+ "@rollup/pluginutils": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0",
+ "@types/babel__core": "^7.1.9",
+ "rollup": "^1.20.0||^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/babel__core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/workbox-build/node_modules/@rollup/plugin-replace": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz",
+ "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^3.1.0",
+ "magic-string": "^0.25.7"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0 || ^2.0.0"
+ }
+ },
+ "node_modules/workbox-build/node_modules/@rollup/pluginutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
+ "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "0.0.39",
+ "estree-walker": "^1.0.1",
+ "picomatch": "^2.2.2"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0"
+ }
+ },
+ "node_modules/workbox-build/node_modules/@types/estree": {
+ "version": "0.0.39",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
+ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/workbox-build/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/workbox-build/node_modules/estree-walker": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
+ "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/workbox-build/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/workbox-build/node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
+ "node_modules/workbox-build/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/workbox-build/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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/workbox-build/node_modules/rollup": {
+ "version": "2.80.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
+ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/workbox-cacheable-response": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz",
+ "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-core": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz",
+ "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/workbox-expiration": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz",
+ "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "idb": "^7.0.1",
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-google-analytics": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz",
+ "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-background-sync": "7.4.0",
+ "workbox-core": "7.4.0",
+ "workbox-routing": "7.4.0",
+ "workbox-strategies": "7.4.0"
+ }
+ },
+ "node_modules/workbox-navigation-preload": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz",
+ "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-precaching": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz",
+ "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0",
+ "workbox-routing": "7.4.0",
+ "workbox-strategies": "7.4.0"
+ }
+ },
+ "node_modules/workbox-range-requests": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz",
+ "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-recipes": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz",
+ "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-cacheable-response": "7.4.0",
+ "workbox-core": "7.4.0",
+ "workbox-expiration": "7.4.0",
+ "workbox-precaching": "7.4.0",
+ "workbox-routing": "7.4.0",
+ "workbox-strategies": "7.4.0"
+ }
+ },
+ "node_modules/workbox-routing": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz",
+ "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-strategies": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz",
+ "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0"
+ }
+ },
+ "node_modules/workbox-streams": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz",
+ "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.0",
+ "workbox-routing": "7.4.0"
+ }
+ },
+ "node_modules/workbox-sw": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz",
+ "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/workbox-window": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz",
+ "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2",
+ "workbox-core": "7.4.0"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 13be776..98f9173 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,10 +14,12 @@
"lucide-react": "^0.562.0",
"motion": "^12.27.0",
"react": "^19.2.0",
+ "react-day-picker": "^9.14.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.12.0",
- "recharts": "^3.6.0"
+ "recharts": "^3.6.0",
+ "sonner": "^2.0.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -35,6 +37,7 @@
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
- "vite": "^7.2.4"
+ "vite": "^7.2.4",
+ "vite-plugin-pwa": "^1.2.0"
}
}
diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png
new file mode 100644
index 0000000..2f87b99
Binary files /dev/null and b/frontend/public/icons/icon-192.png differ
diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png
new file mode 100644
index 0000000..9f94456
Binary files /dev/null and b/frontend/public/icons/icon-512.png differ
diff --git a/frontend/public/push-sw.js b/frontend/public/push-sw.js
new file mode 100644
index 0000000..12e90e9
--- /dev/null
+++ b/frontend/public/push-sw.js
@@ -0,0 +1,39 @@
+self.addEventListener('push', (event) => {
+ console.log('[SW] push received', event.data?.text());
+ if (!event.data) return;
+
+ let title = 'HealthyFit';
+ let body = 'You have a new notification';
+ let url = '/';
+ try {
+ const data = event.data.json();
+ title = data.title;
+ body = data.body;
+ url = data.url ?? '/';
+ } catch {
+ body = event.data.text();
+ }
+
+ event.waitUntil(
+ self.registration.showNotification(title, {
+ body,
+ icon: '/icons/icon-192.png',
+ badge: '/icons/icon-192.png',
+ data: { url },
+ })
+ );
+});
+
+self.addEventListener('notificationclick', (event) => {
+ event.notification.close();
+ const url = event.notification.data?.url ?? '/';
+ event.waitUntil(
+ self.clients
+ .matchAll({ type: 'window', includeUncontrolled: true })
+ .then((clients) => {
+ const existing = clients.find((c) => c.url.includes(url));
+ if (existing) return existing.focus();
+ return self.clients.openWindow(url);
+ })
+ );
+});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fb612e7..631ce42 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,12 +1,18 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
+import { Toaster } from 'sonner';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Nutrition from './pages/Nutrition';
import Health from './pages/Health';
import Plans from './pages/Plans';
import Profile from './pages/Profile';
+import Kettlebell from './pages/Kettlebell';
+import ActiveSession from './pages/ActiveSession';
+import Supplements from './pages/Supplements';
+import KettlebellAnalytics from './pages/KettlebellAnalytics';
+import CalendarPage from './pages/Calendar';
import ProtectedRoute from './components/ProtectedRoute';
import AppLayout from './components/Layout/AppLayout';
@@ -15,6 +21,7 @@ function App() {
+
} />
@@ -55,6 +62,42 @@ function App() {
} />
+
+
+
+
+
+ } />
+
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+
+
+
+
+ } />
+
+ {/* Full-screen active session — no AppLayout/sidebar */}
+
+
+
+ } />
diff --git a/frontend/src/api/calendar.ts b/frontend/src/api/calendar.ts
new file mode 100644
index 0000000..c4e5f37
--- /dev/null
+++ b/frontend/src/api/calendar.ts
@@ -0,0 +1,26 @@
+import client from './client';
+import type { DayMeta, DailyNote, CalendarEvent, DayDetail } from '../types/calendar';
+
+export const getMonthSummary = (year: number, month: number) =>
+ client.get('/calendar/month', { params: { year, month } }).then(r => r.data);
+
+export const getDayDetail = (date: string) =>
+ client.get('/calendar/day', { params: { date } }).then(r => r.data);
+
+export const getNote = (date: string) =>
+ client.get(`/calendar/notes/${date}`).then(r => r.data);
+
+export const upsertNote = (date: string, data: { content: string; mood?: string; energy_level?: number }) =>
+ client.put(`/calendar/notes/${date}`, data).then(r => r.data);
+
+export const getEvents = (start: string, end: string) =>
+ client.get('/calendar/events', { params: { start, end } }).then(r => r.data);
+
+export const createEvent = (data: Omit) =>
+ client.post('/calendar/events', data).then(r => r.data);
+
+export const updateEvent = (id: number, data: Partial>) =>
+ client.put(`/calendar/events/${id}`, data).then(r => r.data);
+
+export const deleteEvent = (id: number) =>
+ client.delete(`/calendar/events/${id}`);
diff --git a/frontend/src/api/health.ts b/frontend/src/api/health.ts
new file mode 100644
index 0000000..99f1fd3
--- /dev/null
+++ b/frontend/src/api/health.ts
@@ -0,0 +1,14 @@
+import client from './client';
+import type { HealthGoal, HealthGoalCreate, HealthMetric, HealthMetricCreate } from '../types/health';
+
+export const getMetrics = (limit = 100) =>
+ client.get('/health/metrics', { params: { limit } }).then(r => r.data);
+
+export const createMetric = (data: HealthMetricCreate) =>
+ client.post('/health/metrics', data).then(r => r.data);
+
+export const getGoals = () =>
+ client.get('/health/goals').then(r => r.data);
+
+export const createGoal = (data: HealthGoalCreate) =>
+ client.post('/health/goals', data).then(r => r.data);
diff --git a/frontend/src/api/kettlebell.ts b/frontend/src/api/kettlebell.ts
new file mode 100644
index 0000000..7f580a3
--- /dev/null
+++ b/frontend/src/api/kettlebell.ts
@@ -0,0 +1,51 @@
+import client from './client';
+import type { KettlebellSession, KettlebellSetLog } from '../types/kettlebell';
+
+export interface GenerateSessionRequest {
+ focus: string;
+ duration_minutes: number;
+ available_weights: number[];
+}
+
+export interface LogSetRequest {
+ exercise_order: number;
+ set_number: number;
+ actual_reps: number;
+ actual_weight_kg: number;
+ actual_duration_seconds: number;
+ perceived_effort: number;
+}
+
+export interface CompleteSessionRequest {
+ notes?: string;
+}
+
+export const generateSession = (data: GenerateSessionRequest) =>
+ client.post('/kettlebell/generate', data).then(r => r.data);
+
+export const getSessions = () =>
+ client.get('/kettlebell/').then(r => r.data);
+
+export const getSession = (id: number) =>
+ client.get(`/kettlebell/${id}`).then(r => r.data);
+
+export const startSession = (id: number) =>
+ client.patch(`/kettlebell/${id}/start`).then(r => r.data);
+
+export const logSet = (id: number, data: LogSetRequest) =>
+ client.post(`/kettlebell/${id}/sets`, data).then(r => r.data);
+
+export const getSets = (id: number) =>
+ client.get(`/kettlebell/${id}/sets`).then(r => r.data);
+
+export const completeSession = (id: number, data: CompleteSessionRequest = {}) =>
+ client.patch(`/kettlebell/${id}/complete`, data).then(r => r.data);
+
+export const retrySession = (id: number) =>
+ client.post(`/kettlebell/${id}/retry`).then(r => r.data);
+
+export const abandonSession = (id: number) =>
+ client.patch(`/kettlebell/${id}/abandon`).then(r => r.data);
+
+export const deleteSession = (id: number) =>
+ client.delete(`/kettlebell/${id}`);
diff --git a/frontend/src/api/nutrition.ts b/frontend/src/api/nutrition.ts
new file mode 100644
index 0000000..4e02865
--- /dev/null
+++ b/frontend/src/api/nutrition.ts
@@ -0,0 +1,27 @@
+import client from './client';
+import type { FoodLog, FoodLogCreate, NutritionalInfo, NutritionSummary } from '../types/nutrition';
+
+export const analyzeFoodText = (description: string) =>
+ client.post('/nutrition/analyze', { description }).then(r => r.data);
+
+export const analyzeFoodImage = (file: File, description = '') => {
+ const formData = new FormData();
+ formData.append('file', file);
+ if (description) formData.append('description', description);
+ return client
+ .post('/nutrition/analyze/image', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ .then(r => r.data);
+};
+
+export const logFood = (data: FoodLogCreate) =>
+ client.post('/nutrition/log', data).then(r => r.data);
+
+export const getLogs = (skip = 0, limit = 100) =>
+ client.get('/nutrition/logs', { params: { skip, limit } }).then(r => r.data);
+
+export const getNutritionSummary = (date?: string) =>
+ client
+ .get('/nutrition/summary', { params: date ? { date } : undefined })
+ .then(r => r.data);
diff --git a/frontend/src/api/plans.ts b/frontend/src/api/plans.ts
new file mode 100644
index 0000000..8dc09b8
--- /dev/null
+++ b/frontend/src/api/plans.ts
@@ -0,0 +1,8 @@
+import client from './client';
+import type { Plan, PlanRequest } from '../types/plans';
+
+export const getPlans = () =>
+ client.get('/plans/').then(r => r.data);
+
+export const generatePlan = (data: PlanRequest) =>
+ client.post('/plans/generate', data).then(r => r.data);
diff --git a/frontend/src/api/push.ts b/frontend/src/api/push.ts
new file mode 100644
index 0000000..4880a9a
--- /dev/null
+++ b/frontend/src/api/push.ts
@@ -0,0 +1,32 @@
+import client from './client';
+
+export interface PushSubscribeRequest {
+ endpoint: string;
+ keys: { p256dh: string; auth: string };
+ reminder_hour: number;
+ reminder_minute: number;
+ timezone: string;
+}
+
+export const getVapidPublicKey = (): Promise =>
+ client.get<{ public_key: string }>('/push/vapid-public-key').then((r) => r.data.public_key);
+
+export const subscribePush = (data: PushSubscribeRequest): Promise =>
+ client.post('/push/subscribe', data).then((r) => r.data);
+
+export const unsubscribePush = (endpoint: string): Promise =>
+ client.delete('/push/unsubscribe', { data: { endpoint } }).then((r) => r.data);
+
+export const sendTestNotification = (): Promise<{ sent: number }> =>
+ client.post('/push/test').then((r) => r.data);
+
+export function urlBase64ToUint8Array(base64String: string): Uint8Array {
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
+ const rawData = atob(base64);
+ const output = new Uint8Array(rawData.length);
+ for (let i = 0; i < rawData.length; i++) {
+ output[i] = rawData.charCodeAt(i);
+ }
+ return output;
+}
diff --git a/frontend/src/api/supplements.ts b/frontend/src/api/supplements.ts
new file mode 100644
index 0000000..9398240
--- /dev/null
+++ b/frontend/src/api/supplements.ts
@@ -0,0 +1,32 @@
+import client from './client';
+import type {
+ Supplement,
+ SupplementCreate,
+ SupplementLog,
+ SupplementUpdate,
+ SupplementWithStatus,
+} from '../types/supplement';
+
+export const getSupplements = () =>
+ client.get('/supplements/').then(r => r.data);
+
+export const createSupplement = (data: SupplementCreate) =>
+ client.post('/supplements/', data).then(r => r.data);
+
+export const updateSupplement = (id: number, data: SupplementUpdate) =>
+ client.put(`/supplements/${id}`, data).then(r => r.data);
+
+export const deleteSupplement = (id: number) =>
+ client.delete(`/supplements/${id}`);
+
+export const logSupplement = (id: number, data: { dose_taken?: number; notes?: string } = {}) =>
+ client.post(`/supplements/${id}/log`, data).then(r => r.data);
+
+export const getSupplementLogs = (params?: {
+ supplement_id?: number;
+ start_date?: string;
+ end_date?: string;
+}) => client.get('/supplements/logs', { params }).then(r => r.data);
+
+export const getTodaySupplements = () =>
+ client.get('/supplements/today').then(r => r.data);
diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts
new file mode 100644
index 0000000..de31f9a
--- /dev/null
+++ b/frontend/src/api/users.ts
@@ -0,0 +1,8 @@
+import client from './client';
+import type { User, UserUpdate } from '../types/user';
+
+export const getMe = () =>
+ client.get('/users/me').then(r => r.data);
+
+export const updateMe = (data: UserUpdate) =>
+ client.put('/users/me', data).then(r => r.data);
diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx
new file mode 100644
index 0000000..d2f989e
--- /dev/null
+++ b/frontend/src/components/ConfirmModal.tsx
@@ -0,0 +1,47 @@
+interface ConfirmModalProps {
+ title: string;
+ message: string;
+ confirmLabel?: string;
+ cancelLabel?: string;
+ destructive?: boolean;
+ onConfirm: () => void;
+ onCancel: () => void;
+}
+
+export function ConfirmModal({
+ title,
+ message,
+ confirmLabel = 'Confirm',
+ cancelLabel = 'Cancel',
+ destructive = false,
+ onConfirm,
+ onCancel,
+}: ConfirmModalProps) {
+ return (
+
+
+
+
{title}
+
{message}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/Layout/AppSidebar.tsx b/frontend/src/components/Layout/AppSidebar.tsx
index abc3d83..752aced 100644
--- a/frontend/src/components/Layout/AppSidebar.tsx
+++ b/frontend/src/components/Layout/AppSidebar.tsx
@@ -14,10 +14,13 @@ import {
Utensils,
Heart,
Calendar,
+ CalendarDays,
User,
Moon,
Sun,
LogOut,
+ Dumbbell,
+ Pill,
} from 'lucide-react'
import { useLocation } from 'react-router-dom'
import { useTheme } from '../../context/ThemeContext'
@@ -34,6 +37,9 @@ export function AppSidebar() {
{ to: "/", icon: LayoutDashboard, label: "Dashboard" },
{ to: "/nutrition", icon: Utensils, label: "Nutrition" },
{ to: "/health", icon: Heart, label: "Health" },
+ { to: "/kettlebell", icon: Dumbbell, label: "Kettlebell" },
+ { to: "/supplements", icon: Pill, label: "Supplements" },
+ { to: "/calendar", icon: CalendarDays, label: "Calendar" },
{ to: "/plans", icon: Calendar, label: "Plans" },
{ to: "/profile", icon: User, label: "Profile" },
]
diff --git a/frontend/src/components/calendar/CalendarEventForm.tsx b/frontend/src/components/calendar/CalendarEventForm.tsx
new file mode 100644
index 0000000..93e4fc5
--- /dev/null
+++ b/frontend/src/components/calendar/CalendarEventForm.tsx
@@ -0,0 +1,173 @@
+import { useState, FormEvent } from 'react';
+import type { CalendarEvent } from '../../types/calendar';
+import { createEvent, updateEvent } from '../../api/calendar';
+import { toast } from 'sonner';
+
+const COLOR_SWATCHES = [
+ { label: 'Blue', value: 'blue', cls: 'bg-blue-500' },
+ { label: 'Green', value: 'green', cls: 'bg-green-500' },
+ { label: 'Red', value: 'red', cls: 'bg-red-500' },
+ { label: 'Purple', value: 'purple', cls: 'bg-purple-500' },
+ { label: 'Orange', value: 'orange', cls: 'bg-orange-500' },
+ { label: 'Pink', value: 'pink', cls: 'bg-pink-500' },
+];
+
+interface Props {
+ date: string;
+ existing?: CalendarEvent;
+ defaultType?: CalendarEvent['event_type'];
+ onSuccess: () => void;
+ onCancel: () => void;
+}
+
+export default function CalendarEventForm({ date, existing, defaultType = 'general', onSuccess, onCancel }: Props) {
+ const [title, setTitle] = useState(existing?.title ?? '');
+ const [description, setDescription] = useState(existing?.description ?? '');
+ const [eventType, setEventType] = useState(existing?.event_type ?? defaultType);
+ const [color, setColor] = useState(existing?.color ?? '');
+ const [startTime, setStartTime] = useState(existing?.start_time ?? '');
+ const [isCompleted, setIsCompleted] = useState(existing?.is_completed ?? false);
+ const [submitting, setSubmitting] = useState(false);
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ if (!title.trim()) {
+ toast.error('Title is required');
+ return;
+ }
+ setSubmitting(true);
+ try {
+ if (existing?.id) {
+ await updateEvent(existing.id, {
+ title: title.trim(),
+ description: description.trim() || undefined,
+ event_type: eventType,
+ color: color || undefined,
+ start_time: startTime || undefined,
+ is_completed: isCompleted,
+ });
+ toast.success('Event updated');
+ } else {
+ await createEvent({
+ date,
+ title: title.trim(),
+ description: description.trim() || undefined,
+ event_type: eventType,
+ color: color || undefined,
+ start_time: startTime || undefined,
+ is_completed: isCompleted,
+ });
+ toast.success('Event created');
+ }
+ onSuccess();
+ } catch {
+ toast.error('Failed to save event');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/calendar/DayModal.tsx b/frontend/src/components/calendar/DayModal.tsx
new file mode 100644
index 0000000..017bf5a
--- /dev/null
+++ b/frontend/src/components/calendar/DayModal.tsx
@@ -0,0 +1,420 @@
+import { useState, useRef, useEffect, useCallback } from 'react';
+import { X, Dumbbell, Pill, FileText, Plus, Pencil, Trash2, CheckCircle2, Circle } from 'lucide-react';
+import { toast } from 'sonner';
+import type { DayDetail, CalendarEvent as CalEvent } from '../../types/calendar';
+import { upsertNote, deleteEvent } from '../../api/calendar';
+import CalendarEventForm from './CalendarEventForm';
+
+const MOOD_OPTIONS = [
+ { value: 'great', emoji: '😄', label: 'Great' },
+ { value: 'good', emoji: '🙂', label: 'Good' },
+ { value: 'okay', emoji: '😐', label: 'Okay' },
+ { value: 'bad', emoji: '😕', label: 'Bad' },
+ { value: 'awful', emoji: '😞', label: 'Awful' },
+];
+
+function formatDate(dateStr: string): string {
+ const [year, month, day] = dateStr.split('-').map(Number);
+ return new Date(year, month - 1, day).toLocaleDateString('en-US', {
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
+ });
+}
+
+function colorClass(color?: string): string {
+ const map: Record = {
+ blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
+ green: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
+ red: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
+ purple: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
+ orange: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
+ pink: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300',
+ };
+ return color && map[color] ? map[color] : 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300';
+}
+
+// ── Workout Tab ───────────────────────────────────────────────────────────────
+
+function WorkoutTab({ detail, onRefresh }: { detail: DayDetail; onRefresh: () => void }) {
+ const [showForm, setShowForm] = useState(false);
+ const [editEvent, setEditEvent] = useState(null);
+
+ const workoutEvents = detail.events.filter(e => e.event_type === 'workout');
+
+ const handleDelete = async (id: number) => {
+ try {
+ await deleteEvent(id);
+ toast.success('Event deleted');
+ onRefresh();
+ } catch {
+ toast.error('Failed to delete event');
+ }
+ };
+
+ if (showForm || editEvent) {
+ return (
+
+
+ {editEvent ? 'Edit Event' : 'Add Workout Event'}
+
+ { setShowForm(false); setEditEvent(null); onRefresh(); }}
+ onCancel={() => { setShowForm(false); setEditEvent(null); }}
+ />
+
+ );
+ }
+
+ return (
+
+ {/* Kettlebell sessions */}
+ {detail.kettlebell_sessions.length > 0 && (
+
+
+ Kettlebell Sessions
+
+
+ {detail.kettlebell_sessions.map(kb => (
+
+
+
+
{kb.title}
+
+ {kb.focus} · {kb.total_duration_min} min · {kb.difficulty}
+
+
+
+ {kb.status}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Workout events */}
+ {workoutEvents.length > 0 && (
+
+
+ Workout Events
+
+
+ {workoutEvents.map(ev => (
+
+
+
+
+ {ev.is_completed ?
:
}
+
{ev.title}
+
+ {ev.description && (
+
{ev.description}
+ )}
+ {ev.start_time && (
+
{ev.start_time}
+ )}
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {detail.kettlebell_sessions.length === 0 && workoutEvents.length === 0 && (
+
No workouts logged for this day.
+ )}
+
+
+
+ );
+}
+
+// ── Supplements Tab ───────────────────────────────────────────────────────────
+
+function SupplementsTab({ detail }: { detail: DayDetail }) {
+ const taken = detail.supplements.filter(s => s.taken_today);
+ const notTaken = detail.supplements.filter(s => !s.taken_today);
+ const compliance = detail.supplements.length > 0
+ ? Math.round((taken.length / detail.supplements.length) * 100)
+ : null;
+
+ return (
+
+ {compliance !== null && (
+
+
+
+ Compliance
+ {taken.length}/{detail.supplements.length}
+
+
+
= 80 ? 'bg-green-500' : compliance >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
+ style={{ width: `${compliance}%` }}
+ />
+
+
+
{compliance}%
+
+ )}
+
+ {detail.supplements.length === 0 && (
+
No active supplements.
+ )}
+
+ {taken.length > 0 && (
+
+
Taken
+
+ {taken.map(s => (
+
+
+ {s.name}
+ {s.dosage} {s.unit}
+
+ ))}
+
+
+ )}
+
+ {notTaken.length > 0 && (
+
+
Not Taken
+
+ {notTaken.map(s => (
+
+
+ {s.name}
+ {s.dosage} {s.unit}
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+// ── Notes Tab ─────────────────────────────────────────────────────────────────
+
+function NotesTab({ detail, onRefresh }: { detail: DayDetail; onRefresh: () => void }) {
+ const [content, setContent] = useState(detail.note?.content ?? '');
+ const [mood, setMood] = useState(detail.note?.mood ?? '');
+ const [energyLevel, setEnergyLevel] = useState(detail.note?.energy_level ?? 5);
+ const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
+ const debounceRef = useRef
| null>(null);
+
+ const save = useCallback(async (c: string, m: string, e: number) => {
+ setSaveStatus('saving');
+ try {
+ await upsertNote(detail.date, {
+ content: c,
+ mood: m || undefined,
+ energy_level: e,
+ });
+ setSaveStatus('saved');
+ onRefresh();
+ } catch {
+ setSaveStatus('idle');
+ toast.error('Failed to save note');
+ }
+ }, [detail.date, onRefresh]);
+
+ const scheduleAutosave = (c: string, m: string, e: number) => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => save(c, m, e), 1500);
+ };
+
+ useEffect(() => {
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ };
+ }, []);
+
+ const handleContentChange = (val: string) => {
+ if (val.length > 10000) return;
+ setContent(val);
+ setSaveStatus('idle');
+ scheduleAutosave(val, mood, energyLevel);
+ };
+
+ const handleMoodChange = (val: string) => {
+ const newMood = mood === val ? '' : val;
+ setMood(newMood);
+ setSaveStatus('idle');
+ scheduleAutosave(content, newMood, energyLevel);
+ };
+
+ const handleEnergyChange = (val: number) => {
+ setEnergyLevel(val);
+ setSaveStatus('idle');
+ scheduleAutosave(content, mood, val);
+ };
+
+ return (
+
+
+
+
+
+ {saveStatus === 'saving' && 'Saving...'}
+ {saveStatus === 'saved' && Saved}
+ {saveStatus === 'idle' && `${content.length}/10000`}
+
+
+
+
+
+
+
+ {MOOD_OPTIONS.map(opt => (
+
+ ))}
+
+
+
+
+
+
+ {energyLevel}/10
+
+
handleEnergyChange(Number(e.target.value))}
+ className="w-full accent-primary"
+ />
+
+ LowHigh
+
+
+
+ );
+}
+
+// ── DayModal ──────────────────────────────────────────────────────────────────
+
+type Tab = 'workout' | 'supplements' | 'notes';
+
+interface Props {
+ date: string;
+ detail: DayDetail | null;
+ loading: boolean;
+ onClose: () => void;
+ onRefresh: () => void;
+}
+
+export default function DayModal({ date, detail, loading, onClose, onRefresh }: Props) {
+ const [activeTab, setActiveTab] = useState('notes');
+
+ const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
+ { id: 'workout', label: 'Workout', icon: },
+ { id: 'supplements', label: 'Supplements', icon: },
+ { id: 'notes', label: 'Notes', icon: },
+ ];
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Panel */}
+
+ {/* Header */}
+
+
+ {formatDate(date)}
+
+
+
+
+ {/* Tabs */}
+
+ {tabs.map(tab => (
+
+ ))}
+
+
+ {/* Content */}
+
+ {loading && (
+
+ )}
+ {!loading && detail && (
+ <>
+ {activeTab === 'workout' &&
}
+ {activeTab === 'supplements' &&
}
+ {activeTab === 'notes' &&
}
+ >
+ )}
+ {!loading && !detail && (
+
Failed to load day details.
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/kettlebell/ElapsedTimer.tsx b/frontend/src/components/kettlebell/ElapsedTimer.tsx
new file mode 100644
index 0000000..b397d83
--- /dev/null
+++ b/frontend/src/components/kettlebell/ElapsedTimer.tsx
@@ -0,0 +1,55 @@
+interface ElapsedTimerProps {
+ seconds: number;
+}
+
+export function ElapsedTimer({ seconds }: ElapsedTimerProps) {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = seconds % 60;
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return (
+
+ {h > 0 ? `${pad(h)}:` : ''}{pad(m)}:{pad(s)}
+
+ );
+}
+
+export function ElapsedTimerCircle({ seconds }: ElapsedTimerProps) {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = seconds % 60;
+ const pad = (n: number) => String(n).padStart(2, '0');
+ const radius = 70;
+ const circumference = 2 * Math.PI * radius;
+
+ return (
+
+
+
+
+
+ {h > 0 ? `${pad(h)}:` : ''}{pad(m)}:{pad(s)}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/kettlebell/ProgressBar.tsx b/frontend/src/components/kettlebell/ProgressBar.tsx
new file mode 100644
index 0000000..ec6740b
--- /dev/null
+++ b/frontend/src/components/kettlebell/ProgressBar.tsx
@@ -0,0 +1,14 @@
+interface ProgressBarProps {
+ exerciseIdx: number;
+ totalExercises: number;
+ setIdx: number;
+ totalSets: number;
+}
+
+export function ProgressBar({ exerciseIdx, totalExercises, setIdx, totalSets }: ProgressBarProps) {
+ return (
+
+ Exercise {exerciseIdx + 1}/{totalExercises} · Set {setIdx + 1}/{totalSets}
+
+ );
+}
diff --git a/frontend/src/components/kettlebell/RestCountdown.tsx b/frontend/src/components/kettlebell/RestCountdown.tsx
new file mode 100644
index 0000000..71151c8
--- /dev/null
+++ b/frontend/src/components/kettlebell/RestCountdown.tsx
@@ -0,0 +1,46 @@
+interface RestCountdownProps {
+ remaining: number;
+ total: number;
+}
+
+export function RestCountdown({ remaining, total }: RestCountdownProps) {
+ const radius = 54;
+ const circumference = 2 * Math.PI * radius;
+ const progress = total > 0 ? remaining / total : 0;
+ const dashOffset = circumference * (1 - progress);
+
+ const m = Math.floor(remaining / 60);
+ const s = remaining % 60;
+ const pad = (n: number) => String(n).padStart(2, '0');
+
+ return (
+
+
+
+
+
+ {pad(m)}:{pad(s)}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/kettlebell/SessionSummary.tsx b/frontend/src/components/kettlebell/SessionSummary.tsx
new file mode 100644
index 0000000..b9f6b2a
--- /dev/null
+++ b/frontend/src/components/kettlebell/SessionSummary.tsx
@@ -0,0 +1,36 @@
+import type { KettlebellSetLog } from '../../types/kettlebell';
+import { ElapsedTimer } from './ElapsedTimer';
+
+interface SessionSummaryProps {
+ totalElapsed: number;
+ logged: KettlebellSetLog[];
+}
+
+export function SessionSummary({ totalElapsed, logged }: SessionSummaryProps) {
+ const avgRpe = logged.length > 0
+ ? Math.round(logged.reduce((sum, s) => sum + s.perceived_effort, 0) / logged.length * 10) / 10
+ : 0;
+
+ return (
+
+
🎉
+
Workout Complete!
+
+
+
+
{logged.length}
+
Sets Done
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/kettlebell/SetLogger.tsx b/frontend/src/components/kettlebell/SetLogger.tsx
new file mode 100644
index 0000000..000c8bc
--- /dev/null
+++ b/frontend/src/components/kettlebell/SetLogger.tsx
@@ -0,0 +1,101 @@
+import type { ExerciseBlock } from '../../types/kettlebell';
+
+interface SetLoggerProps {
+ exercise: ExerciseBlock;
+ reps: number;
+ weightKg: number;
+ effort: number;
+ onRepsChange: (v: number) => void;
+ onWeightChange: (v: number) => void;
+ onEffortChange: (v: number) => void;
+ onComplete: () => void;
+}
+
+function Stepper({ label, value, onChange, step = 1, min = 0 }: {
+ label: string;
+ value: number;
+ onChange: (v: number) => void;
+ step?: number;
+ min?: number;
+}) {
+ return (
+
+
{label}
+
+
+
+ {value}
+
+
+
+
+ );
+}
+
+function RpeDots({ value, onChange }: { value: number; onChange: (v: number) => void }) {
+ return (
+
+
Effort
+
+ {Array.from({ length: 10 }, (_, i) => i + 1).map((dot) => (
+
+
+ );
+}
+
+export function SetLogger({
+ exercise,
+ reps,
+ weightKg,
+ effort,
+ onRepsChange,
+ onWeightChange,
+ onEffortChange,
+ onComplete,
+}: SetLoggerProps) {
+ const isTimed = exercise.reps === 0;
+
+ return (
+
+ {isTimed ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/ActiveSession.tsx b/frontend/src/pages/ActiveSession.tsx
new file mode 100644
index 0000000..a4c779d
--- /dev/null
+++ b/frontend/src/pages/ActiveSession.tsx
@@ -0,0 +1,532 @@
+import { useEffect, useReducer, useCallback, useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { getSession, getSets, startSession, logSet, completeSession, abandonSession } from '../api/kettlebell';
+import type { KettlebellSession, KettlebellSetLog, ExerciseBlock } from '../types/kettlebell';
+import { ElapsedTimer, ElapsedTimerCircle } from '../components/kettlebell/ElapsedTimer';
+import { RestCountdown } from '../components/kettlebell/RestCountdown';
+import { SetLogger } from '../components/kettlebell/SetLogger';
+import { ProgressBar } from '../components/kettlebell/ProgressBar';
+import { SessionSummary } from '../components/kettlebell/SessionSummary';
+import { ConfirmModal } from '../components/ConfirmModal';
+
+// --------------- State Machine ---------------
+
+interface LastSetStats {
+ duration: number;
+ restTaken: number | null;
+ reps: number;
+ weightKg: number;
+ effort: number;
+}
+
+type SessionPhase =
+ | { phase: 'loading' }
+ | { phase: 'planning'; session: KettlebellSession }
+ | { phase: 'active'; session: KettlebellSession; exerciseIdx: number; setIdx: number; elapsed: number; setElapsed: number; lastSetStats: LastSetStats | null; logged: KettlebellSetLog[]; reps: number; weightKg: number; effort: number }
+ | { phase: 'resting'; session: KettlebellSession; exerciseIdx: number; nextSetIdx: number; restRemaining: number; totalRest: number; elapsed: number; pendingSet: { duration: number; reps: number; weightKg: number; effort: number }; logged: KettlebellSetLog[] }
+ | { phase: 'complete'; session: KettlebellSession; totalElapsed: number; logged: KettlebellSetLog[] };
+
+type Action =
+ | { type: 'LOAD'; session: KettlebellSession; existingLogs: KettlebellSetLog[] }
+ | { type: 'START' }
+ | { type: 'TICK_ELAPSED' }
+ | { type: 'TICK_REST' }
+ | { type: 'SET_REPS'; value: number }
+ | { type: 'SET_WEIGHT'; value: number }
+ | { type: 'SET_EFFORT'; value: number }
+ | { type: 'COMPLETE_SET'; log: KettlebellSetLog }
+ | { type: 'SKIP_REST' }
+ | { type: 'FINISH' };
+
+function getExercises(session: KettlebellSession): ExerciseBlock[] {
+ return session.exercises?.exercises ?? [];
+}
+
+function initialSetValues(exercise: ExerciseBlock) {
+ return {
+ reps: exercise.reps > 0 ? exercise.reps : exercise.duration_seconds,
+ weightKg: exercise.weight_kg,
+ effort: 5,
+ };
+}
+
+function reducer(state: SessionPhase, action: Action): SessionPhase {
+ switch (action.type) {
+ case 'LOAD': {
+ const { session, existingLogs } = action;
+ if (session.status === 'completed') {
+ const elapsed = session.started_at && session.completed_at
+ ? Math.round((new Date(session.completed_at).getTime() - new Date(session.started_at).getTime()) / 1000)
+ : 0;
+ return { phase: 'complete', session, totalElapsed: elapsed, logged: existingLogs };
+ }
+ if (session.status === 'in_progress') {
+ const exercises = getExercises(session);
+ const elapsed = session.started_at
+ ? Math.round((Date.now() - new Date(session.started_at).getTime()) / 1000)
+ : 0;
+ // Rehydrate: find next incomplete set
+ let exerciseIdx = 0;
+ let setIdx = 0;
+ for (let ei = 0; ei < exercises.length; ei++) {
+ for (let si = 0; si < exercises[ei].sets; si++) {
+ const done = existingLogs.some(l => l.exercise_order === exercises[ei].order && l.set_number === si + 1);
+ if (!done) {
+ exerciseIdx = ei;
+ setIdx = si;
+ const ex = exercises[ei];
+ return {
+ phase: 'active', session, exerciseIdx, setIdx, elapsed, setElapsed: 0, lastSetStats: null, logged: existingLogs,
+ ...initialSetValues(ex),
+ };
+ }
+ }
+ }
+ // All sets done but session not completed
+ return { phase: 'complete', session, totalElapsed: elapsed, logged: existingLogs };
+ }
+ return { phase: 'planning', session };
+ }
+ case 'START': {
+ if (state.phase !== 'planning') return state;
+ const exercises = getExercises(state.session);
+ if (exercises.length === 0) return state;
+ const ex = exercises[0];
+ return {
+ phase: 'active',
+ session: { ...state.session, status: 'in_progress' },
+ exerciseIdx: 0,
+ setIdx: 0,
+ elapsed: 0,
+ setElapsed: 0,
+ lastSetStats: null,
+ logged: [],
+ ...initialSetValues(ex),
+ };
+ }
+ case 'TICK_ELAPSED': {
+ if (state.phase === 'active') return { ...state, elapsed: state.elapsed + 1, setElapsed: state.setElapsed + 1 };
+ if (state.phase === 'resting') return { ...state, elapsed: state.elapsed + 1 };
+ return state;
+ }
+ case 'TICK_REST': {
+ if (state.phase !== 'resting') return state;
+ if (state.restRemaining <= 1) {
+ // Auto-advance to next set
+ return advanceAfterRest(state);
+ }
+ return { ...state, restRemaining: state.restRemaining - 1 };
+ }
+ case 'SET_REPS': {
+ if (state.phase !== 'active') return state;
+ return { ...state, reps: action.value };
+ }
+ case 'SET_WEIGHT': {
+ if (state.phase !== 'active') return state;
+ return { ...state, weightKg: action.value };
+ }
+ case 'SET_EFFORT': {
+ if (state.phase !== 'active') return state;
+ return { ...state, effort: action.value };
+ }
+ case 'COMPLETE_SET': {
+ if (state.phase !== 'active') return state;
+ const newLogged = [...state.logged, action.log];
+ const exercises = getExercises(state.session);
+ const exercise = exercises[state.exerciseIdx];
+ const totalRest = exercise.rest_seconds;
+
+ // Check if there's a next set or exercise
+ const hasNextSet = state.setIdx + 1 < exercise.sets;
+ const hasNextExercise = state.exerciseIdx + 1 < exercises.length;
+
+ if (!hasNextSet && !hasNextExercise) {
+ // Last set of last exercise — go to complete
+ return {
+ phase: 'complete',
+ session: state.session,
+ totalElapsed: state.elapsed,
+ logged: newLogged,
+ };
+ }
+
+ const pendingSet = { duration: state.setElapsed, reps: state.reps, weightKg: state.weightKg, effort: state.effort };
+
+ if (totalRest > 0) {
+ return {
+ phase: 'resting',
+ session: state.session,
+ exerciseIdx: state.exerciseIdx,
+ nextSetIdx: state.setIdx + 1,
+ restRemaining: totalRest,
+ totalRest,
+ elapsed: state.elapsed,
+ pendingSet,
+ logged: newLogged,
+ };
+ }
+ return advanceFromActive(state, newLogged, { ...pendingSet, restTaken: null });
+ }
+ case 'SKIP_REST': {
+ if (state.phase !== 'resting') return state;
+ return advanceAfterRest(state);
+ }
+ case 'FINISH':
+ return state;
+ default:
+ return state;
+ }
+}
+
+function advanceFromActive(state: Extract, newLogged: KettlebellSetLog[], lastSetStats: LastSetStats): SessionPhase {
+ const exercises = getExercises(state.session);
+ const exercise = exercises[state.exerciseIdx];
+ if (state.setIdx + 1 < exercise.sets) {
+ return { ...state, setIdx: state.setIdx + 1, setElapsed: 0, lastSetStats, logged: newLogged, ...initialSetValues(exercise) };
+ }
+ if (state.exerciseIdx + 1 < exercises.length) {
+ const nextEx = exercises[state.exerciseIdx + 1];
+ return { ...state, exerciseIdx: state.exerciseIdx + 1, setIdx: 0, setElapsed: 0, lastSetStats, logged: newLogged, ...initialSetValues(nextEx) };
+ }
+ return { phase: 'complete', session: state.session, totalElapsed: state.elapsed, logged: newLogged };
+}
+
+function advanceAfterRest(state: Extract): SessionPhase {
+ const exercises = getExercises(state.session);
+ const exercise = exercises[state.exerciseIdx];
+ const restTaken = state.totalRest - state.restRemaining;
+ const lastSetStats: LastSetStats = { ...state.pendingSet, restTaken };
+ if (state.nextSetIdx < exercise.sets) {
+ return {
+ phase: 'active',
+ session: state.session,
+ exerciseIdx: state.exerciseIdx,
+ setIdx: state.nextSetIdx,
+ elapsed: state.elapsed,
+ setElapsed: 0,
+ lastSetStats,
+ logged: state.logged,
+ ...initialSetValues(exercise),
+ };
+ }
+ if (state.exerciseIdx + 1 < exercises.length) {
+ const nextEx = exercises[state.exerciseIdx + 1];
+ return {
+ phase: 'active',
+ session: state.session,
+ exerciseIdx: state.exerciseIdx + 1,
+ setIdx: 0,
+ elapsed: state.elapsed,
+ setElapsed: 0,
+ lastSetStats,
+ logged: state.logged,
+ ...initialSetValues(nextEx),
+ };
+ }
+ return { phase: 'complete', session: state.session, totalElapsed: state.elapsed, logged: state.logged };
+}
+
+// --------------- Component ---------------
+
+export default function ActiveSession() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const [state, dispatch] = useReducer(reducer, { phase: 'loading' });
+
+ // Load session on mount
+ useEffect(() => {
+ if (!id) return;
+ Promise.all([getSession(Number(id)), getSets(Number(id))])
+ .then(([session, logs]) => dispatch({ type: 'LOAD', session, existingLogs: logs }))
+ .catch(() => navigate('/kettlebell'));
+ }, [id, navigate]);
+
+ // Elapsed timer
+ useEffect(() => {
+ if (state.phase !== 'active' && state.phase !== 'resting') return;
+ const interval = setInterval(() => dispatch({ type: 'TICK_ELAPSED' }), 1000);
+ return () => clearInterval(interval);
+ }, [state.phase]);
+
+ // Rest countdown
+ useEffect(() => {
+ if (state.phase !== 'resting') return;
+ const interval = setInterval(() => dispatch({ type: 'TICK_REST' }), 1000);
+ return () => clearInterval(interval);
+ }, [state.phase]);
+
+ // Start session on server
+ const handleStart = useCallback(async () => {
+ if (!id || state.phase !== 'planning') return;
+ await startSession(Number(id));
+ dispatch({ type: 'START' });
+ }, [id, state.phase]);
+
+ // Complete set: log to server, then advance
+ const handleCompleteSet = useCallback(async () => {
+ if (state.phase !== 'active' || !id) return;
+ const exercises = getExercises(state.session);
+ const exercise = exercises[state.exerciseIdx];
+ const isTimed = exercise.reps === 0;
+ try {
+ const log = await logSet(Number(id), {
+ exercise_order: exercise.order,
+ set_number: state.setIdx + 1,
+ actual_reps: isTimed ? 0 : state.reps,
+ actual_weight_kg: state.weightKg,
+ actual_duration_seconds: isTimed ? state.reps : 0,
+ perceived_effort: state.effort,
+ });
+ dispatch({ type: 'COMPLETE_SET', log });
+
+ // Check if this was the last set of last exercise
+ const hasNextSet = state.setIdx + 1 < exercise.sets;
+ const hasNextExercise = state.exerciseIdx + 1 < exercises.length;
+ if (!hasNextSet && !hasNextExercise) {
+ await completeSession(Number(id));
+ }
+ } catch (err) {
+ console.error('Failed to log set', err);
+ alert('Failed to log set. Please try again.');
+ }
+ }, [id, state]);
+
+ const [showAbandonModal, setShowAbandonModal] = useState(false);
+
+ const handleAbandon = useCallback(async () => {
+ if (!id) return;
+ try {
+ await abandonSession(Number(id));
+ navigate('/kettlebell');
+ } catch (err) {
+ console.error('Failed to abandon session', err);
+ }
+ }, [id, navigate]);
+
+ if (state.phase === 'loading') {
+ return (
+
+ );
+ }
+
+ if (state.phase === 'planning') {
+ const exercises = getExercises(state.session);
+ return (
+
+
+
+
{state.session.title}
+
+
+
+ {state.session.focus} · {state.session.difficulty} · {state.session.total_duration_min} min
+
+ {exercises.map((ex, i) => (
+
+
+
+
{ex.order}. {ex.name}
+
{ex.description}
+
+
+
{ex.sets} × {ex.reps > 0 ? `${ex.reps} reps` : `${ex.duration_seconds}s`}
+
{ex.weight_kg} kg
+
+
+
"{ex.coaching_tip}"
+
+ ))}
+
+
+
+
+
+ );
+ }
+
+ if (state.phase === 'active') {
+ const exercises = getExercises(state.session);
+ const exercise = exercises[state.exerciseIdx];
+ return (
+
+ {showAbandonModal && (
+
setShowAbandonModal(false)}
+ />
+ )}
+
+ {/* Sticky top bar */}
+
+
+
+
+
+ {/* Exercise info */}
+
+
{exercise.name}
+
+ Set {state.setIdx + 1} of {exercise.sets} · {exercise.weight_kg} kg · {exercise.reps > 0 ? `${exercise.reps} reps` : `${exercise.duration_seconds}s`}
+
+
"{exercise.coaching_tip}"
+
+
+ {/* Timer row: last set card | current set circle | total */}
+
+ {/* Last set card */}
+
+
Last Set
+
+ {state.lastSetStats !== null
+ ?
+ : —}
+
+ {state.lastSetStats !== null && (
+
+ {state.lastSetStats.restTaken !== null && (
+
+ Rest
+
+
+ )}
+
+ Reps
+ {state.lastSetStats.reps}
+
+
+ Weight
+ {state.lastSetStats.weightKg} kg
+
+
+ Effort
+ {state.lastSetStats.effort}/10
+
+
+ )}
+
+
+
+
+ {/* Total */}
+
+ Total
+
+
+
+
+
+
+ {/* Set logger */}
+
+ dispatch({ type: 'SET_REPS', value })}
+ onWeightChange={value => dispatch({ type: 'SET_WEIGHT', value })}
+ onEffortChange={value => dispatch({ type: 'SET_EFFORT', value })}
+ onComplete={handleCompleteSet}
+ />
+
+
+ {/* Cancel session */}
+
+
+
+
+ );
+ }
+
+ if (state.phase === 'resting') {
+ const exercises = getExercises(state.session);
+ const exercise = exercises[state.exerciseIdx];
+ const isLastSet = state.nextSetIdx >= exercise.sets;
+ const nextExercise = isLastSet ? exercises[state.exerciseIdx + 1] : null;
+ const nextLabel = isLastSet && nextExercise
+ ? `Next: ${nextExercise.name} · Set 1/${nextExercise.sets} · ${nextExercise.weight_kg}kg`
+ : `Next: Set ${state.nextSetIdx + 1}/${exercise.sets} · ${exercise.name} · ${exercise.weight_kg}kg`;
+
+ return (
+
+ {showAbandonModal && (
+
setShowAbandonModal(false)}
+ />
+ )}
+
+
+
+
+
+
+
+
+ REST
+
+ {nextLabel}
+
+
+
+
+
+ );
+ }
+
+ if (state.phase === 'complete') {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/frontend/src/pages/Calendar.tsx b/frontend/src/pages/Calendar.tsx
new file mode 100644
index 0000000..54bfe38
--- /dev/null
+++ b/frontend/src/pages/Calendar.tsx
@@ -0,0 +1,251 @@
+import { useState, useEffect, useCallback } from 'react';
+import { DayPicker } from 'react-day-picker';
+import 'react-day-picker/style.css';
+import { CalendarDays, Dumbbell, Pill, FileText, ChevronLeft, ChevronRight, Plus } from 'lucide-react';
+import { toast } from 'sonner';
+import type { DayMeta, DayDetail } from '../types/calendar';
+import { getMonthSummary, getDayDetail } from '../api/calendar';
+import DayModal from '../components/calendar/DayModal';
+
+function pad(n: number) { return String(n).padStart(2, '0'); }
+function toDateStr(d: Date): string {
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+}
+
+function MonthStats({ meta }: { meta: DayMeta[] }) {
+ const workoutDays = meta.filter(d => d.has_workout).length;
+ const noteDays = meta.filter(d => d.has_note).length;
+ const daysWithSupp = meta.filter(d => d.supplement_compliance !== null);
+ const avgCompliance = daysWithSupp.length > 0
+ ? Math.round(daysWithSupp.reduce((a, d) => a + (d.supplement_compliance ?? 0), 0) / daysWithSupp.length * 100)
+ : null;
+
+ const stats = [
+ { icon: , label: 'Workout Days', value: workoutDays, color: 'text-green-600 dark:text-green-400' },
+ { icon: , label: 'Avg Compliance', value: avgCompliance !== null ? `${avgCompliance}%` : '—', color: 'text-blue-600 dark:text-blue-400' },
+ { icon: , label: 'Days with Notes', value: noteDays, color: 'text-yellow-600 dark:text-yellow-400' },
+ ];
+
+ return (
+
+ {stats.map(s => (
+
+ {s.icon}
+ {s.value}
+ {s.label}
+
+ ))}
+
+ );
+}
+
+export default function Calendar() {
+ const [month, setMonth] = useState(() => new Date());
+ const [monthMeta, setMonthMeta] = useState([]);
+ const [metaLoading, setMetaLoading] = useState(false);
+ const [selectedDate, setSelectedDate] = useState(null);
+ const [dayDetail, setDayDetail] = useState(null);
+ const [detailLoading, setDetailLoading] = useState(false);
+
+ const metaByDate = Object.fromEntries(monthMeta.map(m => [m.date, m]));
+
+ const fetchMonth = useCallback(async (m: Date) => {
+ setMetaLoading(true);
+ try {
+ const data = await getMonthSummary(m.getFullYear(), m.getMonth() + 1);
+ setMonthMeta(data);
+ } catch {
+ toast.error('Failed to load calendar data');
+ } finally {
+ setMetaLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchMonth(month);
+ }, [month, fetchMonth]);
+
+ const openDay = async (date: string) => {
+ setSelectedDate(date);
+ setDayDetail(null);
+ setDetailLoading(true);
+ try {
+ const data = await getDayDetail(date);
+ setDayDetail(data);
+ } catch {
+ toast.error('Failed to load day details');
+ } finally {
+ setDetailLoading(false);
+ }
+ };
+
+ const handleDayClick = (day: Date) => {
+ openDay(toDateStr(day));
+ };
+
+ const handleModalClose = () => {
+ setSelectedDate(null);
+ setDayDetail(null);
+ };
+
+ const handleRefresh = () => {
+ if (selectedDate) openDay(selectedDate);
+ fetchMonth(month);
+ };
+
+ // Build modifiers from monthMeta
+ const workoutDates = monthMeta.filter(d => d.has_workout).map(d => new Date(d.date + 'T00:00:00'));
+ const suppGoodDates = monthMeta
+ .filter(d => d.supplement_compliance !== null && d.supplement_compliance >= 0.8)
+ .map(d => new Date(d.date + 'T00:00:00'));
+ const noteDates = monthMeta.filter(d => d.has_note).map(d => new Date(d.date + 'T00:00:00'));
+ const calorieDates = monthMeta.filter(d => d.calorie_total && d.calorie_total > 0).map(d => new Date(d.date + 'T00:00:00'));
+
+ return (
+
+
+
+
+ Calendar
+
+
Track your health journey day by day.
+
+
+
+
+
+
+ {metaLoading && (
+
+ )}
+
+
+
+ {/* Legend */}
+
+
+
+ Workout
+
+
+
+ Supplements ≥80%
+
+
+
+ Note
+
+
+
+
+
+
+ {selectedDate && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 89f47b0..06867f3 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -1,25 +1,32 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import client from '../api/client';
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
- BarChart, Bar, PieChart, Pie, Cell, Legend
+ PieChart, Pie, Cell, Legend
} from 'recharts';
import { Activity, Flame, Footprints, Scale, Utensils } from 'lucide-react';
+import type { FoodLog } from '../types/nutrition';
+import type { HealthMetric } from '../types/health';
+import type { NutritionSummary } from '../types/nutrition';
+import { getNutritionSummary } from '../api/nutrition';
const Dashboard = () => {
const [loading, setLoading] = useState(true);
- const [metrics, setMetrics] = useState([]);
- const [logs, setLogs] = useState([]);
+ const [metrics, setMetrics] = useState([]);
+ const [logs, setLogs] = useState([]);
+ const [summary, setSummary] = useState(null);
useEffect(() => {
const loadData = async () => {
try {
- const [metricsRes, logsRes] = await Promise.all([
+ const [metricsRes, logsRes, summaryData] = await Promise.all([
client.get('/health/metrics'),
- client.get('/nutrition/logs')
+ client.get('/nutrition/logs'),
+ getNutritionSummary(),
]);
setMetrics(metricsRes.data);
setLogs(logsRes.data);
+ setSummary(summaryData);
} catch (error) {
console.error("Failed to load dashboard data", error);
} finally {
@@ -171,7 +178,7 @@ const Dashboard = () => {
paddingAngle={5}
dataKey="value"
>
- {activeMacroData.map((entry, index) => (
+ {activeMacroData.map((_entry, index) => (
|
))}
@@ -191,6 +198,41 @@ const Dashboard = () => {
+ {/* Macro Targets Progress */}
+ {summary && (summary.target_calories || summary.target_protein || summary.target_carbs || summary.target_fat) && (
+
+
+
+ Today's Macro Targets
+
+
+ {[
+ { label: 'Calories', current: Math.round(summary.total_calories), target: summary.target_calories, unit: 'kcal', color: 'bg-orange-500' },
+ { label: 'Protein', current: Math.round(summary.total_protein), target: summary.target_protein, unit: 'g', color: 'bg-red-500' },
+ { label: 'Carbs', current: Math.round(summary.total_carbs), target: summary.target_carbs, unit: 'g', color: 'bg-amber-500' },
+ { label: 'Fat', current: Math.round(summary.total_fats), target: summary.target_fat, unit: 'g', color: 'bg-blue-500' },
+ ].filter(m => m.target).map(macro => {
+ const pct = Math.min(100, Math.round(((macro.current) / (macro.target!)) * 100));
+ return (
+
+
+ {macro.label}
+ {macro.current} / {macro.target} {macro.unit}
+
+
+
{pct}%
+
+ );
+ })}
+
+
+ )}
+
{/* Recent Activity */}
Recent Food Logs
diff --git a/frontend/src/pages/Health.tsx b/frontend/src/pages/Health.tsx
index 30a5d89..4c6abe6 100644
--- a/frontend/src/pages/Health.tsx
+++ b/frontend/src/pages/Health.tsx
@@ -1,7 +1,8 @@
-import { useState, useEffect, useMemo, FormEvent, ChangeEvent } from 'react';
+import { useState, useEffect, useMemo, FormEvent } from 'react';
+import { toast } from 'sonner';
import client from '../api/client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
-import { Field, Label, Fieldset, Legend } from '../components/catalyst/fieldset';
+import { Field, Label } from '../components/catalyst/fieldset';
import { Input } from '../components/catalyst/input';
import { Select } from '../components/catalyst/select';
import { Button } from '../components/catalyst/button';
@@ -56,7 +57,7 @@ const Health = () => {
fetchData();
} catch (error) {
console.error(error);
- alert('Failed to add metric');
+ toast.error('Failed to add metric');
} finally {
setLoading(false);
}
@@ -74,7 +75,7 @@ const Health = () => {
fetchData();
} catch (error) {
console.error(error);
- alert('Failed to add goal');
+ toast.error('Failed to add goal');
} finally {
setLoading(false);
}
diff --git a/frontend/src/pages/Kettlebell.tsx b/frontend/src/pages/Kettlebell.tsx
new file mode 100644
index 0000000..1adbd4d
--- /dev/null
+++ b/frontend/src/pages/Kettlebell.tsx
@@ -0,0 +1,270 @@
+import { useState, useEffect, FormEvent } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'sonner';
+import { generateSession, getSessions, deleteSession, retrySession } from '../api/kettlebell';
+import type { KettlebellSession } from '../types/kettlebell';
+import { Heading, Subheading } from '../components/catalyst/heading';
+import { Button } from '../components/catalyst/button';
+import { Field, Label } from '../components/catalyst/fieldset';
+import { Input } from '../components/catalyst/input';
+import { ConfirmModal } from '../components/ConfirmModal';
+import { BarChart2 } from 'lucide-react';
+
+const FOCUS_OPTIONS = ['strength', 'conditioning', 'fat loss', 'mobility', 'endurance'];
+const DURATION_OPTIONS = [20, 30, 45, 60];
+
+const statusColors: Record
= {
+ generated: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300',
+ in_progress: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200',
+ completed: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200',
+ abandoned: 'bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300',
+};
+
+export default function Kettlebell() {
+ const navigate = useNavigate();
+ const [sessions, setSessions] = useState([]);
+ const [selected, setSelected] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+
+ const [focus, setFocus] = useState('strength');
+ const [duration, setDuration] = useState(30);
+ const [weightsInput, setWeightsInput] = useState('16, 24, 32');
+
+ useEffect(() => {
+ getSessions().then(data => {
+ setSessions(data);
+ if (data.length > 0) setSelected(data[0]);
+ }).catch(console.error);
+ }, []);
+
+ const handleRetry = async (id: number) => {
+ try {
+ const newSession = await retrySession(id);
+ setSessions(prev => [newSession, ...prev]);
+ setSelected(newSession);
+ navigate(`/kettlebell/session/${newSession.id}`);
+ } catch {
+ toast.error('Failed to create retry session.');
+ }
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (!deleteTarget) return;
+ const id = deleteTarget.id;
+ setDeleteTarget(null);
+ try {
+ await deleteSession(id);
+ setSessions(prev => prev.filter(s => s.id !== id));
+ if (selected?.id === id) setSelected(null);
+ } catch {
+ // silently ignore — could add toast here in future
+ }
+ };
+
+ const handleGenerate = async (e: FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ try {
+ const available_weights = weightsInput
+ .split(',')
+ .map(s => parseFloat(s.trim()))
+ .filter(n => !isNaN(n) && n > 0);
+ const session = await generateSession({ focus, duration_minutes: duration, available_weights });
+ setSessions(prev => [session, ...prev]);
+ setSelected(session);
+ } catch {
+ toast.error('Failed to generate session');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const exercises = selected?.exercises?.exercises ?? [];
+
+ return (
+
+ {deleteTarget && (
+
setDeleteTarget(null)}
+ />
+ )}
+
+ Kettlebell
+
+
+
+
+ {/* Left Column */}
+
+
+
Generate Session
+
+
+
+
+
History
+
+ {sessions.map(s => (
+
setSelected(s)}
+ className={`p-4 rounded-xl cursor-pointer transition-colors border-l-4 ${selected?.id === s.id
+ ? 'bg-base border-primary'
+ : 'bg-base/50 border-border hover:bg-base'
+ }`}
+ >
+
+
{s.title}
+
+
+ {s.status}
+
+
+
+
+
+ {s.focus} · {s.total_duration_min}min · {new Date(s.created_at).toLocaleDateString()}
+
+
+ ))}
+ {sessions.length === 0 &&
No sessions yet.
}
+
+
+
+
+ {/* Right Column */}
+
+ {selected ? (
+
+
+
+
+ {selected.title}
+
+
+ {selected.focus} · {selected.difficulty} · {selected.total_duration_min} min
+
+
+
+ {selected.status}
+
+
+
+
+ {exercises.map((ex, i) => (
+
+
+
+
{ex.order}. {ex.name}
+
{ex.description}
+
+
+
{ex.sets} × {ex.reps > 0 ? `${ex.reps} reps` : `${ex.duration_seconds}s`}
+
{ex.weight_kg} kg
+
+
+
"{ex.coaching_tip}"
+
+ ))}
+
+
+ {selected.notes && (
+
+ )}
+
+ {(selected.status === 'generated' || selected.status === 'in_progress') && (
+
+
+
+ )}
+ {selected.status === 'abandoned' && (
+
+
+
+ )}
+
+ ) : (
+
+
🏋️
+
Ready to Train?
+
+ Generate an AI-powered kettlebell session tailored to your profile and goals.
+
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/KettlebellAnalytics.tsx b/frontend/src/pages/KettlebellAnalytics.tsx
new file mode 100644
index 0000000..f765c48
--- /dev/null
+++ b/frontend/src/pages/KettlebellAnalytics.tsx
@@ -0,0 +1,242 @@
+import { useState, useEffect } from 'react';
+import { toast } from 'sonner';
+import {
+ BarChart, Bar, LineChart, Line, AreaChart, Area,
+ XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
+} from 'recharts';
+import { TrendingUp, Trophy, Zap, BarChart2, ChevronLeft } from 'lucide-react';
+import { Heading, Subheading } from '../components/catalyst/heading';
+import { Select } from '../components/catalyst/select';
+import { Button } from '../components/catalyst/button';
+import client from '../api/client';
+import { useNavigate } from 'react-router-dom';
+
+interface ExerciseProgression {
+ date: string;
+ max_weight: number;
+ avg_rpe: number;
+ total_volume: number;
+}
+
+interface PersonalRecord {
+ exercise_name: string;
+ max_weight: number;
+ date: string;
+}
+
+interface WeeklyVolume {
+ week: string;
+ sessions: number;
+ total_volume: number;
+}
+
+interface AnalyticsData {
+ weekly_sessions: WeeklyVolume[];
+ exercise_progressions: Record;
+ personal_records: PersonalRecord[];
+ avg_rpe_trend: { date: string; avg_rpe: number; session_title: string }[];
+}
+
+const KettlebellAnalytics = () => {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [selectedExercise, setSelectedExercise] = useState('');
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const fetchAnalytics = async () => {
+ try {
+ const res = await client.get('/kettlebell/analytics');
+ setData(res.data);
+ const exercises = Object.keys(res.data.exercise_progressions);
+ if (exercises.length > 0) setSelectedExercise(exercises[0]);
+ } catch (err) {
+ console.error(err);
+ toast.error('Failed to load analytics');
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchAnalytics();
+ }, []);
+
+ if (loading) {
+ return Loading analytics...
;
+ }
+
+ if (!data) return null;
+
+ const hasData = data.weekly_sessions.length > 0;
+ const exerciseNames = Object.keys(data.exercise_progressions);
+ const progressionData = selectedExercise ? data.exercise_progressions[selectedExercise] ?? [] : [];
+
+ return (
+
+
+
+
+
Workout Analytics
+
Performance insights from your completed sessions.
+
+
+
+ {!hasData ? (
+
+
+
No completed sessions yet
+
Complete some kettlebell sessions to see your analytics here.
+
+ ) : (
+ <>
+ {/* Summary Stats */}
+
+
+
{data.weekly_sessions.reduce((a, w) => a + w.sessions, 0)}
+
Total Sessions
+
+
+
{data.personal_records.length}
+
Exercises Tracked
+
+
+
+ {data.avg_rpe_trend.length > 0
+ ? (data.avg_rpe_trend.reduce((a, r) => a + r.avg_rpe, 0) / data.avg_rpe_trend.length).toFixed(1)
+ : '--'}
+
+
Avg RPE
+
+
+
+ {Math.round(data.weekly_sessions.reduce((a, w) => a + w.total_volume, 0))} kg
+
+
Total Volume
+
+
+
+
+ {/* Sessions per Week */}
+
+
+ Sessions Per Week
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* RPE Trend */}
+
+
+ Avg RPE Trend
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Exercise Weight Progression */}
+ {exerciseNames.length > 0 && (
+
+
+
+ Weight Progression
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Personal Records Table */}
+ {data.personal_records.length > 0 && (
+
+
+ Personal Records
+
+
+
+
+
+ | Exercise |
+ Max Weight |
+ Date |
+
+
+
+ {data.personal_records.map((pr, i) => (
+
+ |
+ {i === 0 && }
+ {pr.exercise_name}
+ |
+ {pr.max_weight} kg |
+ {pr.date} |
+
+ ))}
+
+
+
+
+ )}
+ >
+ )}
+
+ );
+};
+
+export default KettlebellAnalytics;
diff --git a/frontend/src/pages/Nutrition.tsx b/frontend/src/pages/Nutrition.tsx
index 169cb17..6f176ec 100644
--- a/frontend/src/pages/Nutrition.tsx
+++ b/frontend/src/pages/Nutrition.tsx
@@ -1,4 +1,5 @@
-import { useState, useRef, FormEvent, ChangeEvent } from 'react';
+import { useState, useRef, ChangeEvent } from 'react';
+import { toast } from 'sonner';
import client from '../api/client';
import {
Upload,
@@ -14,7 +15,7 @@ import {
Info,
Utensils
} from 'lucide-react';
-import { Field, Label } from '../components/catalyst/fieldset';
+import { Field } from '../components/catalyst/fieldset';
import { Textarea } from '../components/catalyst/textarea';
import { Button } from '../components/catalyst/button';
import { Heading } from '../components/catalyst/heading';
@@ -78,7 +79,7 @@ const Nutrition = () => {
setShowReasoning(false);
} catch (error) {
console.error(error);
- alert('Failed to analyze. Please try again.');
+ toast.error('Failed to analyze. Please try again.');
} finally {
setLoading(false);
}
@@ -91,10 +92,10 @@ const Nutrition = () => {
setAnalysis(null);
setDescription('');
clearFile();
- alert('Meal logged successfully!');
+ toast.success('Meal logged successfully!');
} catch (error) {
console.error(error);
- alert('Failed to save log.');
+ toast.error('Failed to save log.');
}
};
diff --git a/frontend/src/pages/Plans.tsx b/frontend/src/pages/Plans.tsx
index e9d2f48..dae2007 100644
--- a/frontend/src/pages/Plans.tsx
+++ b/frontend/src/pages/Plans.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect, FormEvent } from 'react';
+import { toast } from 'sonner';
import client from '../api/client';
import { Field, Label } from '../components/catalyst/fieldset';
import { Input } from '../components/catalyst/input';
@@ -52,7 +53,7 @@ const Plans = () => {
setUserDetails('');
} catch (error) {
console.error(error);
- alert('Failed to generate plan');
+ toast.error('Failed to generate plan');
} finally {
setLoading(false);
}
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 03bba44..10007aa 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -1,11 +1,13 @@
import { useState, useEffect, useContext, FormEvent, ChangeEvent } from 'react';
import { AuthContext } from '../context/AuthContext';
-import { User, Ruler, Weight, Activity, Save, AlertCircle } from 'lucide-react';
+import { toast } from 'sonner';
+import { User, Ruler, Weight, Activity, Save, Target, Bell } from 'lucide-react';
import { Field, Label, Fieldset, Legend } from '../components/catalyst/fieldset';
import { Input } from '../components/catalyst/input';
import { Select } from '../components/catalyst/select';
import { Button } from '../components/catalyst/button';
import { Heading } from '../components/catalyst/heading';
+import { getVapidPublicKey, subscribePush, unsubscribePush, sendTestNotification, urlBase64ToUint8Array } from '../api/push';
interface FormData {
firstname: string;
@@ -22,9 +24,21 @@ interface FormData {
const Profile = () => {
const { user, updateUser } = useContext(AuthContext);
const [loading, setLoading] = useState(false);
- const [message, setMessage] = useState({ type: '', text: '' });
+ const [targets, setTargets] = useState({
+ target_calories: '',
+ target_protein: '',
+ target_carbs: '',
+ target_fat: '',
+ });
const [unitSystem, setUnitSystem] = useState('metric'); // 'metric' or 'imperial'
+
+ // Push notification state
+ const [notifEnabled, setNotifEnabled] = useState(false);
+ const [notifLoading, setNotifLoading] = useState(false);
+ const [reminderHour, setReminderHour] = useState(9);
+ const [reminderMinute, setReminderMinute] = useState(0);
+ const [currentSub, setCurrentSub] = useState(null);
const [formData, setFormData] = useState({
firstname: '',
lastname: '',
@@ -41,6 +55,12 @@ const Profile = () => {
if (user) {
const prefs = user.unit_preference || 'metric';
setUnitSystem(prefs);
+ setTargets({
+ target_calories: user.target_calories?.toString() || '',
+ target_protein: user.target_protein?.toString() || '',
+ target_carbs: user.target_carbs?.toString() || '',
+ target_fat: user.target_fat?.toString() || '',
+ });
const h_cm = user.height || '';
const w_kg = user.weight || '';
@@ -70,6 +90,86 @@ const Profile = () => {
}
}, [user]);
+ useEffect(() => {
+ if ('serviceWorker' in navigator && 'PushManager' in window) {
+ navigator.serviceWorker.ready
+ .then((reg) => reg.pushManager.getSubscription())
+ .then((sub) => {
+ if (sub) {
+ setCurrentSub(sub);
+ setNotifEnabled(true);
+ }
+ });
+ }
+ }, []);
+
+ const handleEnableNotifications = async () => {
+ setNotifLoading(true);
+ try {
+ const permission = await Notification.requestPermission();
+ if (permission !== 'granted') {
+ toast.error('Notification permission denied');
+ return;
+ }
+ const publicKey = await getVapidPublicKey();
+ const reg = await navigator.serviceWorker.ready;
+ const sub = await reg.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(publicKey),
+ });
+ const json = sub.toJSON();
+ await subscribePush({
+ endpoint: json.endpoint!,
+ keys: { p256dh: json.keys!.p256dh, auth: json.keys!.auth },
+ reminder_hour: reminderHour,
+ reminder_minute: reminderMinute,
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ });
+ setCurrentSub(sub);
+ setNotifEnabled(true);
+ toast.success('Notifications enabled!');
+ } catch (err) {
+ console.error('Push subscribe error:', err);
+ toast.error('Failed to enable notifications');
+ } finally {
+ setNotifLoading(false);
+ }
+ };
+
+ const handleDisableNotifications = async () => {
+ setNotifLoading(true);
+ try {
+ if (currentSub) {
+ await unsubscribePush(currentSub.endpoint);
+ await currentSub.unsubscribe();
+ }
+ setCurrentSub(null);
+ setNotifEnabled(false);
+ toast.success('Notifications disabled');
+ } catch {
+ toast.error('Failed to disable notifications');
+ } finally {
+ setNotifLoading(false);
+ }
+ };
+
+ const handleUpdateReminderTime = async () => {
+ if (!currentSub) return;
+ try {
+ const json = currentSub.toJSON();
+ await subscribePush({
+ endpoint: json.endpoint!,
+ keys: { p256dh: json.keys!.p256dh, auth: json.keys!.auth },
+ reminder_hour: reminderHour,
+ reminder_minute: reminderMinute,
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ });
+ toast.success('Reminder time updated');
+ } catch {
+ toast.error('Failed to update reminder time');
+ }
+ };
+
const handleUnitChange = (system: string) => {
setUnitSystem(system);
};
@@ -108,9 +208,8 @@ const Profile = () => {
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setLoading(true);
- setMessage({ type: '', text: '' });
- const payload = {
+ const payload: Record = {
firstname: formData.firstname,
lastname: formData.lastname,
age: parseInt(formData.age),
@@ -119,12 +218,16 @@ const Profile = () => {
height: parseFloat(String(formData.height_cm)),
weight: parseFloat(String(formData.weight_kg)),
};
+ if (targets.target_calories) payload.target_calories = parseFloat(targets.target_calories);
+ if (targets.target_protein) payload.target_protein = parseFloat(targets.target_protein);
+ if (targets.target_carbs) payload.target_carbs = parseFloat(targets.target_carbs);
+ if (targets.target_fat) payload.target_fat = parseFloat(targets.target_fat);
const success = await updateUser(payload);
if (success) {
- setMessage({ type: 'success', text: 'Profile updated successfully!' });
+ toast.success('Profile updated successfully!');
} else {
- setMessage({ type: 'error', text: 'Failed to update profile.' });
+ toast.error('Failed to update profile.');
}
setLoading(false);
};
@@ -137,14 +240,6 @@ const Profile = () => {
- {message.text && (
-
- )}
-
+
+
+ {/* Nutrition Targets */}
+
+
+
+
+ {/* Notifications */}
+ {'serviceWorker' in navigator && 'PushManager' in window && (
+
+ )}
+