diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8ebeb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Caché de Python +__pycache__/ +*.py[cod] +*$py.class + +# Variables de entorno +.env +.env* + + +# Configuraciones del editor +.vscode/ +.idea/ \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3639ea9 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/api/database/mongodb.py b/backend/api/database/mongodb.py new file mode 100644 index 0000000..fbe5848 --- /dev/null +++ b/backend/api/database/mongodb.py @@ -0,0 +1,9 @@ +from motor.motor_asyncio import AsyncIOMotorClient + +MONGO_URL = "mongodb://mongo:27017" +DB_NAME = "deckofcards" + +client = AsyncIOMotorClient(MONGO_URL) +db = client[DB_NAME] + +users_collection = db["users"] diff --git a/backend/api/main.py b/backend/api/main.py new file mode 100644 index 0000000..a5b71b5 --- /dev/null +++ b/backend/api/main.py @@ -0,0 +1,43 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager +from api.database.mongodb import db + +# Routers +from api.routers.test_mongo import router as test_mongo_router +from api.routers.value_function import router as value_router +from api.routers.docmf_build import router as docmf_build_router +from api.routers.docmf_evaluate import router as docmf_eval_router +from api.routers.docmf_simple_validation import router as simple_validation_router +from api.routers.docmf_validation import router as validation_router +from api.routers.auth import router as auth_router +from api.routers.history import router as history_router +from api.routers.test_mongo import router as test_mongo_router +from api.routers.docit2mf_build import router as docit2mf_router +from api.routers.google_auth import router as google_auth_router + +@asynccontextmanager +async def lifespan(app: FastAPI): + yield + +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(test_mongo_router, prefix="/api") +app.include_router(value_router, prefix="/api/criteria/doc") +app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf") +app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf") +app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf") +app.include_router(validation_router, prefix="/api/criteria/doc-mf") +app.include_router(test_mongo_router, prefix="/api") +app.include_router(auth_router, prefix="/api") +app.include_router(history_router, prefix="/api") +app.include_router(docit2mf_router, prefix="/api") +app.include_router(google_auth_router, prefix="/api") \ No newline at end of file diff --git a/backend/api/models/docit2mf_models.py b/backend/api/models/docit2mf_models.py new file mode 100644 index 0000000..cd33af4 --- /dev/null +++ b/backend/api/models/docit2mf_models.py @@ -0,0 +1,65 @@ +from pydantic import BaseModel, Field, field_validator, ValidationInfo +from typing import List, Tuple, Union + +BlankCardInput = Union[int, Tuple[int, int], List[int]] + +class DoCIT2MFRequest(BaseModel): + term: str + core: tuple[float, float] = Field(..., description="Núcleo del conjunto difuso: [a, b]") + support: tuple[float, float] = Field(..., description="Soporte del conjunto difuso: [c, d]") + + left_nodes_x: List[float] + left_blank_cards: List[BlankCardInput] + + right_nodes_x: List[float] + right_blank_cards: List[BlankCardInput] + + @field_validator("term") + def term_not_empty(cls, v): + if not v.strip(): + raise ValueError("El término no puede estar vacío.") + return v + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("el 'Inicio del Núcleo' debe ser menor o igual al 'Fin del Núcleo'.") + return v + + @field_validator("support") + def support_valid(cls, v, info: ValidationInfo): + c, d = v + if c > d: + raise ValueError("el 'Inicio del Soporte' debe ser menor o igual al 'Fin del Soporte'.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a and b <= d): + raise ValueError("los valores del 'Núcleo' deben estar estrictamente dentro del 'Soporte'.") + return v + + @field_validator("left_blank_cards", "right_blank_cards") + def validate_cards(cls, v): + for item in v: + # Caso 1: entero + if isinstance(item, int): + if item < 0: + raise ValueError("Las cartas no pueden ser negativas.") + # Caso 2: lista o tupla [min,max] + elif isinstance(item, (list, tuple)): + if len(item) != 2: + raise ValueError("Los intervalos deben ser [min, max].") + lo, hi = item + if lo < 0 or hi < 0: + raise ValueError("Las cartas no pueden ser negativas.") + if lo > hi: + raise ValueError("Debe cumplirse min <= max.") + else: + raise ValueError("Formato inválido para cartas blancas.") + return v + + +class DoCIT2MFMultiRequest(BaseModel): + levels: List[DoCIT2MFRequest] diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py new file mode 100644 index 0000000..8239d45 --- /dev/null +++ b/backend/api/models/docmf_models.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class DoCMFRequest(BaseModel): + term: str + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes_x: List[float] + left_blank_cards: List[int] + right_nodes_x: List[float] + right_blank_cards: List[int] + + @field_validator("term") + def term_not_empty(cls, v): + if not v.strip(): + raise ValueError("El término no puede estar vacío.") + return v + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("El núcleo debe cumplir a <= b.") + return v + + @field_validator("support") + def support_valid(cls, v, info): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a <= b <= d): + raise ValueError("El núcleo debe estar dentro del soporte.") + return v + + @field_validator("left_blank_cards", "right_blank_cards") + def cards_valid(cls, v): + if any(c < 0 for c in v): + raise ValueError("Las cartas no pueden ser negativas.") + return v + + +class DoCMFMultiRequest(BaseModel): + levels: List[DoCMFRequest] + diff --git a/backend/api/models/docmf_simple_validation_models.py b/backend/api/models/docmf_simple_validation_models.py new file mode 100644 index 0000000..246e5d5 --- /dev/null +++ b/backend/api/models/docmf_simple_validation_models.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class SimpleLevelDefinition(BaseModel): + core: Tuple[float, float] + support: Tuple[float, float] + + @field_validator("core") + def validate_core(cls, v): + a, b = v + if a >= b: + raise ValueError("El núcleo debe cumplir a < b.") + return v + + @field_validator("support") + def validate_support(cls, v): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + return v + + +class SimpleValidationRequest(BaseModel): + levels: List[SimpleLevelDefinition] diff --git a/backend/api/models/docmf_validation_models.py b/backend/api/models/docmf_validation_models.py new file mode 100644 index 0000000..e62907d --- /dev/null +++ b/backend/api/models/docmf_validation_models.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class LevelDefinition(BaseModel): + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes: List[Tuple[float, float]] + right_nodes: List[Tuple[float, float]] + + @field_validator("core") + def validate_core(cls, v): + a, b = v + if a >= b: + raise ValueError("El núcleo debe cumplir a < b.") + return v + + @field_validator("support") + def validate_support(cls, v): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + return v + + @field_validator("left_nodes", "right_nodes") + def validate_nodes(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 nodos.") + xs = [p[0] for p in v] + if xs != sorted(xs): + raise ValueError("Los nodos deben estar ordenados por x.") + if len(xs) != len(set(xs)): + raise ValueError("Los nodos no pueden tener valores x duplicados.") + return v + + +class ValidationRequest(BaseModel): + levels: List[LevelDefinition] diff --git a/backend/api/models/evaluation_models.py b/backend/api/models/evaluation_models.py new file mode 100644 index 0000000..ca18221 --- /dev/null +++ b/backend/api/models/evaluation_models.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class EvaluationRequest(BaseModel): + x: float + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes: List[Tuple[float, float]] + right_nodes: List[Tuple[float, float]] + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("El núcleo debe cumplir a <= b.") + return v + + @field_validator("support") + def support_valid(cls, v, info): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a < b <= d): + raise ValueError("El núcleo debe estar dentro del soporte.") + return v + + @field_validator("left_nodes", "right_nodes") + def nodes_valid(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 nodos.") + return v diff --git a/backend/api/models/user_models.py b/backend/api/models/user_models.py new file mode 100644 index 0000000..3049a70 --- /dev/null +++ b/backend/api/models/user_models.py @@ -0,0 +1,54 @@ +from typing import List, Optional, Union +from pydantic import BaseModel, EmailStr, Field +from datetime import datetime + +# MODELOS DE FUNCIONES DIFUSAS + +class FuzzyTerm(BaseModel): + term: str + core: List[float] + support: List[float] + left_nodes: List[List[float]] + right_nodes: List[List[float]] + + +class IT2FuzzyTerm(BaseModel): + term: str + lower: FuzzyTerm + upper: FuzzyTerm + + +# HISTORIAL + +class HistoryItem(BaseModel): + id: Optional[str] = Field(default=None, alias="_id") + name: str + created_at: datetime + results: List[Union[FuzzyTerm, IT2FuzzyTerm]] + + +class HistoryCreateRequest(BaseModel): + name: str + results: List[Union[FuzzyTerm, IT2FuzzyTerm]] + + +# USUARIOS + +class UserCreate(BaseModel): + username: str + email: EmailStr + password: str + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class UserInDB(BaseModel): + id: Optional[str] = Field(default=None, alias="_id") + username: str + email: EmailStr + password_hash: str + token: Optional[str] = None + history: List[HistoryItem] = [] diff --git a/backend/api/models/value_function_models.py b/backend/api/models/value_function_models.py new file mode 100644 index 0000000..66aaed5 --- /dev/null +++ b/backend/api/models/value_function_models.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, field_validator +from typing import List, Dict + +class ValueFunctionRequest(BaseModel): + criterion_name: str + levels: List[str] + blank_cards: List[int] + references: Dict[str, float] + + @field_validator("criterion_name") + def name_not_empty(cls, v): + if not v.strip(): + raise ValueError("El nombre no puede estar vacío.") + return v + + @field_validator("levels") + def levels_not_empty(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 niveles.") + return v + + @field_validator("blank_cards") + def cards_valid(cls, v, info): + if any(c < 0 for c in v): + raise ValueError("Las cartas no pueden ser negativas.") + + levels = info.data.get("levels") + if levels and len(v) != len(levels) - 1: + raise ValueError("Debe haber uno menos de número de cartas blancas que de niveles.") + return v + + @field_validator("references") + def refs_valid(cls, v): + if len(v) != 2: + raise ValueError("Debe haber 2 referencias.") + return v diff --git a/backend/api/routers/auth.py b/backend/api/routers/auth.py new file mode 100644 index 0000000..e0d6c68 --- /dev/null +++ b/backend/api/routers/auth.py @@ -0,0 +1,116 @@ +from fastapi import APIRouter, HTTPException, status +from api.database.mongodb import users_collection +from api.models.user_models import UserCreate, UserLogin +from api.utils.security import hash_password, verify_password, generate_token +from bson import ObjectId +from fastapi import APIRouter, HTTPException, status, Depends +from api.utils.security import ( + hash_password, + verify_password, + generate_token, + get_current_user, +) + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/register") +async def register_user(user: UserCreate): + existing_username = await users_collection.find_one({"username": user.username}) + if existing_username: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="El nombre de usuario ya está en uso", + ) + + existing_email = await users_collection.find_one({"email": user.email}) + if existing_email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="El email ya está registrado", + ) + + token = generate_token() + + user_doc = { + "username": user.username, + "email": user.email, + "password_hash": hash_password(user.password), + "token": token, + "history": [], + } + + result = await users_collection.insert_one(user_doc) + + return { + "message": "Usuario registrado correctamente", + "user_id": str(result.inserted_id), + "token": token, + } + + +@router.post("/login") +async def login_user(credentials: UserLogin): + user = await users_collection.find_one({"email": credentials.email}) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Credenciales inválidas", + ) + + if not verify_password(credentials.password, user["password_hash"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Credenciales inválidas", + ) + + new_token = generate_token() + + await users_collection.update_one( + {"_id": user["_id"]}, + {"$set": {"token": new_token}} + ) + + return { + "message": "Login correcto", + "user_id": str(user["_id"]), + "username": user["username"], + "token": new_token, + } + + +@router.post("/logout/{user_id}") +async def logout_user(user_id: str): + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado", + ) + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$set": {"token": None}} + ) + + return {"message": "Sesión cerrada correctamente"} + + +@router.post("/logout") +async def logout_user(current_user: dict = Depends(get_current_user)): + user_id = current_user["_id"] + + await users_collection.update_one( + {"_id": user_id}, + {"$set": {"token": None}}, + ) + + return {"message": "Sesión cerrada correctamente"} + +@router.get("/me") +async def get_me(current_user: dict = Depends(get_current_user)): + return { + "user_id": str(current_user["_id"]), + "username": current_user["username"], + "email": current_user["email"], + } diff --git a/backend/api/routers/docit2mf_build.py b/backend/api/routers/docit2mf_build.py new file mode 100644 index 0000000..ba604b3 --- /dev/null +++ b/backend/api/routers/docit2mf_build.py @@ -0,0 +1,28 @@ +import logging +from fastapi import APIRouter, HTTPException, Request +from slowapi import Limiter +from slowapi.util import get_remote_address +from api.models.docit2mf_models import DoCIT2MFMultiRequest +from api.services.docit2mf_build_service import build_it2mf_from_level + +router = APIRouter(prefix="/criteria", tags=["criteria"]) +limiter = Limiter(key_func=get_remote_address) +logger = logging.getLogger(__name__) + + +@router.post("/doc-it2mf/build") +@limiter.limit("10/minute") +async def build_doc_it2mf(request: Request, body: DoCIT2MFMultiRequest): + results = [] + + try: + for level in body.levels: + results.append(build_it2mf_from_level(level)) + except ValueError as e: + logger.warning(f"Validation error in doc-it2mf/build: {str(e)}") + raise HTTPException(status_code=400, detail="Invalid input data") + except Exception as e: + logger.error(f"Unexpected error in doc-it2mf/build: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + return {"levels": results} \ No newline at end of file diff --git a/backend/api/routers/docmf_build.py b/backend/api/routers/docmf_build.py new file mode 100644 index 0000000..24467c4 --- /dev/null +++ b/backend/api/routers/docmf_build.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, HTTPException +from api.models.docmf_models import DoCMFMultiRequest +from api.services.docmf_build_service import build_docmf_multi + +router = APIRouter() + +@router.post("/build") +def build(request: DoCMFMultiRequest): + try: + return build_docmf_multi(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routers/docmf_evaluate.py b/backend/api/routers/docmf_evaluate.py new file mode 100644 index 0000000..3024fe8 --- /dev/null +++ b/backend/api/routers/docmf_evaluate.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, HTTPException +from api.models.evaluation_models import EvaluationRequest +from api.services.docmf_evaluate_service import evaluate_docmf + +router = APIRouter() + +@router.post("/evaluate") +def evaluate(request: EvaluationRequest): + try: + return evaluate_docmf(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routers/docmf_simple_validation.py b/backend/api/routers/docmf_simple_validation.py new file mode 100644 index 0000000..33c4d73 --- /dev/null +++ b/backend/api/routers/docmf_simple_validation.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, HTTPException +from api.models.docmf_simple_validation_models import SimpleValidationRequest +from api.services.docmf_simple_validation_service import validate_simple_levels + +router = APIRouter() + +@router.post("/validate-simple") +def validate_simple_docmf(request: SimpleValidationRequest): + results = validate_simple_levels(request.levels) + invalid = [r for r in results if not r["valid"]] + + if len(request.levels) == 1: + if invalid: + raise HTTPException( + status_code=400, + detail={ + "message": "El nivel es incorrecto.", + "errors": invalid[0]["errors"] + } + ) + return { + "message": "El nivel es correcto.", + "details": results[0] + } + + if invalid: + return { + "message": "Validación completada.", + "valid_levels": [r for r in results if r["valid"]], + "invalid_levels": invalid + } + + return { + "message": "Todos los niveles son correctos.", + "results": results + } diff --git a/backend/api/routers/docmf_validation.py b/backend/api/routers/docmf_validation.py new file mode 100644 index 0000000..92eda8d --- /dev/null +++ b/backend/api/routers/docmf_validation.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, HTTPException +from api.models.docmf_validation_models import ValidationRequest +from api.services.docmf_validation_service import validate_levels + +router = APIRouter() + +@router.post("/validate") +def validate_docmf_levels(request: ValidationRequest): + results = validate_levels(request.levels) + + invalid = [r for r in results if not r["valid"]] + + if len(request.levels) == 1: + # Caso de un solo nivel + if invalid: + raise HTTPException( + status_code=400, + detail={ + "message": "El nivel es incorrecto.", + "errors": invalid[0]["errors"] + } + ) + return { + "message": "El nivel es correcto.", + "details": results[0] + } + + # Caso de varios niveles + if invalid: + return { + "message": "Validación completada.", + "valid_levels": [r for r in results if r["valid"]], + "invalid_levels": invalid + } + + return { + "message": "Todos los niveles son correctos.", + "results": results + } diff --git a/backend/api/routers/google_auth.py b/backend/api/routers/google_auth.py new file mode 100644 index 0000000..2769868 --- /dev/null +++ b/backend/api/routers/google_auth.py @@ -0,0 +1,100 @@ +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import RedirectResponse +from datetime import datetime, timedelta +import httpx +import os +import jwt + +from api.database.mongodb import users_collection + +router = APIRouter(prefix="/auth/google", tags=["auth"]) + +GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") +REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI") +SECRET_KEY = os.getenv("SECRET_KEY") + +@router.get("/login") +async def google_login(): + google_auth_url = ( + "https://accounts.google.com/o/oauth2/auth" + "?response_type=code" + f"&client_id={GOOGLE_CLIENT_ID}" + f"&redirect_uri={REDIRECT_URI}" + "&scope=openid%20email%20profile" + "&access_type=offline" + "&prompt=consent" + ) + + return RedirectResponse(google_auth_url) + +@router.get("/callback") +async def google_callback(request: Request): + + code = request.query_params.get("code") + if not code: + raise HTTPException(status_code=400, detail="Missing code parameter") + + token_url = "https://oauth2.googleapis.com/token" + + data = { + "code": code, + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "redirect_uri": REDIRECT_URI, + "grant_type": "authorization_code", + } + + async with httpx.AsyncClient() as client: + token_response = await client.post(token_url, data=data) + token_json = token_response.json() + + if "access_token" not in token_json: + raise HTTPException(status_code=400, detail=token_json) + + access_token = token_json["access_token"] + + async with httpx.AsyncClient() as client: + userinfo = await client.get( + "https://www.googleapis.com/oauth2/v2/userinfo", + headers={"Authorization": f"Bearer {access_token}"} + ) + + user_data = userinfo.json() + + google_id = user_data["id"] + email = user_data["email"] + name = user_data.get("name", "Usuario") + + user = await users_collection.find_one({"email": email}) + + if not user: + new_user = { + "username": name, + "email": email, + "password_hash": None, + "google_id": google_id, + "history": [], + } + result = await users_collection.insert_one(new_user) + user_id = result.inserted_id + else: + user_id = user["_id"] + + token = jwt.encode( + { + "sub": str(user_id), + "email": email, + "name": name, + "exp": datetime.utcnow() + timedelta(hours=24) + }, + SECRET_KEY, + algorithm="HS256" + ) + + await users_collection.update_one( + {"_id": user_id}, + {"$set": {"token": token}} + ) + + return RedirectResponse(f"http://localhost:5173/login?token={token}") \ No newline at end of file diff --git a/backend/api/routers/history.py b/backend/api/routers/history.py new file mode 100644 index 0000000..e3c65e6 --- /dev/null +++ b/backend/api/routers/history.py @@ -0,0 +1,119 @@ +# api/routers/history.py + +from fastapi import APIRouter, HTTPException, status, Depends +from datetime import datetime +from bson import ObjectId + +from api.database.mongodb import users_collection +from api.models.user_models import FuzzyTerm, IT2FuzzyTerm, HistoryCreateRequest +from api.utils.security import get_current_user + +router = APIRouter(prefix="/history", tags=["history"]) + +@router.post("/add") +async def add_history_item( + data: HistoryCreateRequest, + current_user: dict = Depends(get_current_user), +): + user_id = current_user["_id"] + + history_item_id = ObjectId() + history_item = { + "_id": history_item_id, + "name": data.name, + "created_at": datetime.utcnow(), + "results": [r.dict() for r in data.results], + } + + await users_collection.update_one( + {"_id": user_id}, + {"$push": {"history": history_item}}, + ) + + return { + "message": "Elemento añadido al historial", + "history_item_id": str(history_item_id), + } + +@router.delete("/delete/{history_item_id}") +async def delete_history_item( + history_item_id: str, + current_user: dict = Depends(get_current_user), +): + user_id = current_user["_id"] + + result = await users_collection.update_one( + {"_id": user_id}, + {"$pull": {"history": {"_id": ObjectId(history_item_id)}}}, + ) + + if result.modified_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Elemento de historial no encontrado", + ) + + return {"message": "Elemento eliminado del historial"} + + +@router.get("/all") +async def get_all_histories(): + users = await users_collection.find().to_list(None) + + all_histories = [] + + for user in users: + user_id = str(user["_id"]) + username = user.get("username", "unknown") + history = user.get("history", []) + + for item in history: + all_histories.append({ + "user_id": user_id, + "username": username, + "history_item": { + "_id": str(item["_id"]), + "name": item["name"], + "created_at": item["created_at"], + "results": item["results"] + } + }) + + all_histories_sorted = sorted( + all_histories, + key=lambda h: h["history_item"]["created_at"] + ) + + return {"count": len(all_histories_sorted), "histories": all_histories_sorted} + +@router.get("/user") +async def get_user_histories(current_user: dict = Depends(get_current_user)): + user_id = current_user["_id"] + + user = await users_collection.find_one({"_id": user_id}) + + if not user: + raise HTTPException( + status_code=404, + detail="Usuario no encontrado." + ) + + history = user.get("history", []) + + history_sorted = sorted(history, key=lambda h: h["created_at"]) + + formatted_history = [] + for item in history_sorted: + formatted_history.append({ + "_id": str(item["_id"]), + "name": item["name"], + "created_at": item["created_at"], + "results": item["results"] + }) + + return { + "user_id": str(user_id), + "username": user.get("username", "unknown"), + "count": len(formatted_history), + "history": formatted_history + } diff --git a/backend/api/routers/test_mongo.py b/backend/api/routers/test_mongo.py new file mode 100644 index 0000000..43dd15d --- /dev/null +++ b/backend/api/routers/test_mongo.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from api.database.mongodb import db + +router = APIRouter() + +@router.get("/test-mongo") +async def test_mongo(): + try: + await db.command("ping") + return {"status": "ok", "message": "Conexión a MongoDB correcta"} + except Exception as e: + return {"status": "error", "message": str(e)} diff --git a/backend/api/routers/value_function.py b/backend/api/routers/value_function.py new file mode 100644 index 0000000..0dfc5a4 --- /dev/null +++ b/backend/api/routers/value_function.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, HTTPException +from api.models.value_function_models import ValueFunctionRequest +from api.services.value_function_service import compute_value_function, compute_points + +router = APIRouter() + +@router.post("/value-function") +def value_function(request: ValueFunctionRequest): + try: + return compute_value_function(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/value-function/points") +def value_function_points(request: ValueFunctionRequest): + try: + return compute_points(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/services/docit2mf_build_service.py b/backend/api/services/docit2mf_build_service.py new file mode 100644 index 0000000..c5e7aee --- /dev/null +++ b/backend/api/services/docit2mf_build_service.py @@ -0,0 +1,103 @@ +from typing import List, Union +from api.models.docit2mf_models import DoCIT2MFRequest +from api.models.docmf_models import DoCMFRequest +from api.services.docmf_build_service import build_doc_mf_level + + +def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> List[int]: + """ + Devuelve una lista de enteros: + - Si el valor es un entero → se usa tal cual para LMF y UMF + - Si es un intervalo [min,max] → se usa min o max según mode + """ + result = [] + for item in values: + if isinstance(item, int): + result.append(item) + else: + lo, hi = item + result.append(lo if mode == "min" else hi) + return result + + +def _sort_nodes(nodes): + """Ordena los nodos por su coordenada X.""" + return sorted([list(p) for p in nodes], key=lambda p: p[0]) + + +def _enforce_upper_ge_lower(lower, upper): + """ + Garantiza que la UMF (upper) nunca quede por debajo de la LMF (lower). + Ajusta los valores de pertenencia si es necesario. + """ + for i in range(len(lower["left_nodes"])): + ly = lower["left_nodes"][i][1] + uy = upper["left_nodes"][i][1] + upper["left_nodes"][i][1] = max(uy, ly) + + for i in range(len(lower["right_nodes"])): + ly = lower["right_nodes"][i][1] + uy = upper["right_nodes"][i][1] + upper["right_nodes"][i][1] = max(uy, ly) + + return upper + + +def build_it2mf_from_level(level: DoCIT2MFRequest): + """ + Construye una función IT2MF a partir de un nivel con intervalos de cartas blancas. + Devuelve: + { + "term": ..., + "lower": {...}, + "upper": {...} + } + """ + + left_min = _extract_bounds(level.left_blank_cards, "min") + right_min = _extract_bounds(level.right_blank_cards, "min") + + lower_level = DoCMFRequest( + term=level.term, + core=level.core, + support=level.support, + left_nodes_x=level.left_nodes_x, + left_blank_cards=left_min, + right_nodes_x=level.right_nodes_x, + right_blank_cards=right_min, + ) + + lower = build_doc_mf_level(lower_level) + if hasattr(lower, "model_dump"): + lower = lower.model_dump() + + lower["left_nodes"] = _sort_nodes(lower["left_nodes"]) + lower["right_nodes"] = _sort_nodes(lower["right_nodes"]) + + left_max = _extract_bounds(level.left_blank_cards, "max") + right_max = _extract_bounds(level.right_blank_cards, "max") + + upper_level = DoCMFRequest( + term=level.term, + core=level.core, + support=level.support, + left_nodes_x=level.left_nodes_x, + left_blank_cards=left_max, + right_nodes_x=level.right_nodes_x, + right_blank_cards=right_max, + ) + + upper = build_doc_mf_level(upper_level) + if hasattr(upper, "model_dump"): + upper = upper.model_dump() + + upper["left_nodes"] = _sort_nodes(upper["left_nodes"]) + upper["right_nodes"] = _sort_nodes(upper["right_nodes"]) + + upper = _enforce_upper_ge_lower(lower, upper) + + return { + "term": level.term, + "lower": lower, + "upper": upper + } \ No newline at end of file diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py new file mode 100644 index 0000000..2f11197 --- /dev/null +++ b/backend/api/services/docmf_build_service.py @@ -0,0 +1,63 @@ +from api.models.docmf_models import DoCMFRequest +from api.models.user_models import FuzzyTerm + + +def build_single_docmf(request: DoCMFRequest): + a, b = request.core + c, d = request.support + + TL = sum(e + 1 for e in request.left_blank_cards) + YL = 1 / TL + left_nodes = [] + acc = 0 + + for i, x in enumerate(request.left_nodes_x): + if i == 0: + left_nodes.append((x, 0.0)) + else: + acc += request.left_blank_cards[i - 1] + 1 + left_nodes.append((x, round(acc * YL, 4))) + + TR = sum(e + 1 for e in request.right_blank_cards) + YR = 1 / TR + right_nodes = [] + acc = 0 + + for i, x in enumerate(request.right_nodes_x): + if i == 0: + right_nodes.append((x, 1.0)) + else: + acc += request.right_blank_cards[i - 1] + 1 + right_nodes.append((x, round(1 - acc * YR, 4))) + + return { + "term": request.term, + "core": request.core, + "support": request.support, + "left_nodes": left_nodes, + "right_nodes": right_nodes + } + + +def build_docmf_multi(request): + results = [] + for level in request.levels: + result = build_single_docmf(level) + results.append(result) + return {"results": results} + + +def build_doc_mf_level(level: DoCMFRequest) -> FuzzyTerm: + """ + Adaptador para reutilizar build_single_docmf con el modelo DoCMFRequest. + Devuelve un FuzzyTerm, que es lo que espera el sistema IT2MF. + """ + result = build_single_docmf(level) + + return FuzzyTerm( + term=result["term"], + core=list(result["core"]), + support=list(result["support"]), + left_nodes=[list(p) for p in result["left_nodes"]], + right_nodes=[list(p) for p in result["right_nodes"]], + ) diff --git a/backend/api/services/docmf_evaluate_service.py b/backend/api/services/docmf_evaluate_service.py new file mode 100644 index 0000000..5b31eb0 --- /dev/null +++ b/backend/api/services/docmf_evaluate_service.py @@ -0,0 +1,22 @@ +from api.utils.interpolation import linear_interpolation + +def evaluate_docmf(request): + x = request.x + a, b = request.core + c, d = request.support + + if x < c or x > d: + return {"membership": 0.0, "explanation": "Fuera del soporte."} + + if a <= x <= b: + return {"membership": 1.0, "explanation": "Dentro del núcleo."} + + if c <= x < a: + mu = linear_interpolation(x, request.left_nodes) + return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.left_nodes} del lado izquierdo."} + + if b < x <= d: + mu = linear_interpolation(x, request.right_nodes) + return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.right_nodes} del lado derecho."} + + raise ValueError("No se pudo evaluar el valor.") diff --git a/backend/api/services/docmf_simple_validation_service.py b/backend/api/services/docmf_simple_validation_service.py new file mode 100644 index 0000000..4d62818 --- /dev/null +++ b/backend/api/services/docmf_simple_validation_service.py @@ -0,0 +1,24 @@ +def validate_simple_level(level: dict): + errors = [] + + a, b = level["core"] + c, d = level["support"] + + if not (c <= a < b <= d): + errors.append("El núcleo debe estar completamente dentro del soporte.") + + return errors + + +def validate_simple_levels(levels): + results = [] + + for idx, level in enumerate(levels): + errors = validate_simple_level(level.dict()) + results.append({ + "level_index": idx, + "valid": len(errors) == 0, + "errors": errors + }) + + return results diff --git a/backend/api/services/docmf_validation_service.py b/backend/api/services/docmf_validation_service.py new file mode 100644 index 0000000..e49b90c --- /dev/null +++ b/backend/api/services/docmf_validation_service.py @@ -0,0 +1,37 @@ +def validate_single_level(level: dict): + errors = [] + + a, b = level["core"] + c, d = level["support"] + + if not (c <= a < b <= d): + errors.append("El núcleo debe estar completamente dentro del soporte.") + + left = level["left_nodes"] + right = level["right_nodes"] + + if left[0][0] != c: + errors.append("El primer nodo izquierdo debe coincidir con el inicio del soporte.") + if left[-1][0] != a: + errors.append("El último nodo izquierdo debe coincidir con el inicio del núcleo.") + + if right[0][0] != b: + errors.append("El primer nodo derecho debe coincidir con el final del núcleo.") + if right[-1][0] != d: + errors.append("El último nodo derecho debe coincidir con el final del soporte.") + + return errors + + +def validate_levels(levels): + results = [] + + for idx, level in enumerate(levels): + errors = validate_single_level(level.dict()) + results.append({ + "level_index": idx, + "valid": len(errors) == 0, + "errors": errors + }) + + return results diff --git a/backend/api/services/value_function_service.py b/backend/api/services/value_function_service.py new file mode 100644 index 0000000..07039b0 --- /dev/null +++ b/backend/api/services/value_function_service.py @@ -0,0 +1,38 @@ +def compute_value_function(request): + levels = request.levels + cards = request.blank_cards + refs = request.references + + p, q = sorted(int(k) for k in refs) + up, uq = refs[str(p)], refs[str(q)] + + if len(levels) < 3: + raise ValueError("Mínimo debe haber 3 niveles para esta funcionalidad.") + + total_units = sum(cards[i] + 1 for i in range(p, q)) + if total_units == 0: + raise ValueError("Las cartas no pueden generar 0 unidades.") + + alpha = (uq - up) / total_units + + values = [0] * len(levels) + values[p] = up + + for i in range(p + 1, len(levels)): + units = sum(cards[r] + 1 for r in range(p, i)) + values[i] = up + alpha * units + + for i in range(p - 1, -1, -1): + units = sum(cards[r] + 1 for r in range(i, p)) + values[i] = up - alpha * units + + return { + "criterion_name": request.criterion_name, + "values": {levels[i]: round(values[i], 4) for i in range(len(levels))} + } + + +def compute_points(request): + result = compute_value_function(request) + values = list(result["values"].values()) + return {"points": [{"x": i, "y": values[i]} for i in range(len(values))]} diff --git a/backend/api/utils/interpolation.py b/backend/api/utils/interpolation.py new file mode 100644 index 0000000..ccfc35f --- /dev/null +++ b/backend/api/utils/interpolation.py @@ -0,0 +1,8 @@ +def linear_interpolation(x, nodes): + for i in range(len(nodes) - 1): + x0, y0 = nodes[i] + x1, y1 = nodes[i+1] + if x0 <= x <= x1: + t = (x - x0) / (x1 - x0) + return y0 + t * (y1 - y0) + return 0.0 diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py new file mode 100644 index 0000000..f6abe1c --- /dev/null +++ b/backend/api/utils/security.py @@ -0,0 +1,46 @@ +import secrets +from passlib.context import CryptContext +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from api.database.mongodb import users_collection +from bson import ObjectId +import os +import jwt + +SECRET_KEY = os.getenv("SECRET_KEY") + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def generate_token() -> str: + return secrets.token_hex(32) + + +security_scheme = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security_scheme), +): + token = credentials.credentials + + user = await users_collection.find_one({"token": token}) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token inválido o usuario no autenticado", + ) + + user["id"] = str(user["_id"]) + return user + +def create_access_token(data: dict): + return jwt.encode(data, SECRET_KEY, algorithm="HS256") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..6387273 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn +motor +pydantic +passlib +bcrypt==4.0.1 +email-validator +slowapi +httpx +PyJWT diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..aead62e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,35 @@ +services: + backend: + build: + context: ./backend + container_name: backend + ports: + - "8000:8000" + volumes: + - ./backend:/app + depends_on: + - db + env_file: + - backend\.env + + frontend: + build: + context: ./frontend + container_name: frontend + ports: + - "5173:5173" + volumes: + - ./frontend:/app + - /app/node_modules + + db: + image: mongo:6 + container_name: mongo + restart: always + ports: + - "27018:27017" + volumes: + - mongo_data:/data/db + +volumes: + mongo_data: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..82936ab --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,5 @@ +FROM node:20-alpine +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..a36934d --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..609e6e7 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Deck of Cards + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6727ebe --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3618 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.2.2", + "axios": "^1.13.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-icons": "^5.6.0", + "react-router-dom": "^7.13.2", + "recharts": "^3.8.0", + "tailwindcss": "^4.2.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "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.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.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-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "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", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6b49322 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.2.2", + "axios": "^1.13.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-icons": "^5.6.0", + "react-router-dom": "^7.13.2", + "recharts": "^3.8.0", + "tailwindcss": "^4.2.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.1" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..cd55914 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ +logo doc \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/uja-logo.png b/frontend/public/uja-logo.png new file mode 100644 index 0000000..77d17fd Binary files /dev/null and b/frontend/public/uja-logo.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..e05ea1a --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,9 @@ +import AppRouter from './routers/AppRouter'; + +function App() { + return ( + + ); +} + +export default App; \ No newline at end of file diff --git a/frontend/src/components/AddLevelButton.jsx b/frontend/src/components/AddLevelButton.jsx new file mode 100644 index 0000000..67fa95f --- /dev/null +++ b/frontend/src/components/AddLevelButton.jsx @@ -0,0 +1,11 @@ +export default function AddLevelButton({ handleAddLevel }) { + return ( +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx new file mode 100644 index 0000000..3292eea --- /dev/null +++ b/frontend/src/components/BlankCardsCounter.jsx @@ -0,0 +1,51 @@ +import React from 'react'; + +export default function BlankCardsCounter({ index, blankCardsCount, handleBlankCardChange }) { + + const maxCardsPerRow = 7; + const rows = []; + for (let i = 0; i < blankCardsCount; i += maxCardsPerRow) { + rows.push(Array.from({ length: Math.min(maxCardsPerRow, blankCardsCount - i) })); + } + + return ( +
+ + {/* Bloque de botones */} +
+ + +
+ Blancas + {blankCardsCount} +
+ + +
+ + {/* Cartas blancas */} + {blankCardsCount > 0 && ( +
+ {rows.map((row, rowIndex) => ( +
+ {row.map((_, colIndex) => ( +
+ ))} +
+ ))} +
+ )} + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx new file mode 100644 index 0000000..4ca15f8 --- /dev/null +++ b/frontend/src/components/CardEditor.jsx @@ -0,0 +1,18 @@ + +export default function CardEditor({ index, level, handleLevelChange, handleRemoveLevel, totalLevels, error }) { + return ( +
+
+ {totalLevels > 3 && ( + + )} + {index + 1} + {index + 1} + handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} /> +
+
{error &&

Escribe un término

}
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/CriterionInput.jsx b/frontend/src/components/CriterionInput.jsx new file mode 100644 index 0000000..55b1ce7 --- /dev/null +++ b/frontend/src/components/CriterionInput.jsx @@ -0,0 +1,29 @@ +export default function CriterionInput({ criterionName, setCriterionName, error }) { + return ( +
+ + +
+ setCriterionName(e.target.value)} + className={`w-full px-4 py-1.5 rounded-lg border-2 font-bold outline-none transition-all ${ + error + ? 'border-red-400 focus:border-red-500 bg-red-50 text-red-700 placeholder:text-red-300' + : 'border-slate-200 focus:border-blue-400 bg-slate-50 text-slate-800' + }`} + /> + + {error && ( + + Obligatorio + + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ValueFunctionChart.jsx b/frontend/src/components/ValueFunctionChart.jsx new file mode 100644 index 0000000..dad2e8e --- /dev/null +++ b/frontend/src/components/ValueFunctionChart.jsx @@ -0,0 +1,43 @@ +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + +export default function ValueFunctionChart({ result }) { + if (!result) return null; + + return ( +
+

+ Función de Valor: {result.criterion_name} +

+ +
+ + ({ + nombre: label, + valor: value + }))} + margin={{ top: 20, right: 30, left: 20, bottom: 20 }} + > + + + + [value.toFixed(4), 'Valor DoC']} + labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }} + /> + + + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/editor/Step1BaseScale.jsx b/frontend/src/components/editor/Step1BaseScale.jsx new file mode 100644 index 0000000..519b634 --- /dev/null +++ b/frontend/src/components/editor/Step1BaseScale.jsx @@ -0,0 +1,111 @@ +import React, { useState, useEffect, useRef } from 'react'; +import CriterionInput from '../CriterionInput'; +import CardEditor from '../CardEditor'; +import BlankCardsCounter from '../BlankCardsCounter'; +import AddLevelButton from '../AddLevelButton'; +import { FiZoomIn, FiMaximize } from 'react-icons/fi'; + +export default function Step1BaseScale({ + criterionName, handleCriterionChange, + levels, handleLevelChange, handleAddLevel, handleRemoveLevel, + blankCards, handleBlankCardChange, + errors, handleGenerateBaseScale, isLoading +}) { + const [isZoomActive, setIsZoomActive] = useState(true); + const containerRef = useRef(null); + const tableRef = useRef(null); + const [dimensions, setDimensions] = useState({ container: 1000, table: 0 }); + + useEffect(() => { + const updateMeasurements = () => { + if (containerRef.current && tableRef.current) { + setDimensions({ + container: containerRef.current.offsetWidth, + table: tableRef.current.scrollWidth + }); + } + }; + const timeoutId = setTimeout(updateMeasurements, 50); + window.addEventListener('resize', updateMeasurements); + return () => { + clearTimeout(timeoutId); + window.removeEventListener('resize', updateMeasurements); + }; + }, [levels, blankCards]); + + const needsZoom = dimensions.table > dimensions.container; + const dynamicScale = needsZoom ? (dimensions.container / dimensions.table) * 0.95 : 1; + const currentScale = isZoomActive && needsZoom ? dynamicScale : 1; + + return ( +
+ +
+

+ Paso 1: Escala de Referencia +

+ {needsZoom && ( + + )} +
+ + + +
+
+ +
+ {levels.map((level, index) => ( + + + {/* CARTA DE NIVEL */} +
+ 3} /> +
+ + {/* HUECO ENTRE CARTAS Y CONTADOR */} + {index < levels.length - 1 && ( +
+
+ +
+ +
+
+ )} +
+ ))} + + {/* LÍNEA HACIA EL BOTÓN DE AÑADIR */} +
+
+
+ + {/* BOTÓN AÑADIR NIVEL */} +
+ +
+ +
+ +
+
+ + {/* Generar Gráfica Continua */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx new file mode 100644 index 0000000..2369c54 --- /dev/null +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -0,0 +1,87 @@ +import Chart from '../membershipFunction/Chart'; +import Controls from '../membershipFunction/Controls'; +import { CHART_COLORS } from '../../config'; + +export default function Step2FuzzyModeling({ + baseScale, + mfDefinitions, + selectedTerm, + setSelectedTerm, + updateCurrentMf, + handleFinalSubmit, + onBack, + subscales, + onOpenSubscale, + submitError +}) { + const scaleKeys = Object.keys(baseScale); + + const selectedColor = CHART_COLORS[scaleKeys.indexOf(selectedTerm) % CHART_COLORS.length] || '#2563eb'; + + return ( +
+ +
+

Paso 2: Modelar Conceptos Difusos

+ +
+ +
+ {scaleKeys.map((name, index) => { + const isSelected = selectedTerm === name; + const color = CHART_COLORS[index % CHART_COLORS.length]; + + return ( + + ); + })} +
+ + + + {submitError && ( +
+
+ ⚠️ +
+

Error de validación al generar la gráfica

+
+ {submitError} +
+
+
+
+ )} + + + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx new file mode 100644 index 0000000..8e344f9 --- /dev/null +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -0,0 +1,92 @@ +import React, { useState, useEffect, memo } from 'react'; +import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import { useGraphData } from './finalGraph/useGraphData'; +import { GraphTooltip } from './finalGraph/GraphTooltip'; + +const Step3FinalGraph = memo(({ data, criterionName }) => { + const { sortedResults, denseData } = useGraphData(data); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsReady(true); + }, 400); + return () => clearTimeout(timer); + }, []); + + if (!data || (!data.levels && !data.results)) { + return

Cargando datos...

; + } + + return ( +
+ + +

+ {criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'} +

+ +
+ + {!isReady && ( +
+
+ Generando gráfica... +
+ )} + + {/* Gráfica */} +
+ {isReady && ( + + + + + Number(val.toFixed(2))} + tick={{ fill: '#475569', fontSize: 14 }} + /> + + } + cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }} + isAnimationActive={false} + /> + + {sortedResults.map((item) => ( + + {item.isType2 ? ( + <> + + + + + ) : ( + + )} + + ))} + + + )} +
+
+ + {/* Leyenda */} +
+ {sortedResults.map((item) => ( +
+ + {item.term} +
+ ))} +
+
+ ); +}); + +export default Step3FinalGraph; \ No newline at end of file diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx new file mode 100644 index 0000000..6f70c3a --- /dev/null +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import BlankCardsCounter from '../BlankCardsCounter'; + +export default function SubscaleModal({ onClose, onSave, targetInfo }) { + + const initialCount = Math.max(3, targetInfo?.initialData?.cardsCount || 3); + const [cardsCount, setCardsCount] = useState(initialCount); + + const [blankCards, setBlankCards] = useState(() => { + let initialBlanks = targetInfo?.initialData?.blankCards; + + if (!initialBlanks || initialBlanks.length === 0) { + initialBlanks = [0, 0]; + } else if (initialBlanks.length < initialCount - 1) { + const padding = Array(initialCount - 1 - initialBlanks.length).fill(0); + initialBlanks = [...initialBlanks, ...padding]; + } + + return initialBlanks.map(b => { + if (Array.isArray(b)) { + return { min: b[0], max: b[1], isRange: true }; + } + return { min: b, max: b, isRange: false }; + }); + }); + + const handleAddCard = () => { + setCardsCount(prev => prev + 1); + setBlankCards([...blankCards, { min: 0, max: 0, isRange: false }]); + }; + + const handleRemoveCard = () => { + if (cardsCount <= 3) return; + setCardsCount(prev => prev - 1); + setBlankCards(blankCards.slice(0, -1)); + }; + + const handleExactChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0) { + newBlanks[index].min = newVal; + newBlanks[index].max = newVal; + setBlankCards(newBlanks); + } + }; + + const handleMinChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0 && newVal <= newBlanks[index].max) { + newBlanks[index].min = newVal; + setBlankCards(newBlanks); + } + }; + + const handleMaxChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].max + delta; + if (newVal >= newBlanks[index].min) { + newBlanks[index].max = newVal; + setBlankCards(newBlanks); + } + }; + + const toggleRangeMode = (index) => { + const newBlanks = [...blankCards]; + newBlanks[index].isRange = !newBlanks[index].isRange; + if (!newBlanks[index].isRange) { + newBlanks[index].max = newBlanks[index].min; + } + setBlankCards(newBlanks); + }; + + const handleSave = () => { + const payloadBlanks = blankCards.map(b => b.isRange ? [b.min, b.max] : b.min); + onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards: payloadBlanks }); + }; + + const handleDelete = () => { + onSave(targetInfo.term, targetInfo.side, null); + }; + + return ( +
+
+ +
+
+

Diseñar Subescala

+

+ Ajustando pendiente {targetInfo.side === 'left' ? 'Izquierda (Ascendente)' : 'Derecha (Descendente)'} del término "{targetInfo.term}" +

+
+ +
+ +
+
+ + {Array.from({ length: cardsCount }).map((_, index) => ( + + {/* CARTA DE REFERENCIA */} +
+
+ {cardsCount > 3 && index === cardsCount - 1 && ( + + )} + {index + 1} +
+
+ + {/* HUECO ENTRE CARTAS */} + {index < cardsCount - 1 && ( +
+
+ +
+ {blankCards[index].isRange ? ( +
+
+ MÍN + handleMinChange(idx, delta)} + /> +
+ +
-
+ +
+ MÁX + handleMaxChange(idx, delta)} + /> +
+
+ ) : ( +
+ CARTAS + handleExactChange(idx, delta)} + /> +
+ )} + + +
+
+ )} +
+ ))} + + {/* Botón Añadir Carta */} +
+ +
+ +
+
+ + {/* Botones de Acción */} +
+ + +
+ + +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/editor/finalGraph/GraphTooltip.jsx b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx new file mode 100644 index 0000000..bc95ac9 --- /dev/null +++ b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx @@ -0,0 +1,53 @@ +const TermInfo = ({ title, color, children }) => ( +
+ {title} + {children} +
+); + +export const GraphTooltip = ({ active, payload, label, sortedResults }) => { + if (!active || !payload || !payload.length) return null; + const dataPoint = payload[0].payload; + + const activeTerms = sortedResults.filter(item => + item.isType2 ? (dataPoint[`${item.term}_upper`] ?? 0) > 0 : (dataPoint[item.term] ?? 0) > 0 + ); + + if (activeTerms.length === 0) return null; + + return ( +
+

+ Punto X: {Number(label).toFixed(3)} +

+
+ {activeTerms.map(item => { + if (item.isType2) { + const lower = dataPoint[`${item.term}_lower`] ?? 0; + const upper = dataPoint[`${item.term}_upper`] ?? 0; + const range = Math.abs(upper - lower); + + return range <= 0.001 ? ( + + Pertenencia: {Number(upper).toFixed(3)} + + ) : ( + + Mínimo: {Number(lower).toFixed(3)} + Máximo: {Number(upper).toFixed(3)} + + Incertidumbre: {Number(range).toFixed(3)} + + + ); + } + return ( + + Pertenencia: {Number(dataPoint[item.term]).toFixed(3)} + + ); + })} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/editor/finalGraph/useGraphData.js b/frontend/src/components/editor/finalGraph/useGraphData.js new file mode 100644 index 0000000..408cdfc --- /dev/null +++ b/frontend/src/components/editor/finalGraph/useGraphData.js @@ -0,0 +1,89 @@ +import { useMemo } from 'react'; +import { CHART_COLORS } from '../../../config'; + +const interpolateY = (x, nodes) => { + if (!nodes || nodes.length === 0) return null; + const EPSILON = 1e-5; + const MICRO_STEP = 0.0001; + const firstX = nodes[0][0]; + const lastX = nodes[nodes.length - 1][0]; + + if (x < firstX - MICRO_STEP - EPSILON) return null; + if (x > lastX + MICRO_STEP + EPSILON) return null; + if (x < firstX - EPSILON) return 0; + if (x > lastX + EPSILON) return 0; + + for (let i = nodes.length - 1; i >= 0; i--) { + if (Math.abs(nodes[i][0] - x) < EPSILON) return nodes[i][1]; + } + + for (let i = 0; i < nodes.length - 1; i++) { + const x1 = nodes[i][0]; + const x2 = nodes[i + 1][0]; + if (Math.abs(x2 - x1) < EPSILON) continue; + if (x >= x1 && x <= x2) { + const y1 = nodes[i][1]; + const y2 = nodes[i + 1][1]; + return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); + } + } + return null; +}; + +export const useGraphData = (data) => { + const sortedResults = useMemo(() => { + const rawItems = data?.levels || data?.results || []; + const processed = rawItems.map((item, index) => { + const isType2 = !!item.lower && !!item.upper; + const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; + let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; + + if (isType2) { + const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; + } else { + const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + return { ...item, term: termName, isType2, nodes, color, coreVal }; + } + }); + return processed.sort((a, b) => a.coreVal - b.coreVal); + }, [data]); + + const denseData = useMemo(() => { + const xSet = new Set(); + const steps = 1000; + for (let i = 0; i <= steps; i++) xSet.add(Number((i / steps).toFixed(4))); + + sortedResults.forEach(item => { + const addNodes = (nodes) => nodes.forEach(n => { + const x = n[0]; + xSet.add(Number((x - 0.0001).toFixed(4))); + xSet.add(Number(x.toFixed(4))); + xSet.add(Number((x + 0.0001).toFixed(4))); + }); + item.isType2 ? (addNodes(item.lowerNodes), addNodes(item.upperNodes)) : addNodes(item.nodes); + }); + + const xValues = Array.from(xSet).sort((a, b) => a - b); + return xValues.map(x => { + const point = { x }; + sortedResults.forEach(item => { + if (item.isType2) { + const lowerRaw = interpolateY(x, item.lowerNodes); + const upperRaw = interpolateY(x, item.upperNodes); + point[`${item.term}_lower`] = lowerRaw; + point[`${item.term}_upper`] = upperRaw; + point[`${item.term}_range`] = (lowerRaw === null && upperRaw === null) ? null : [lowerRaw ?? 0, upperRaw ?? 0]; + } else { + point[item.term] = interpolateY(x, item.nodes); + } + }); + return point; + }); + }, [sortedResults]); + + return { sortedResults, denseData }; +}; \ No newline at end of file diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx new file mode 100644 index 0000000..b272700 --- /dev/null +++ b/frontend/src/components/layout/Footer.jsx @@ -0,0 +1,95 @@ +export default function Footer() { + return ( + + ); +} \ No newline at end of file diff --git a/frontend/src/components/layout/Header.jsx b/frontend/src/components/layout/Header.jsx new file mode 100644 index 0000000..2011ca7 --- /dev/null +++ b/frontend/src/components/layout/Header.jsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; +import { FiLogIn, FiLogOut } from 'react-icons/fi'; + +export default function Header() { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + const { user, logout, isAuthenticated } = useAuth(); + + const userInitial = user?.username ? user.username[0].toUpperCase() : "U"; + + const handleLogout = () => { + logout(); + setIsDropdownOpen(false); + navigate('/login'); + }; + + const isActive = (path) => { + return location.pathname === path || (path === '/editor' && location.pathname === '/'); + }; + + return ( +
+
+ + + Deck of Cards Logo + + Deck of Cards + + + +
+
+ + Editor + + {isAuthenticated && ( + + Historial + + )} +
+ + {isAuthenticated ? ( +
+ + + {isDropdownOpen && ( + <> +
setIsDropdownOpen(false)}>
+
+
+

Usuario

+

{user?.username}

+
+ + +
+ + )} +
+ ) : ( +
+ + + + Acceder + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx new file mode 100644 index 0000000..b6d6da7 --- /dev/null +++ b/frontend/src/components/layout/MainLayout.jsx @@ -0,0 +1,18 @@ +import Header from './Header'; +import Footer from './Footer'; + +export default function MainLayout({ children }) { + return ( +
+ +
+ +
+ {children} +
+ +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Chart.jsx b/frontend/src/components/membershipFunction/Chart.jsx new file mode 100644 index 0000000..9844798 --- /dev/null +++ b/frontend/src/components/membershipFunction/Chart.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, ReferenceArea, ReferenceLine, ResponsiveContainer, Tooltip } from 'recharts'; + +export default function Chart({ baseScale, mfDefinitions, selectedTerm, colors }) { + const scaleKeys = Object.keys(baseScale); + + return ( +
+ + + + + + typeof value === 'number' ? value.toFixed(2) : value} /> + + {scaleKeys.map((name, index) => { + const val = baseScale[name]; + const mf = mfDefinitions[name]; + if (!mf) return null; + const color = colors[index % colors.length]; + const isSelected = selectedTerm === name; + const trapezeData = [ { x: mf.supportStart, y: 0 }, { x: mf.coreStart, y: 1 }, { x: mf.coreEnd, y: 1 }, { x: mf.supportEnd, y: 0 } ]; + + return ( + + + + + + + ); + })} + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx new file mode 100644 index 0000000..565cc84 --- /dev/null +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -0,0 +1,75 @@ +export default function Controls({ + selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf, + subscales, onOpenSubscale +}) { + if (!selectedTerm || !currentMf) return null; + + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + + let absoluteMin = 0, absoluteMax = 1; + if (selectedIndex > 0) absoluteMin = mfDefinitions[scaleKeys[selectedIndex - 1]].coreEnd; + if (selectedIndex < scaleKeys.length - 1) absoluteMax = mfDefinitions[scaleKeys[selectedIndex + 1]].coreStart; + + const leftSubscale = subscales?.[selectedTerm]?.left; + const rightSubscale = subscales?.[selectedTerm]?.right; + + return ( +
+
+

+ Ajustando: "{selectedTerm}" +

+ +
+
+
+ + updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+
+ + updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
+ +
+ +
+
+ +
+
+ + updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+
+ + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
+ +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/config.js b/frontend/src/config.js new file mode 100644 index 0000000..91fea1e --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1,6 @@ +export const API_BASE_URL = import.meta.env.VITE_API_URL; + +export const CHART_COLORS = [ + '#ef4444', '#f59e0b', '#10b981', '#3b82f6', + '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1' +]; \ No newline at end of file diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js new file mode 100644 index 0000000..ac7e88c --- /dev/null +++ b/frontend/src/context/AuthContext.js @@ -0,0 +1,7 @@ +import { createContext, useContext } from 'react'; + +export const AuthContext = createContext(); + +export const useAuth = () => { + return useContext(AuthContext); +}; \ No newline at end of file diff --git a/frontend/src/context/AuthProvider.jsx b/frontend/src/context/AuthProvider.jsx new file mode 100644 index 0000000..4f76f7e --- /dev/null +++ b/frontend/src/context/AuthProvider.jsx @@ -0,0 +1,42 @@ +import { useState, useCallback } from 'react'; +import { AuthContext } from './AuthContext'; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(() => { + try { + const storedUser = localStorage.getItem('user'); + return storedUser ? JSON.parse(storedUser) : null; + } catch { + return null; + } + }); + + const login = useCallback((data) => { + const currentUser = data.user || data; + const token = data.access_token || data.token; + + setUser(currentUser); + localStorage.setItem('user', JSON.stringify(currentUser)); + + if (token) { + localStorage.setItem('token', token); + } + }, []); + + const logout = useCallback(() => { + setUser(null); + localStorage.removeItem('user'); + localStorage.removeItem('token'); + }, []); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..e63f41c --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,5 @@ +@import "tailwindcss"; + +body { + overflow-y: scroll; +} \ No newline at end of file diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js new file mode 100644 index 0000000..e94beef --- /dev/null +++ b/frontend/src/lib/api.js @@ -0,0 +1,48 @@ +import Axios from 'axios'; +import { API_BASE_URL } from '../config'; + +const api = Axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } +}); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + // Si es un error 401 (No autorizado) + if (error.response && error.response.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + + // SOLUCIÓN: Solo recargamos y redirigimos si NO estamos ya en /login + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } + } + + // Propagamos el error para que los componentes puedan leer backendData + if (error.response && error.response.data) { + return Promise.reject({ + ...error, + backendData: error.response.data + }); + } + + return Promise.reject(error); + } +); + +export default api; \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..33c21e7 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' +import { AuthProvider } from './context/AuthProvider.jsx' + +createRoot(document.getElementById('root')).render( + + + + + , +) \ No newline at end of file diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx new file mode 100644 index 0000000..8e318ab --- /dev/null +++ b/frontend/src/pages/DocEditor.jsx @@ -0,0 +1,276 @@ +import { useState } from 'react'; +import Step1BaseScale from '../components/editor/Step1BaseScale'; +import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; +import SubscaleModal from '../components/editor/SubscaleModal'; +import { calculateValueFunction, buildFuzzyGraph, saveToHistory } from '../services/docService'; +import Step3FinalGraph from '../components/editor/Step3FinalGraph'; + +export default function DocEditor() { + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); + + // ESTADOS: FASE 1 + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + const [errors, setErrors] = useState({ criterion: false, levels: [] }); + + // ESTADOS: FASE 2 + const [baseScale, setBaseScale] = useState({}); + const [selectedTerm, setSelectedTerm] = useState(null); + const [mfDefinitions, setMfDefinitions] = useState({}); + const [subscales, setSubscales] = useState({}); + const [modalTarget, setModalTarget] = useState(null); + + // ESTADO: FASE 3 + const [finalResult, setFinalResult] = useState(null); + const [submitError, setSubmitError] = useState(null); + + // MANEJADORES: FASE 1 + const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; + const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; + const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; + const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; + + const handleGenerateBaseScale = async () => { + const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; + if (newErrors.criterion || newErrors.levels.includes(true)) { + setErrors(newErrors); + return alert("Por favor, rellena todos los campos."); + } + + setIsLoading(true); + try { + const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; + const baseResult = await calculateValueFunction(payloadBase); + setBaseScale(baseResult.values); + const initialMfs = {}; + Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); + setMfDefinitions(initialMfs); + setSelectedTerm(Object.keys(baseResult.values)[0]); + setStep(2); + } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } + }; + + // MANEJADORES: FASE 2 + const updateCurrentMf = (field, value) => { + if (!selectedTerm) return; + let numValue = parseFloat(value); + setMfDefinitions(prev => { + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + + if (selectedIndex > 0) { + prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; + prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; + } + if (selectedIndex < scaleKeys.length - 1) { + nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; + nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; + } + + const anchor = baseScale[selectedTerm]; + + if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; + if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; + if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; + if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; + if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; + + const current = { ...prev[selectedTerm], [field]: numValue }; + + if (field === 'supportStart') { + if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreStart') { + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreEnd') { + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } else if (field === 'supportEnd') { + if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } + return { ...prev, [selectedTerm]: current }; + }); + }; + + const handleOpenSubscale = (term, side, initialData) => { + setModalTarget({ term, side, initialData }); + }; + + const handleSaveSubscale = (term, side, data) => { + setSubscales(prev => ({ + ...prev, + [term]: { + ...prev[term], + [side]: data + } + })); + setModalTarget(null); + }; + + // Petición para el endpoint "build" + const handleFinalSubmit = async () => { + setSubmitError(null); + const scaleKeys = Object.keys(baseScale); + + const payload = { + levels: scaleKeys.map(term => { + const mf = mfDefinitions[term]; + const sub = subscales[term] || {}; + + const c_start = Number(mf.coreStart.toFixed(4)); + const c_end = Number(mf.coreEnd.toFixed(4)); + + const s_start = Math.min(Number(mf.supportStart.toFixed(4)), c_start); + const s_end = Math.max(Number(mf.supportEnd.toFixed(4)), c_end); + + const leftCount = sub.left ? sub.left.cardsCount : 2; + const left_nodes_x = Array.from({ length: leftCount }).map((_, i) => + Number((s_start + (c_start - s_start) * (i / (leftCount - 1))).toFixed(4)) + ); + + const rightCount = sub.right ? sub.right.cardsCount : 2; + const right_nodes_x = Array.from({ length: rightCount }).map((_, i) => + Number((c_end + (s_end - c_end) * (i / (rightCount - 1))).toFixed(4)) + ); + + return { + term: term, + core: [ c_start, c_end ], + support: [ s_start, s_end ], + left_nodes_x: left_nodes_x, + left_blank_cards: sub.left ? sub.left.blankCards : [0], + right_nodes_x: right_nodes_x, + right_blank_cards: sub.right ? sub.right.blankCards : [0] + }; + }) + }; + + setIsLoading(true); + try { + const result = await buildFuzzyGraph(payload); + setFinalResult(result); + setStep(3); + } catch (error) { + + let friendlyMessage = "Ocurrió un error al procesar la solicitud."; + const errorData = error.backendData || error.response?.data || error; + + if (errorData.detail) { + if (errorData.detail === "Invalid input data") { + friendlyMessage = "Revisa los valores del Soporte y Núcleo. Asegúrate de que el 'Inicio del Soporte' sea menor o igual al 'Fin del Soporte', y que el 'Núcleo' esté dentro del 'Soporte'."; + } else if (typeof errorData.detail === 'string') { + friendlyMessage = errorData.detail; + } + } else if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) { + friendlyMessage = errorData.errors.map(err => { + let cleanMsg = err.msg ? err.msg.replace("Value error, ", "") : "Valor incorrecto"; + if (err.loc && err.loc.includes("levels")) { + const levelIndex = err.loc[err.loc.indexOf("levels") + 1]; + const termName = scaleKeys[levelIndex] || `Nivel ${Number(levelIndex) + 1}`; + return `• En la etiqueta "${termName}": ${cleanMsg}`; + } + return `• ${cleanMsg}`; + }).join("\n"); + } + + setSubmitError(friendlyMessage); + } finally { + setIsLoading(false); + } + }; + + // Petición para guardar en el historial + const handleSaveToHistory = async () => { + const token = localStorage.getItem('token'); + if (!token) { + alert("Para guardar tu modelo debes iniciar sesión primero. Puedes seguir visualizando la gráfica sin problema."); + return; + } + + const defaultName = criterionName ? `Modelo de ${criterionName}` : "Mi nueva gráfica DoC-IT2MF"; + const historyName = prompt("Dale un nombre a este modelo para guardarlo en tu historial:", defaultName); + + if (!historyName) return; + + setIsLoading(true); + try { + const payload = { + name: historyName, + results: finalResult.levels || finalResult.results + }; + + await saveToHistory(payload); + + alert("¡Gráfica guardada con éxito en tu historial!"); + + } catch (error) { + console.error("Error al guardar en el historial:", error); + alert("Hubo un problema al guardar el modelo: " + error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + {step === 1 && ( + + )} + + {step === 2 && ( + setStep(1)} + subscales={subscales} + onOpenSubscale={handleOpenSubscale} + submitError={submitError} + /> + )} + + {step === 3 && finalResult && ( +
+ + + +
+ )} + + {modalTarget && ( + setModalTarget(null)} + onSave={handleSaveSubscale} + targetInfo={modalTarget} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx new file mode 100644 index 0000000..d8829d8 --- /dev/null +++ b/frontend/src/pages/History.jsx @@ -0,0 +1,144 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { getUserHistory, deleteHistoryItem } from '../services/docService'; +import Step3FinalGraph from '../components/editor/Step3FinalGraph'; +import { FiEye, FiTrash2, FiBarChart2, FiInbox, FiClock } from 'react-icons/fi'; + +export default function History() { + const [historyItems, setHistoryItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [expandedId, setExpandedId] = useState(null); + + useEffect(() => { + fetchHistory(); + }, []); + + const fetchHistory = async () => { + setIsLoading(true); + try { + const data = await getUserHistory(); + const items = Array.isArray(data) ? data : data.history || data.items || []; + + setHistoryItems(items.reverse()); + } catch (error) { + console.error("Error fetching history:", error); + alert("Hubo un problema al cargar el historial."); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm('¿Seguro que quieres borrar este modelo definitivamente?')) return; + + try { + await deleteHistoryItem(id); + setHistoryItems(prev => prev.filter(item => item._id !== id && item.id !== id)); + if (expandedId === id) setExpandedId(null); + } catch (err) { + const errorMessage = err.response?.data?.detail || err.message || "Error desconocido"; + alert("Error al borrar: " + errorMessage); + } + }; + + const toggleExpand = (id) => { + setExpandedId(expandedId === id ? null : id); + }; + + return ( +
+ {/* Cabecera */} +
+
+

Mi Historial

+

+ Aquí están todas las gráficas y modelos que has guardado. +

+
+ + + Nuevo Modelo + +
+ + {/* Lista de Historial */} + {isLoading ? ( +
+
+

Cargando tus gráficas...

+ +
+ ) : historyItems.length === 0 ? ( +
+ +

Aún no has guardado ningún modelo.

+

Ve al editor, crea una gráfica y dale a "Guardar".

+
+ ) : ( +
+ {historyItems.map((item) => { + const itemId = item._id || item.id; + const isExpanded = expandedId === itemId; + + return ( +
+ + {/* Cabecera de la Card */} +
+
+
+ +
+
+

{item.name || 'Modelo sin título'}

+

+ {item.created_at + ? `Guardado el ${new Date(item.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: 'long', year: 'numeric' })}` + : 'Guardado en el historial' + } +

+
+
+ +
+ + +
+
+ + {/* Contenido Desplegable (La gráfica)*/} +
+
+ {isExpanded ? ( + + ) : ( +
+ )} +
+
+ +
+ ); + })} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..a119649 --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,139 @@ +import { useState, useEffect, useRef } from 'react'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { authService } from '../services/authService'; +import { FiEye, FiEyeOff } from 'react-icons/fi'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + + const navigate = useNavigate(); + const { login } = useAuth(); + const [searchParams] = useSearchParams(); + + const googleLoginProcessed = useRef(false); + + useEffect(() => { + const token = searchParams.get('token'); + + if (token && !googleLoginProcessed.current) { + googleLoginProcessed.current = true; + + const url = new URL(window.location); + url.searchParams.delete('token'); + window.history.replaceState({}, '', url); + + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + + const decodedToken = JSON.parse(jsonPayload); + + const googleUser = { + _id: decodedToken.sub || decodedToken.user_id || "google_id", + username: decodedToken.email ? decodedToken.email.split('@')[0] : "Usuario Google", + email: decodedToken.email || "" + }; + + login({ user: googleUser, access_token: token }); + navigate('/', { replace: true }); + + } catch (err) { + console.error("Error al decodificar el token de Google:", err); + setTimeout(() => { + setError("Error al procesar el login con Google. El token está corrupto."); + }, 0); + } + } + }, [searchParams, login, navigate]); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + try { + const data = await authService.login(email, password); + login(data); + navigate('/'); + } catch (err) { + setError('Credenciales incorrectas.'); + } + }; + + const handleGoogleLogin = () => { + window.location.href = "http://localhost:8000/api/auth/google/login"; + }; + + return ( +
+
+ +
+

Deck of Cards

+

Accede a tu historial y gráficas guardadas

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + className="w-full px-5 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white" + placeholder="correo@ejemplo.com" + /> +
+ +
+ +
+ setPassword(e.target.value)} + className="w-full pl-5 pr-12 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white" + placeholder="••••••••" + /> + +
+
+ + +
+ +
+
+
O
+
+ + + +

¿Nuevo por aquí? Crea una cuenta

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000..fc66b73 --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,132 @@ +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { authService } from '../services/authService'; +import { FiEye, FiEyeOff } from 'react-icons/fi'; + +export default function Register() { + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const [error, setError] = useState(''); + const navigate = useNavigate(); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + if (password !== confirmPassword) { + setError('Las contraseñas no coinciden. Por favor, revísalas.'); + return; + } + + try { + const data = await authService.register(username, email, password); + const userData = { id: data.user_id, username: username, email: email }; + login(userData, data.token); + navigate('/'); + } catch (err) { + setError(err.response?.data?.detail || 'Error al registrar el usuario.'); + } + }; + + return ( +
+
+ +
+

Crear Cuenta

+

Inicia sesión para guardar tu progreso

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setUsername(e.target.value)} + placeholder="Ej: alexis99" + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="tu@email.com" + /> +
+ +
+ +
+ setPassword(e.target.value)} + placeholder="••••••••" + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="••••••••" + /> + +
+
+ + +
+ +

+ ¿Ya tienes cuenta? Inicia sesión aquí +

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx new file mode 100644 index 0000000..3337675 --- /dev/null +++ b/frontend/src/routers/AppRouter.jsx @@ -0,0 +1,22 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import MainLayout from '../components/layout/MainLayout'; +import DocEditor from '../pages/DocEditor'; +import Login from '../pages/Login'; +import Register from '../pages/Register'; +import History from '../pages/History'; + +export default function AppRouter() { + return ( + + + + } /> + } /> + } /> + } /> + } /> + + + + ); +} \ No newline at end of file diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000..f568a1d --- /dev/null +++ b/frontend/src/services/authService.js @@ -0,0 +1,18 @@ +import api from '../lib/api'; + +export const authService = { + login: async (email, password) => { + const response = await api.post('/auth/login', { email, password }); + return response.data; + }, + + register: async (username, email, password) => { + const response = await api.post('/auth/register', { username, email, password }); + return response.data; + }, + + getCurrentUser: async () => { + const response = await api.get('/auth/me'); + return response.data; + } +}; \ No newline at end of file diff --git a/frontend/src/services/docService.js b/frontend/src/services/docService.js new file mode 100644 index 0000000..b024748 --- /dev/null +++ b/frontend/src/services/docService.js @@ -0,0 +1,51 @@ +import api from '../lib/api'; + +export const calculateValueFunction = async (payload) => { + try { + const response = await api.post('/criteria/doc/value-function', payload); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } +}; + +export const buildFuzzyGraph = async (payload) => { + try { + const response = await api.post('/criteria/doc-it2mf/build', payload); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } +}; + +export const saveToHistory = async (payload) => { + try { + const response = await api.post('/history/add', payload); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } +}; + +export const getUserHistory = async () => { + try { + const response = await api.get('/history/user'); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } +}; + +export const deleteHistoryItem = async (historyId) => { + try { + const response = await api.delete(`/history/delete/${historyId}`); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } +}; \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..3d15f68 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react(), + tailwindcss(), + ], +})