diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..552299e --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +CADDY_DOMAIN=localhost +DJANGO_DEBUG=0 +DJANGO_SECRET_KEY=replace-with-a-long-random-value +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,api +DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,https://localhost +CORS_ALLOWED_ORIGINS=http://localhost,http://localhost:5173 +DJANGO_SECURE_SSL_REDIRECT=0 +DJANGO_SESSION_COOKIE_SECURE=0 +DJANGO_CSRF_COOKIE_SECURE=0 +DJANGO_SECURE_HSTS_SECONDS=0 +DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=0 +DJANGO_SECURE_HSTS_PRELOAD=0 +POSTGRES_DB=enhancer +POSTGRES_USER=enhancer +POSTGRES_PASSWORD=enhancer +IMAGE_SESSION_TTL_HOURS=6 diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..b4672d9 --- /dev/null +++ b/.env.sample @@ -0,0 +1,16 @@ +CADDY_DOMAIN=example.com +DJANGO_DEBUG=0 +DJANGO_SECRET_KEY=replace-with-a-long-random-secret +DJANGO_ALLOWED_HOSTS=example.com,www.example.com,api +DJANGO_CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com +CORS_ALLOWED_ORIGINS=https://example.com,https://www.example.com +DJANGO_SECURE_SSL_REDIRECT=1 +DJANGO_SESSION_COOKIE_SECURE=1 +DJANGO_CSRF_COOKIE_SECURE=1 +DJANGO_SECURE_HSTS_SECONDS=31536000 +DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=1 +DJANGO_SECURE_HSTS_PRELOAD=0 +POSTGRES_DB=enhancer +POSTGRES_USER=enhancer +POSTGRES_PASSWORD=replace-with-a-strong-password +IMAGE_SESSION_TTL_HOURS=6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f262369 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +__pycache__/ +*.py[cod] +.venv/ +.pytest_cache/ +.coverage +htmlcov/ +db.sqlite3 +backend/media/ +backend/staticfiles/ +.env +node_modules/ +dist/ +coverage/ +.vite/ +*.log diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..3795f2c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,15 @@ +{$CADDY_DOMAIN:localhost} { + encode zstd gzip + + handle /api/* { + reverse_proxy api:8000 + } + + handle /media/* { + reverse_proxy api:8000 + } + + handle { + reverse_proxy web:80 + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..00f9f31 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Spatial Image Enhancer Pro + +Production-grade public SPA for spatial-domain image enhancement using Django REST Framework, OpenCV, NumPy, React, Tailwind CSS, Celery, Redis, PostgreSQL, Docker Compose, and Caddy. + +## Local Development + +Backend: + +```bash +cd backend +python -m venv .venv +.venv\Scripts\activate +copy .env.sample .env +pip install -r requirements.txt +python manage.py migrate +python manage.py runserver +``` + +Frontend: + +```bash +cd frontend +copy .env.sample .env +npm install +npm run dev +``` + +Open `http://localhost:5173`. The frontend `.env` uses `VITE_API_BASE=http://localhost:8000`, and the backend `.env` allows that origin via CORS/CSRF settings. + +## Docker + +```bash +copy .env.example .env +copy backend\.env.sample backend\.env +docker compose up --build +``` + +Caddy serves the SPA and proxies `/api/*` and `/media/*`. Set `CADDY_DOMAIN`, `DJANGO_ALLOWED_HOSTS`, `DJANGO_CSRF_TRUSTED_ORIGINS`, `CORS_ALLOWED_ORIGINS`, and a strong `DJANGO_SECRET_KEY` before production deployment. + +For production, use `.env.sample` as the root Compose template and `backend/.env.production.sample` as the backend-only template. + +## Backend Structure + +The Django app follows the HackSoftware Django Styleguide pattern: + +- API views validate request input and return responses. +- `processing/services.py` contains business workflows and writes. +- `processing/selectors.py` contains database fetch helpers. +- Settings are environment-driven through `backend/.env`. + +## API + +- `POST /api/images/` +- `POST /api/process/` +- `POST /api/batch/` +- `GET /api/jobs/{job_id}/` + +Images are stored as ephemeral sessions and removed by the cleanup task after the configured TTL. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..c93af90 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +db.sqlite3 +media/ +staticfiles/ +.env +.env.local diff --git a/backend/.env.production.sample b/backend/.env.production.sample new file mode 100644 index 0000000..866cfa9 --- /dev/null +++ b/backend/.env.production.sample @@ -0,0 +1,23 @@ +DJANGO_DEBUG=0 +DJANGO_SECRET_KEY=replace-with-a-long-random-secret +DJANGO_ALLOWED_HOSTS=example.com,www.example.com,api +DJANGO_CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com +CORS_ALLOWED_ORIGINS=https://example.com,https://www.example.com +DJANGO_SECURE_SSL_REDIRECT=1 +DJANGO_SESSION_COOKIE_SECURE=1 +DJANGO_CSRF_COOKIE_SECURE=1 +DJANGO_SECURE_HSTS_SECONDS=31536000 +DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=1 +DJANGO_SECURE_HSTS_PRELOAD=0 +DJANGO_LOG_LEVEL=INFO +POSTGRES_DB=enhancer +POSTGRES_USER=enhancer +POSTGRES_PASSWORD=replace-with-a-strong-password +POSTGRES_HOST=db +POSTGRES_PORT=5432 +POSTGRES_CONN_MAX_AGE=60 +CELERY_BROKER_URL=redis://redis:6379/0 +CELERY_RESULT_BACKEND=redis://redis:6379/0 +CELERY_TASK_TIME_LIMIT=600 +IMAGE_SESSION_TTL_HOURS=6 +MAX_UPLOAD_MB=20 diff --git a/backend/.env.sample b/backend/.env.sample new file mode 100644 index 0000000..ac34b7e --- /dev/null +++ b/backend/.env.sample @@ -0,0 +1,23 @@ +DJANGO_DEBUG=1 +DJANGO_SECRET_KEY=dev-spatial-image-enhancer-change-before-production +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,api +DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:8000,http://127.0.0.1:8000 +CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +DJANGO_SECURE_SSL_REDIRECT=0 +DJANGO_SESSION_COOKIE_SECURE=0 +DJANGO_CSRF_COOKIE_SECURE=0 +DJANGO_SECURE_HSTS_SECONDS=0 +DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=0 +DJANGO_SECURE_HSTS_PRELOAD=0 +DJANGO_LOG_LEVEL=INFO +POSTGRES_DB=enhancer +POSTGRES_USER=enhancer +POSTGRES_PASSWORD=enhancer +POSTGRES_HOST= +POSTGRES_PORT=5432 +POSTGRES_CONN_MAX_AGE=60 +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 +CELERY_TASK_TIME_LIMIT=600 +IMAGE_SESSION_TTL_HOURS=6 +MAX_UPLOAD_MB=20 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..9474dd2 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libglib2.0-0 libgl1 \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["gunicorn", "enhancer_project.wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/backend/enhancer_project/__init__.py b/backend/enhancer_project/__init__.py new file mode 100644 index 0000000..53f4ccb --- /dev/null +++ b/backend/enhancer_project/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/backend/enhancer_project/asgi.py b/backend/enhancer_project/asgi.py new file mode 100644 index 0000000..73d8ab3 --- /dev/null +++ b/backend/enhancer_project/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "enhancer_project.settings") + +application = get_asgi_application() diff --git a/backend/enhancer_project/celery.py b/backend/enhancer_project/celery.py new file mode 100644 index 0000000..1f5c229 --- /dev/null +++ b/backend/enhancer_project/celery.py @@ -0,0 +1,9 @@ +import os + +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "enhancer_project.settings") + +app = Celery("enhancer_project") +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() diff --git a/backend/enhancer_project/env.py b/backend/enhancer_project/env.py new file mode 100644 index 0000000..83d3e81 --- /dev/null +++ b/backend/enhancer_project/env.py @@ -0,0 +1,34 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + + +BASE_DIR = Path(__file__).resolve().parent.parent + +load_dotenv(BASE_DIR / ".env") + + +def env(name, default=None): + return os.environ.get(name, default) + + +def env_bool(name, default=False): + value = env(name) + if value is None: + return default + return value.lower() in {"1", "true", "yes", "on"} + + +def env_int(name, default): + value = env(name) + if value is None or value == "": + return default + return int(value) + + +def env_list(name, default=""): + value = env(name, default) + if not value: + return [] + return [item.strip() for item in value.split(",") if item.strip()] diff --git a/backend/enhancer_project/settings.py b/backend/enhancer_project/settings.py new file mode 100644 index 0000000..9b557e2 --- /dev/null +++ b/backend/enhancer_project/settings.py @@ -0,0 +1,145 @@ +from pathlib import Path + +from .env import env, env_bool, env_int, env_list + +BASE_DIR = Path(__file__).resolve().parent.parent + + +DEBUG = env_bool("DJANGO_DEBUG", False) +SECRET_KEY = env("DJANGO_SECRET_KEY") +if not SECRET_KEY: + if DEBUG: + SECRET_KEY = "dev-only-spatial-image-enhancer" + else: + raise RuntimeError("DJANGO_SECRET_KEY must be set when DJANGO_DEBUG is false.") + +ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,api") +CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS", "") + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "rest_framework", + "processing", +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "enhancer_project.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "enhancer_project.wsgi.application" + +if env("POSTGRES_HOST"): + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env("POSTGRES_DB", "enhancer"), + "USER": env("POSTGRES_USER", "enhancer"), + "PASSWORD": env("POSTGRES_PASSWORD", "enhancer"), + "HOST": env("POSTGRES_HOST", "db"), + "PORT": env("POSTGRES_PORT", "5432"), + "CONN_MAX_AGE": env_int("POSTGRES_CONN_MAX_AGE", 60), + } + } +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + "rest_framework.parsers.MultiPartParser", + "rest_framework.parsers.FormParser", + ], +} + +CORS_ALLOWED_ORIGINS = env_list("CORS_ALLOWED_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173") +CORS_ALLOW_CREDENTIALS = False + +CELERY_BROKER_URL = env("CELERY_BROKER_URL", "redis://redis:6379/0") +CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", CELERY_BROKER_URL) +CELERY_TASK_TRACK_STARTED = True +CELERY_TASK_TIME_LIMIT = env_int("CELERY_TASK_TIME_LIMIT", 600) + +IMAGE_SESSION_TTL_HOURS = env_int("IMAGE_SESSION_TTL_HOURS", 6) +MAX_UPLOAD_MB = env_int("MAX_UPLOAD_MB", 20) + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SECURE_SSL_REDIRECT = env_bool("DJANGO_SECURE_SSL_REDIRECT", not DEBUG) +SESSION_COOKIE_SECURE = env_bool("DJANGO_SESSION_COOKIE_SECURE", not DEBUG) +CSRF_COOKIE_SECURE = env_bool("DJANGO_CSRF_COOKIE_SECURE", not DEBUG) +SECURE_HSTS_SECONDS = env_int("DJANGO_SECURE_HSTS_SECONDS", 0 if DEBUG else 31536000) +SECURE_HSTS_INCLUDE_SUBDOMAINS = env_bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", not DEBUG) +SECURE_HSTS_PRELOAD = env_bool("DJANGO_SECURE_HSTS_PRELOAD", False) +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = "DENY" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + } + }, + "root": { + "handlers": ["console"], + "level": env("DJANGO_LOG_LEVEL", "INFO"), + }, +} diff --git a/backend/enhancer_project/urls.py b/backend/enhancer_project/urls.py new file mode 100644 index 0000000..f27a422 --- /dev/null +++ b/backend/enhancer_project/urls.py @@ -0,0 +1,13 @@ +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path + + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api/", include("processing.urls")), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/enhancer_project/wsgi.py b/backend/enhancer_project/wsgi.py new file mode 100644 index 0000000..0fb3329 --- /dev/null +++ b/backend/enhancer_project/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "enhancer_project.settings") + +application = get_wsgi_application() diff --git a/backend/manage.py b/backend/manage.py new file mode 100644 index 0000000..9af2bf1 --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +import os +import sys + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "enhancer_project.settings") + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/backend/processing/__init__.py b/backend/processing/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/processing/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/processing/algorithms.py b/backend/processing/algorithms.py new file mode 100644 index 0000000..0969ed7 --- /dev/null +++ b/backend/processing/algorithms.py @@ -0,0 +1,436 @@ +import base64 +import math +from io import BytesIO + +import cv2 +import numpy as np +from numpy.lib.stride_tricks import sliding_window_view +from PIL import Image + + +LAPLACIAN_MASK = np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]], dtype=np.float32) +SOBEL_GX = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32) +SOBEL_GY = SOBEL_GX.T +ROBERTS_GX = np.array([[1, 0], [0, -1]], dtype=np.float32) +ROBERTS_GY = np.array([[0, 1], [-1, 0]], dtype=np.float32) + + +class ProcessingError(ValueError): + pass + + +def ensure_uint8(image): + return np.clip(image, 0, 255).astype(np.uint8) + + +def normalize_to_uint8(image): + arr = image.astype(np.float32) + min_value = float(np.min(arr)) + max_value = float(np.max(arr)) + if math.isclose(min_value, max_value): + return np.zeros(arr.shape, dtype=np.uint8) + return np.round((arr - min_value) * 255.0 / (max_value - min_value)).astype(np.uint8) + + +def require_odd(value, name="size", minimum=3): + try: + value = int(value) + except (TypeError, ValueError) as exc: + raise ProcessingError(f"{name} must be an odd integer.") from exc + if value < minimum or value % 2 == 0: + raise ProcessingError(f"{name} must be an odd integer >= {minimum}.") + return value + + +def require_finite_positive(value, name): + try: + value = float(value) + except (TypeError, ValueError) as exc: + raise ProcessingError(f"{name} must be a finite positive number.") from exc + if not np.isfinite(value) or value <= 0: + raise ProcessingError(f"{name} must be a finite positive number.") + return value + + +def to_gray(image): + if image.ndim == 2: + return image + return cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + + +def gray_to_rgb(gray): + return cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) + + +def histogram(image): + gray = to_gray(image) + counts = np.bincount(gray.ravel(), minlength=256).astype(np.float64) + probabilities = counts / max(gray.size, 1) + return probabilities.round(8).tolist() + + +def image_to_data_url(image): + pil_image = Image.fromarray(ensure_uint8(image)) + buffer = BytesIO() + pil_image.save(buffer, format="PNG") + payload = base64.b64encode(buffer.getvalue()).decode("ascii") + return f"data:image/png;base64,{payload}" + + +def data_url_to_bytes(value): + if "," in value: + value = value.split(",", 1)[1] + return base64.b64decode(value) + + +def decode_image(uploaded_file=None, base64_image=None): + if uploaded_file is None and not base64_image: + raise ProcessingError("Provide an image file or base64 image payload.") + if uploaded_file is not None: + raw = uploaded_file.read() + else: + raw = data_url_to_bytes(base64_image) + + image = Image.open(BytesIO(raw)) + image = image.convert("RGB") + return np.array(image, dtype=np.uint8) + + +def negative(image, params): + return 255 - image + + +def logarithmic(image, params): + c = require_finite_positive(params.get("c", 1.0 / math.log(2.0)), "c") + normalized = image.astype(np.float32) / 255.0 + transformed = c * np.log1p(normalized) + return ensure_uint8(np.round(np.clip(transformed, 0.0, 1.0) * 255.0)) + + +def gamma(image, params): + gamma_value = require_finite_positive(params.get("gamma", 1.0), "gamma") + c = require_finite_positive(params.get("c", 1.0), "c") + normalized = image.astype(np.float32) / 255.0 + transformed = c * np.power(normalized, gamma_value) + return ensure_uint8(np.round(np.clip(transformed, 0.0, 1.0) * 255.0)) + + +def contrast_stretch(image, params): + low = int(params.get("low", 0)) + high = int(params.get("high", 255)) + if low < 0 or high > 255 or low >= high: + raise ProcessingError("Contrast stretch requires 0 <= low < high <= 255.") + stretched = (image.astype(np.float32) - low) * (255.0 / (high - low)) + return ensure_uint8(np.round(stretched)) + + +def gray_slice(image, params): + start = int(params.get("start", 96)) + end = int(params.get("end", 160)) + if start < 0 or end > 255 or start > end: + raise ProcessingError("Gray-level slicing requires 0 <= start <= end <= 255.") + preserve = bool(params.get("preserve_background", True)) + highlight = np.array(params.get("highlight", [255, 64, 64]), dtype=np.uint8) + if highlight.shape != (3,): + raise ProcessingError("highlight must be an RGB triplet.") + gray = to_gray(image) + mask = (gray >= start) & (gray <= end) + base = image.copy() if image.ndim == 3 else gray_to_rgb(gray if preserve else np.zeros_like(gray)) + if not preserve: + base = np.zeros((*gray.shape, 3), dtype=np.uint8) + base[mask] = highlight + return base + + +def bit_plane(image, params): + bit = int(params.get("bit", 7)) + if bit < 0 or bit > 7: + raise ProcessingError("bit must be between 0 and 7.") + plane = ((to_gray(image) >> bit) & 1) * 255 + return gray_to_rgb(plane.astype(np.uint8)) + + +def histogram_equalization(image, params): + gray = to_gray(image) + counts = np.bincount(gray.ravel(), minlength=256) + cdf = counts.cumsum().astype(np.float64) + nonzero = cdf[cdf > 0] + if nonzero.size == 0: + return gray_to_rgb(gray) + cdf_min = nonzero[0] + denom = gray.size - cdf_min + if denom <= 0: + equalized = np.zeros_like(gray) + else: + lut = np.round((cdf - cdf_min) / denom * 255.0).clip(0, 255).astype(np.uint8) + equalized = lut[gray] + return gray_to_rgb(equalized) + + +def target_cdf_from_params(params): + if "cdf" in params: + cdf = np.array(params["cdf"], dtype=np.float64) + if cdf.shape != (256,) or np.any(np.diff(cdf) < 0): + raise ProcessingError("cdf must contain 256 non-decreasing values.") + if cdf[-1] <= 0: + raise ProcessingError("cdf must end with a positive value.") + return cdf / cdf[-1] + + mode = params.get("target", "uniform") + levels = np.arange(256, dtype=np.float64) + if mode == "dark": + pdf = np.exp(-levels / 64.0) + elif mode == "bright": + pdf = np.exp(-(255.0 - levels) / 64.0) + elif mode == "bimodal": + pdf = np.exp(-((levels - 72.0) ** 2) / (2 * 22.0**2)) + np.exp(-((levels - 190.0) ** 2) / (2 * 28.0**2)) + else: + pdf = np.ones(256, dtype=np.float64) + cdf = np.cumsum(pdf) + return cdf / cdf[-1] + + +def histogram_matching(image, params): + gray = to_gray(image) + source_counts = np.bincount(gray.ravel(), minlength=256).astype(np.float64) + source_cdf = np.cumsum(source_counts) + source_cdf /= source_cdf[-1] + target_cdf = target_cdf_from_params(params) + target_levels = np.arange(256) + mapping = np.interp(source_cdf, target_cdf, target_levels).round().clip(0, 255).astype(np.uint8) + return gray_to_rgb(mapping[gray]) + + +def local_equalization(image, params): + size = require_odd(params.get("size", 7), "size") + gray = to_gray(image) + radius = size // 2 + padded = np.pad(gray, radius, mode="edge") + windows = sliding_window_view(padded, (size, size)) + centers = gray[..., None, None] + ranks = np.count_nonzero(windows <= centers, axis=(-1, -2)) + equalized = np.round(ranks * 255.0 / (size * size)).astype(np.uint8) + return gray_to_rgb(equalized) + + +def apply_kernel(image, kernel, normalize_derivative=False): + source = image.astype(np.float32) + if image.ndim == 2: + filtered = cv2.filter2D(source, cv2.CV_32F, kernel, borderType=cv2.BORDER_REFLECT) + else: + channels = [cv2.filter2D(source[:, :, idx], cv2.CV_32F, kernel, borderType=cv2.BORDER_REFLECT) for idx in range(source.shape[2])] + filtered = np.stack(channels, axis=2) + if normalize_derivative: + return normalize_to_uint8(filtered) + return ensure_uint8(np.round(filtered)) + + +def filter_float(image, kernel): + source = image.astype(np.float32) + if image.ndim == 2: + return cv2.filter2D(source, cv2.CV_32F, kernel, borderType=cv2.BORDER_REFLECT) + channels = [cv2.filter2D(source[:, :, idx], cv2.CV_32F, kernel, borderType=cv2.BORDER_REFLECT) for idx in range(source.shape[2])] + return np.stack(channels, axis=2) + + +def box_filter(image, params): + size = require_odd(params.get("size", 3), "size") + return cv2.blur(image, (size, size), borderType=cv2.BORDER_REFLECT) + + +def weighted_average(image, params): + size = require_odd(params.get("size", 3), "size") + if "kernel" in params: + kernel = np.array(params["kernel"], dtype=np.float32) + if kernel.shape != (size, size): + raise ProcessingError("kernel dimensions must match size.") + elif size == 3: + kernel = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]], dtype=np.float32) + else: + sigma = max(size / 6.0, 0.1) + ax = np.arange(-(size // 2), size // 2 + 1, dtype=np.float32) + xx, yy = np.meshgrid(ax, ax) + kernel = np.exp(-(xx**2 + yy**2) / (2.0 * sigma**2)) + total = float(np.sum(kernel)) + if math.isclose(total, 0.0): + raise ProcessingError("weighted average kernel sum must not be zero.") + return apply_kernel(image, kernel / total) + + +def median_filter(image, params): + size = require_odd(params.get("size", 3), "size") + return cv2.medianBlur(image, size) + + +def laplacian(image, params): + mode = params.get("mode", "sharpen") + lap = filter_float(image, LAPLACIAN_MASK) + if mode == "edge": + return normalize_to_uint8(lap) + sign = params.get("sign", "add") + source = image.astype(np.float32) + sharpened = source + lap if sign == "add" else source - lap + return ensure_uint8(np.round(sharpened)) + + +def high_boost(image, params): + amplification = float(params.get("amplification", 1.5)) + if not np.isfinite(amplification) or amplification < 1.0: + raise ProcessingError("amplification must be >= 1.") + size = require_odd(params.get("size", 3), "size") + blurred = cv2.blur(image, (size, size), borderType=cv2.BORDER_REFLECT).astype(np.float32) + boosted = amplification * image.astype(np.float32) - blurred + return ensure_uint8(np.round(boosted)) + + +def gradient_magnitude(image, gx_kernel, gy_kernel): + gray = to_gray(image).astype(np.float32) + gx = cv2.filter2D(gray, cv2.CV_32F, gx_kernel, borderType=cv2.BORDER_REFLECT) + gy = cv2.filter2D(gray, cv2.CV_32F, gy_kernel, borderType=cv2.BORDER_REFLECT) + magnitude = np.sqrt(gx**2 + gy**2) + return gray_to_rgb(normalize_to_uint8(magnitude)) + + +def sobel(image, params): + return gradient_magnitude(image, SOBEL_GX, SOBEL_GY) + + +def roberts(image, params): + return gradient_magnitude(image, ROBERTS_GX, ROBERTS_GY) + + +def rgb_to_hsi(image): + rgb = image.astype(np.float32) / 255.0 + r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2] + numerator = 0.5 * ((r - g) + (r - b)) + denominator = np.sqrt((r - g) ** 2 + (r - b) * (g - b)) + 1e-8 + theta = np.arccos(np.clip(numerator / denominator, -1.0, 1.0)) + h = np.where(b <= g, theta, 2.0 * np.pi - theta) / (2.0 * np.pi) + total = r + g + b + s = np.where(total <= 1e-8, 0.0, 1.0 - 3.0 * np.minimum(np.minimum(r, g), b) / total) + i = total / 3.0 + return np.stack([h, s, i], axis=-1) + + +def hsi_to_rgb(hsi): + h = (hsi[..., 0] % 1.0) * 2.0 * np.pi + s = np.clip(hsi[..., 1], 0.0, 1.0) + i = np.clip(hsi[..., 2], 0.0, 1.0) + r = np.zeros_like(h) + g = np.zeros_like(h) + b = np.zeros_like(h) + + sector0 = h < 2.0 * np.pi / 3.0 + sector1 = (h >= 2.0 * np.pi / 3.0) & (h < 4.0 * np.pi / 3.0) + sector2 = ~sector0 & ~sector1 + + h0 = h[sector0] + b[sector0] = i[sector0] * (1.0 - s[sector0]) + r[sector0] = i[sector0] * (1.0 + s[sector0] * np.cos(h0) / (np.cos(np.pi / 3.0 - h0) + 1e-8)) + g[sector0] = 3.0 * i[sector0] - (r[sector0] + b[sector0]) + + h1 = h[sector1] - 2.0 * np.pi / 3.0 + r[sector1] = i[sector1] * (1.0 - s[sector1]) + g[sector1] = i[sector1] * (1.0 + s[sector1] * np.cos(h1) / (np.cos(np.pi / 3.0 - h1) + 1e-8)) + b[sector1] = 3.0 * i[sector1] - (r[sector1] + g[sector1]) + + h2 = h[sector2] - 4.0 * np.pi / 3.0 + g[sector2] = i[sector2] * (1.0 - s[sector2]) + b[sector2] = i[sector2] * (1.0 + s[sector2] * np.cos(h2) / (np.cos(np.pi / 3.0 - h2) + 1e-8)) + r[sector2] = 3.0 * i[sector2] - (g[sector2] + b[sector2]) + + return ensure_uint8(np.round(np.clip(np.stack([r, g, b], axis=-1), 0.0, 1.0) * 255.0)) + + +def hsi_intensity_filter(image, params): + method = params.get("method", "smooth") + hsi = rgb_to_hsi(image) + intensity = np.round(hsi[..., 2] * 255.0).astype(np.uint8) + if method == "sharpen": + filtered = laplacian(intensity, {"mode": "sharpen", "sign": params.get("sign", "add")}) + else: + filtered = box_filter(intensity, {"size": params.get("size", 3)}) + hsi[..., 2] = filtered.astype(np.float32) / 255.0 + return hsi_to_rgb(hsi) + + +def pseudo_color_slices(image, params): + gray = to_gray(image) + slices = params.get( + "slices", + [ + {"start": 0, "end": 85, "color": [59, 130, 246]}, + {"start": 86, "end": 170, "color": [34, 197, 94]}, + {"start": 171, "end": 255, "color": [239, 68, 68]}, + ], + ) + output = np.zeros((*gray.shape, 3), dtype=np.uint8) + for item in slices: + start = int(item["start"]) + end = int(item["end"]) + color = np.array(item["color"], dtype=np.uint8) + if start < 0 or end > 255 or start > end or color.shape != (3,): + raise ProcessingError("Each pseudo-color slice requires start/end in 0..255 and an RGB color.") + output[(gray >= start) & (gray <= end)] = color + return output + + +def gray_to_color_sinusoidal(image, params): + gray = to_gray(image).astype(np.float32) / 255.0 + hue_frequency = float(params.get("hue_frequency", 1.0)) + saturation_frequency = float(params.get("saturation_frequency", 0.5)) + intensity_frequency = float(params.get("intensity_frequency", 0.25)) + h = (0.5 + 0.5 * np.sin(2.0 * np.pi * hue_frequency * gray)) % 1.0 + s = 0.55 + 0.4 * np.sin(2.0 * np.pi * saturation_frequency * gray + np.pi / 3.0) + i = 0.5 + 0.45 * np.sin(2.0 * np.pi * intensity_frequency * gray - np.pi / 2.0) + return hsi_to_rgb(np.stack([h, np.clip(s, 0, 1), np.clip(i, 0, 1)], axis=-1)) + + +OPERATIONS = { + "negative": negative, + "log": logarithmic, + "gamma": gamma, + "contrast_stretch": contrast_stretch, + "gray_slice": gray_slice, + "bit_plane": bit_plane, + "hist_equalization": histogram_equalization, + "hist_match": histogram_matching, + "local_equalization": local_equalization, + "box_filter": box_filter, + "weighted_average": weighted_average, + "median_filter": median_filter, + "laplacian": laplacian, + "high_boost": high_boost, + "sobel": sobel, + "roberts": roberts, + "hsi_intensity_filter": hsi_intensity_filter, + "pseudo_color_slices": pseudo_color_slices, + "gray_to_color_sinusoidal": gray_to_color_sinusoidal, +} + + +def process_image(image, operation, params=None): + params = params or {} + if operation not in OPERATIONS: + raise ProcessingError(f"Unsupported operation '{operation}'.") + return ensure_uint8(OPERATIONS[operation](ensure_uint8(image), params)) + + +def subtract_images(left, right): + verify_registration([left, right]) + diff = left.astype(np.float32) - right.astype(np.float32) + return normalize_to_uint8(np.abs(diff)) + + +def average_images(images): + verify_registration(images) + stack = np.stack([image.astype(np.float32) for image in images], axis=0) + return ensure_uint8(np.round(np.mean(stack, axis=0))) + + +def verify_registration(images): + if len(images) < 2: + raise ProcessingError("At least two registered images are required.") + shape = images[0].shape + if any(image.shape != shape for image in images[1:]): + raise ProcessingError("Images must have identical width, height, and channel count.") diff --git a/backend/processing/apps.py b/backend/processing/apps.py new file mode 100644 index 0000000..eac519d --- /dev/null +++ b/backend/processing/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProcessingConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "processing" diff --git a/backend/processing/migrations/0001_initial.py b/backend/processing/migrations/0001_initial.py new file mode 100644 index 0000000..960a994 --- /dev/null +++ b/backend/processing/migrations/0001_initial.py @@ -0,0 +1,43 @@ +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ImageSession", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("original_image", models.CharField(max_length=255)), + ("processed_image", models.CharField(blank=True, max_length=255)), + ("width", models.PositiveIntegerField()), + ("height", models.PositiveIntegerField()), + ("channels", models.PositiveSmallIntegerField()), + ("color_mode", models.CharField(max_length=16)), + ("original_histogram", models.JSONField(default=list)), + ("processed_histogram", models.JSONField(blank=True, default=list)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("expires_at", models.DateTimeField()), + ], + ), + migrations.CreateModel( + name="ProcessingJob", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("operation", models.CharField(max_length=64)), + ("params", models.JSONField(blank=True, default=dict)), + ("status", models.CharField(choices=[("pending", "Pending"), ("running", "Running"), ("complete", "Complete"), ("failed", "Failed")], default="pending", max_length=16)), + ("progress", models.PositiveSmallIntegerField(default=0)), + ("result_image", models.CharField(blank=True, max_length=255)), + ("result_histogram", models.JSONField(blank=True, default=list)), + ("error", models.TextField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/backend/processing/migrations/__init__.py b/backend/processing/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/processing/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/processing/models.py b/backend/processing/models.py new file mode 100644 index 0000000..de22d07 --- /dev/null +++ b/backend/processing/models.py @@ -0,0 +1,53 @@ +import uuid + +from django.conf import settings +from django.db import models +from django.utils import timezone + + +class ImageSession(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + original_image = models.CharField(max_length=255) + processed_image = models.CharField(max_length=255, blank=True) + width = models.PositiveIntegerField() + height = models.PositiveIntegerField() + channels = models.PositiveSmallIntegerField() + color_mode = models.CharField(max_length=16) + original_histogram = models.JSONField(default=list) + processed_histogram = models.JSONField(default=list, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + + def save(self, *args, **kwargs): + if not self.expires_at: + self.expires_at = timezone.now() + timezone.timedelta(hours=settings.IMAGE_SESSION_TTL_HOURS) + super().save(*args, **kwargs) + + @property + def expired(self): + return timezone.now() >= self.expires_at + + +class ProcessingJob(models.Model): + STATUS_PENDING = "pending" + STATUS_RUNNING = "running" + STATUS_COMPLETE = "complete" + STATUS_FAILED = "failed" + + STATUS_CHOICES = [ + (STATUS_PENDING, "Pending"), + (STATUS_RUNNING, "Running"), + (STATUS_COMPLETE, "Complete"), + (STATUS_FAILED, "Failed"), + ] + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + operation = models.CharField(max_length=64) + params = models.JSONField(default=dict, blank=True) + status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_PENDING) + progress = models.PositiveSmallIntegerField(default=0) + result_image = models.CharField(max_length=255, blank=True) + result_histogram = models.JSONField(default=list, blank=True) + error = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/processing/selectors.py b/backend/processing/selectors.py new file mode 100644 index 0000000..a4b4ce0 --- /dev/null +++ b/backend/processing/selectors.py @@ -0,0 +1,9 @@ +from .models import ImageSession, ProcessingJob + + +def image_session_get(*, session_id): + return ImageSession.objects.filter(id=session_id).first() + + +def processing_job_get(*, job_id): + return ProcessingJob.objects.filter(id=job_id).first() diff --git a/backend/processing/services.py b/backend/processing/services.py new file mode 100644 index 0000000..5206b51 --- /dev/null +++ b/backend/processing/services.py @@ -0,0 +1,84 @@ +import time + +from django.conf import settings +from django.utils import timezone + +from .algorithms import ProcessingError, decode_image, histogram, process_image +from .models import ImageSession, ProcessingJob +from .storage import load_image_array, payload_for_image, save_image_array +from .tasks import run_batch_job + + +def image_session_create(*, uploaded_file=None, image_base64=None): + if uploaded_file and uploaded_file.size > settings.MAX_UPLOAD_MB * 1024 * 1024: + raise ProcessingError(f"Upload exceeds {settings.MAX_UPLOAD_MB} MB.") + + image = decode_image(uploaded_file=uploaded_file, base64_image=image_base64) + relative_path = save_image_array(image, "original") + hist = histogram(image) + session = ImageSession.objects.create( + original_image=relative_path, + width=image.shape[1], + height=image.shape[0], + channels=image.shape[2] if image.ndim == 3 else 1, + color_mode="RGB" if image.ndim == 3 else "L", + original_histogram=hist, + expires_at=timezone.now() + timezone.timedelta(hours=settings.IMAGE_SESSION_TTL_HOURS), + ) + payload = { + "session_id": str(session.id), + "width": session.width, + "height": session.height, + "channels": session.channels, + "color_mode": session.color_mode, + "original_histogram": hist, + "expires_at": session.expires_at.isoformat(), + } + payload.update(payload_for_image(image, relative_path)) + return payload + + +def image_session_process(*, session, operation, params): + if session.expired: + raise ProcessingError("Image session has expired.") + + started_at = time.perf_counter() + source = load_image_array(session.original_image) + result = process_image(source, operation, params) + relative_path = save_image_array(result, f"processed-{operation}") + hist = histogram(result) + + session.processed_image = relative_path + session.processed_histogram = hist + session.save(update_fields=["processed_image", "processed_histogram"]) + + payload = { + "session_id": str(session.id), + "operation": operation, + "params": params, + "processed_histogram": hist, + "elapsed_ms": round((time.perf_counter() - started_at) * 1000, 2), + } + payload.update(payload_for_image(result, relative_path)) + return payload + + +def batch_job_create(*, operation, session_ids, params=None): + job = ProcessingJob.objects.create(operation=operation, params=params or {}) + run_batch_job.delay(str(job.id), operation, [str(session_id) for session_id in session_ids]) + return job + + +def processing_job_payload(*, job): + payload = { + "job_id": str(job.id), + "operation": job.operation, + "status": job.status, + "progress": job.progress, + "error": job.error, + "result_histogram": job.result_histogram, + } + if job.result_image: + image = load_image_array(job.result_image) + payload.update(payload_for_image(image, job.result_image)) + return payload diff --git a/backend/processing/storage.py b/backend/processing/storage.py new file mode 100644 index 0000000..e4b5538 --- /dev/null +++ b/backend/processing/storage.py @@ -0,0 +1,57 @@ +from pathlib import Path +from uuid import uuid4 + +import cv2 +import numpy as np +from django.conf import settings + +from .algorithms import ProcessingError, ensure_uint8, image_to_data_url + + +def session_dir(): + path = Path(settings.MEDIA_ROOT) / "sessions" + path.mkdir(parents=True, exist_ok=True) + return path + + +def save_image_array(image, prefix="image"): + filename = f"sessions/{prefix}-{uuid4().hex}.png" + path = Path(settings.MEDIA_ROOT) / filename + path.parent.mkdir(parents=True, exist_ok=True) + rgb = ensure_uint8(image) + bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) + ok, encoded = cv2.imencode(".png", bgr) + if not ok: + raise ProcessingError("Unable to encode image for temporary storage.") + path.write_bytes(encoded.tobytes()) + return filename + + +def load_image_array(relative_path): + path = Path(settings.MEDIA_ROOT) / relative_path + if not path.exists(): + raise ProcessingError("Temporary image file is missing or unreadable.") + raw = np.frombuffer(path.read_bytes(), dtype=np.uint8) + image = cv2.imdecode(raw, cv2.IMREAD_COLOR) + if image is None: + raise ProcessingError("Temporary image file is missing or unreadable.") + return cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.uint8) + + +def delete_relative_file(relative_path): + if not relative_path: + return + path = (Path(settings.MEDIA_ROOT) / relative_path).resolve() + media_root = Path(settings.MEDIA_ROOT).resolve() + if media_root not in path.parents and path != media_root: + return + if path.exists(): + path.unlink() + + +def payload_for_image(image, relative_path): + return { + "image_path": relative_path, + "image_url": f"{settings.MEDIA_URL}{relative_path}", + "image_data": image_to_data_url(image), + } diff --git a/backend/processing/tasks.py b/backend/processing/tasks.py new file mode 100644 index 0000000..f2474d8 --- /dev/null +++ b/backend/processing/tasks.py @@ -0,0 +1,56 @@ +from pathlib import Path + +from celery import shared_task +from django.utils import timezone + +from .algorithms import ProcessingError, average_images, histogram, subtract_images +from .models import ImageSession, ProcessingJob +from .storage import delete_relative_file, load_image_array, save_image_array + + +@shared_task(bind=True) +def run_batch_job(self, job_id, operation, session_ids): + job = ProcessingJob.objects.get(id=job_id) + job.status = ProcessingJob.STATUS_RUNNING + job.progress = 10 + job.save(update_fields=["status", "progress", "updated_at"]) + try: + sessions = list(ImageSession.objects.filter(id__in=session_ids)) + if len(sessions) != len(session_ids): + raise ProcessingError("One or more sessions do not exist.") + images = [load_image_array(session.processed_image or session.original_image) for session in sessions] + job.progress = 45 + job.save(update_fields=["progress", "updated_at"]) + if operation == "average": + result = average_images(images) + elif operation == "subtract": + result = subtract_images(images[0], images[1]) + else: + raise ProcessingError(f"Unsupported batch operation '{operation}'.") + relative_path = save_image_array(result, f"batch-{operation}") + job.status = ProcessingJob.STATUS_COMPLETE + job.progress = 100 + job.result_image = relative_path + job.result_histogram = histogram(result) + job.error = "" + job.save(update_fields=["status", "progress", "result_image", "result_histogram", "error", "updated_at"]) + except Exception as exc: + job.status = ProcessingJob.STATUS_FAILED + job.error = str(exc) + job.progress = 100 + job.save(update_fields=["status", "error", "progress", "updated_at"]) + raise + + +@shared_task +def cleanup_expired_sessions(): + expired = ImageSession.objects.filter(expires_at__lt=timezone.now()) + for session in expired: + delete_relative_file(session.original_image) + delete_relative_file(session.processed_image) + expired.delete() + + old_jobs = ProcessingJob.objects.filter(created_at__lt=timezone.now() - timezone.timedelta(hours=24)) + for job in old_jobs: + delete_relative_file(job.result_image) + old_jobs.delete() diff --git a/backend/processing/tests/test_algorithms.py b/backend/processing/tests/test_algorithms.py new file mode 100644 index 0000000..c0a0da9 --- /dev/null +++ b/backend/processing/tests/test_algorithms.py @@ -0,0 +1,61 @@ +import numpy as np +from django.test import SimpleTestCase + +from processing.algorithms import ( + LAPLACIAN_MASK, + ProcessingError, + average_images, + gamma, + histogram_equalization, + median_filter, + negative, + roberts, + sobel, + subtract_images, + verify_registration, +) + + +class AlgorithmTests(SimpleTestCase): + def test_negative_transform_uses_l_minus_one(self): + image = np.array([[[0, 127, 255]]], dtype=np.uint8) + result = negative(image, {}) + np.testing.assert_array_equal(result, np.array([[[255, 128, 0]]], dtype=np.uint8)) + + def test_gamma_identity(self): + image = np.array([[[0, 128, 255]]], dtype=np.uint8) + result = gamma(image, {"gamma": 1, "c": 1}) + np.testing.assert_array_equal(result, image) + + def test_laplacian_mask_sums_to_zero(self): + self.assertEqual(int(LAPLACIAN_MASK.sum()), 0) + + def test_histogram_equalization_spreads_two_levels(self): + image = np.array([[0, 0], [255, 255]], dtype=np.uint8) + result = histogram_equalization(image, {}) + expected = np.dstack([image, image, image]) + np.testing.assert_array_equal(result, expected) + + def test_median_removes_impulse_noise(self): + image = np.full((3, 3, 3), 100, dtype=np.uint8) + image[1, 1] = 255 + result = median_filter(image, {"size": 3}) + self.assertEqual(int(result[1, 1, 0]), 100) + + def test_gradient_outputs_are_display_normalized(self): + image = np.zeros((5, 5, 3), dtype=np.uint8) + image[:, 3:] = 255 + self.assertEqual(sobel(image, {}).dtype, np.uint8) + self.assertEqual(roberts(image, {}).dtype, np.uint8) + + def test_arithmetic_requires_registered_shapes(self): + left = np.zeros((2, 2, 3), dtype=np.uint8) + right = np.zeros((3, 2, 3), dtype=np.uint8) + with self.assertRaises(ProcessingError): + verify_registration([left, right]) + + def test_average_and_subtraction(self): + left = np.zeros((2, 2, 3), dtype=np.uint8) + right = np.full((2, 2, 3), 100, dtype=np.uint8) + self.assertEqual(int(average_images([left, right])[0, 0, 0]), 50) + self.assertEqual(int(subtract_images(left, right)[0, 0, 0]), 0) diff --git a/backend/processing/tests/test_api.py b/backend/processing/tests/test_api.py new file mode 100644 index 0000000..53b9db3 --- /dev/null +++ b/backend/processing/tests/test_api.py @@ -0,0 +1,64 @@ +import tempfile +from io import BytesIO +from pathlib import Path +from unittest.mock import patch + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase, override_settings +from PIL import Image +from rest_framework.test import APIClient + + +def png_upload(color=(32, 64, 128), size=(4, 4), name="sample.png"): + buffer = BytesIO() + Image.new("RGB", size, color).save(buffer, format="PNG") + return SimpleUploadedFile(name, buffer.getvalue(), content_type="image/png") + + +class ApiTests(TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.override = override_settings(MEDIA_ROOT=Path(self.tmp.name), IMAGE_SESSION_TTL_HOURS=1) + self.override.enable() + self.client = APIClient() + + def tearDown(self): + self.override.disable() + self.tmp.cleanup() + + def test_upload_and_process(self): + upload = self.client.post("/api/images/", {"image": png_upload()}, format="multipart") + self.assertEqual(upload.status_code, 201) + session_id = upload.data["session_id"] + self.assertEqual(len(upload.data["original_histogram"]), 256) + + processed = self.client.post( + "/api/process/", + {"session_id": session_id, "operation": "gamma", "params": {"gamma": 1, "c": 1}}, + format="json", + ) + self.assertEqual(processed.status_code, 200) + self.assertEqual(len(processed.data["processed_histogram"]), 256) + self.assertTrue(processed.data["image_data"].startswith("data:image/png;base64,")) + + def test_invalid_kernel_rejected(self): + upload = self.client.post("/api/images/", {"image": png_upload()}, format="multipart") + processed = self.client.post( + "/api/process/", + {"session_id": upload.data["session_id"], "operation": "median_filter", "params": {"size": 4}}, + format="json", + ) + self.assertEqual(processed.status_code, 400) + + @patch("processing.services.run_batch_job.delay") + def test_batch_returns_job_id(self, delay): + first = self.client.post("/api/images/", {"image": png_upload(name="a.png")}, format="multipart") + second = self.client.post("/api/images/", {"image": png_upload(color=(96, 96, 96), name="b.png")}, format="multipart") + response = self.client.post( + "/api/batch/", + {"operation": "average", "session_ids": [first.data["session_id"], second.data["session_id"]]}, + format="json", + ) + self.assertEqual(response.status_code, 202) + self.assertIn("job_id", response.data) + delay.assert_called_once() diff --git a/backend/processing/urls.py b/backend/processing/urls.py new file mode 100644 index 0000000..43e61fb --- /dev/null +++ b/backend/processing/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from .views import BatchView, HealthView, ImageUploadView, JobDetailView, ProcessView + + +urlpatterns = [ + path("health/", HealthView.as_view(), name="health"), + path("images/", ImageUploadView.as_view(), name="image-upload"), + path("process/", ProcessView.as_view(), name="process"), + path("batch/", BatchView.as_view(), name="batch"), + path("jobs//", JobDetailView.as_view(), name="job-detail"), +] diff --git a/backend/processing/views.py b/backend/processing/views.py new file mode 100644 index 0000000..2aa9504 --- /dev/null +++ b/backend/processing/views.py @@ -0,0 +1,89 @@ +from rest_framework import serializers, status +from rest_framework.response import Response +from rest_framework.views import APIView + +from .algorithms import ProcessingError +from .selectors import image_session_get, processing_job_get +from .services import batch_job_create, image_session_create, image_session_process, processing_job_payload + + +def error_response(message, code=status.HTTP_400_BAD_REQUEST): + return Response({"detail": message}, status=code) + + +class ImageUploadView(APIView): + class InputSerializer(serializers.Serializer): + image = serializers.ImageField(required=False) + image_base64 = serializers.CharField(required=False, allow_blank=False) + + def validate(self, attrs): + if not attrs.get("image") and not attrs.get("image_base64"): + raise serializers.ValidationError("Provide image or image_base64.") + return attrs + + def post(self, request): + serializer = self.InputSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + try: + payload = image_session_create( + uploaded_file=serializer.validated_data.get("image"), + image_base64=serializer.validated_data.get("image_base64"), + ) + return Response(payload, status=status.HTTP_201_CREATED) + except ProcessingError as exc: + return error_response(str(exc)) + + +class ProcessView(APIView): + class InputSerializer(serializers.Serializer): + session_id = serializers.UUIDField() + operation = serializers.CharField() + params = serializers.DictField(required=False, default=dict) + + def post(self, request): + serializer = self.InputSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + session = image_session_get(session_id=serializer.validated_data["session_id"]) + if session is None: + return error_response("Image session does not exist.", status.HTTP_404_NOT_FOUND) + + try: + payload = image_session_process( + session=session, + operation=serializer.validated_data["operation"], + params=serializer.validated_data.get("params", {}), + ) + return Response(payload) + except ProcessingError as exc: + code = status.HTTP_410_GONE if str(exc) == "Image session has expired." else status.HTTP_400_BAD_REQUEST + return error_response(str(exc), code) + + +class BatchView(APIView): + class InputSerializer(serializers.Serializer): + operation = serializers.ChoiceField(choices=["average", "subtract"]) + session_ids = serializers.ListField(child=serializers.UUIDField(), min_length=2) + params = serializers.DictField(required=False, default=dict) + + def post(self, request): + serializer = self.InputSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + job = batch_job_create( + operation=serializer.validated_data["operation"], + session_ids=serializer.validated_data["session_ids"], + params=serializer.validated_data.get("params", {}), + ) + return Response({"job_id": str(job.id), "status": job.status}, status=status.HTTP_202_ACCEPTED) + + +class JobDetailView(APIView): + def get(self, request, job_id): + job = processing_job_get(job_id=job_id) + if job is None: + return error_response("Job does not exist.", status.HTTP_404_NOT_FOUND) + return Response(processing_job_payload(job=job)) + + +class HealthView(APIView): + def get(self, request): + return Response({"status": "ok"}) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..a53c86f --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,12 @@ +Django==5.0.7 +djangorestframework==3.15.2 +django-cors-headers==4.4.0 +python-dotenv==1.0.1 +whitenoise==6.7.0 +celery==5.4.0 +redis==5.0.7 +psycopg2-binary==2.9.9 +opencv-python-headless==4.10.0.84 +numpy==1.26.4 +Pillow==10.4.0 +gunicorn==22.0.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..96d60b9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,116 @@ +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-enhancer} + POSTGRES_USER: ${POSTGRES_USER:-enhancer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-enhancer} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + command: ["redis-server", "--appendonly", "yes"] + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + api: + build: ./backend + command: sh -c "python manage.py migrate && python manage.py collectstatic --noinput && gunicorn enhancer_project.wsgi:application --bind 0.0.0.0:8000" + env_file: + - ./backend/.env + environment: + DJANGO_DEBUG: ${DJANGO_DEBUG:-0} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-change-me} + DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,api} + DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS:-http://localhost,https://localhost} + DJANGO_SECURE_SSL_REDIRECT: ${DJANGO_SECURE_SSL_REDIRECT:-0} + DJANGO_SESSION_COOKIE_SECURE: ${DJANGO_SESSION_COOKIE_SECURE:-0} + DJANGO_CSRF_COOKIE_SECURE: ${DJANGO_CSRF_COOKIE_SECURE:-0} + DJANGO_SECURE_HSTS_SECONDS: ${DJANGO_SECURE_HSTS_SECONDS:-0} + DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS: ${DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS:-0} + DJANGO_SECURE_HSTS_PRELOAD: ${DJANGO_SECURE_HSTS_PRELOAD:-0} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:5173,http://localhost} + POSTGRES_HOST: db + POSTGRES_DB: ${POSTGRES_DB:-enhancer} + POSTGRES_USER: ${POSTGRES_USER:-enhancer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-enhancer} + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/0 + IMAGE_SESSION_TTL_HOURS: ${IMAGE_SESSION_TTL_HOURS:-6} + volumes: + - media_data:/app/media + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + + worker: + build: ./backend + command: celery -A enhancer_project worker --loglevel=info + env_file: + - ./backend/.env + environment: + DJANGO_DEBUG: ${DJANGO_DEBUG:-0} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-change-me} + DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,api} + DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS:-http://localhost,https://localhost} + DJANGO_SECURE_SSL_REDIRECT: ${DJANGO_SECURE_SSL_REDIRECT:-0} + DJANGO_SESSION_COOKIE_SECURE: ${DJANGO_SESSION_COOKIE_SECURE:-0} + DJANGO_CSRF_COOKIE_SECURE: ${DJANGO_CSRF_COOKIE_SECURE:-0} + DJANGO_SECURE_HSTS_SECONDS: ${DJANGO_SECURE_HSTS_SECONDS:-0} + DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS: ${DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS:-0} + DJANGO_SECURE_HSTS_PRELOAD: ${DJANGO_SECURE_HSTS_PRELOAD:-0} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:5173,http://localhost} + POSTGRES_HOST: db + POSTGRES_DB: ${POSTGRES_DB:-enhancer} + POSTGRES_USER: ${POSTGRES_USER:-enhancer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-enhancer} + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/0 + IMAGE_SESSION_TTL_HOURS: ${IMAGE_SESSION_TTL_HOURS:-6} + volumes: + - media_data:/app/media + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + + web: + build: ./frontend + depends_on: + - api + + caddy: + image: caddy:2-alpine + ports: + - "80:80" + - "443:443" + environment: + CADDY_DOMAIN: ${CADDY_DOMAIN:-localhost} + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - web + - api + +volumes: + postgres_data: + redis_data: + media_data: + caddy_data: + caddy_config: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..5124ac2 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +coverage/ +.vite/ +.env +.env.local +*.log diff --git a/frontend/.env.sample b/frontend/.env.sample new file mode 100644 index 0000000..27786df --- /dev/null +++ b/frontend/.env.sample @@ -0,0 +1 @@ +VITE_API_BASE=http://localhost:8000 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..a265a4f --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-alpine AS build + +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8fd5e2a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Spatial Image Enhancer Pro + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..bb3f608 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..45b7dfd --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3674 @@ +{ + "name": "spatial-image-enhancer-pro", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spatial-image-enhancer-pro", + "version": "1.0.0", + "dependencies": { + "@vitejs/plugin-react": "latest", + "lucide-react": "latest", + "prop-types": "latest", + "react": "latest", + "react-dom": "latest", + "react-quick-pinch-zoom": "latest", + "recharts": "latest", + "vite": "latest" + }, + "devDependencies": { + "@testing-library/jest-dom": "latest", + "@testing-library/react": "latest", + "autoprefixer": "10.4.20", + "jsdom": "latest", + "postcss": "8.4.49", + "tailwindcss": "3.4.17", + "vitest": "latest" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://package-mirror.liara.ir/repository/npm/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://package-mirror.liara.ir/repository/npm/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://package-mirror.liara.ir/repository/npm/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://package-mirror.liara.ir/repository/npm/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://package-mirror.liara.ir/repository/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://package-mirror.liara.ir/repository/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://package-mirror.liara.ir/repository/npm/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://package-mirror.liara.ir/repository/npm/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "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/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "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.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@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://package-mirror.liara.ir/repository/npm/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://package-mirror.liara.ir/repository/npm/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://package-mirror.liara.ir/repository/npm/@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.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "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/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://package-mirror.liara.ir/repository/npm/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://package-mirror.liara.ir/repository/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "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.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://package-mirror.liara.ir/repository/npm/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "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/chai": { + "version": "6.2.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://package-mirror.liara.ir/repository/npm/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://package-mirror.liara.ir/repository/npm/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/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/fill-range": { + "version": "7.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/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/hasown": { + "version": "2.0.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/immer": { + "version": "11.1.11", + "resolved": "https://package-mirror.liara.ir/repository/npm/immer/-/immer-11.1.11.tgz", + "integrity": "sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lucide-react": { + "version": "1.23.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/lucide-react/-/lucide-react-1.23.0.tgz", + "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://package-mirror.liara.ir/repository/npm/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://package-mirror.liara.ir/repository/npm/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://package-mirror.liara.ir/repository/npm/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/node-releases": { + "version": "2.0.50", + "resolved": "https://package-mirror.liara.ir/repository/npm/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT", + "peer": true + }, + "node_modules/react-quick-pinch-zoom": { + "version": "5.1.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/react-quick-pinch-zoom/-/react-quick-pinch-zoom-5.1.1.tgz", + "integrity": "sha512-x+uL7n0Y8OA+W3wvAEWDQlVkHcsD1BRYow3k3Z5Kki6CaQY92qxQbOrPvs2PC1ygzJpcqFwp+MzMyjlgyWsYCQ==", + "license": "MIT", + "dependencies": { + "tslib": ">=2.0.0" + }, + "peerDependencies": { + "prop-types": ">=15.0.0", + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + }, + "peerDependenciesMeta": { + "prop-types": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/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/read-cache": { + "version": "1.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/recharts": { + "version": "3.9.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/recharts/-/recharts-3.9.2.tgz", + "integrity": "sha512-G4fy+Pk46RaXgwWMh+Nzhyo/lbFAVqXo9gtetlyehe6Ehge9CsgDuOTwQDD+i1+llaLktNBiNq4bhnGlDRXFtw==", + "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": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "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/redent": { + "version": "3.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/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://package-mirror.liara.ir/repository/npm/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://package-mirror.liara.ir/repository/npm/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/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/stackback": { + "version": "0.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://package-mirror.liara.ir/repository/npm/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://package-mirror.liara.ir/repository/npm/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://package-mirror.liara.ir/repository/npm/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/tldts/-/tldts-7.4.7.tgz", + "integrity": "sha512-56L0/9HELHSsG1bFCzay8UoLxzRL7kpFf7Wl5q/kSYwiSJGACvro61xnKzPNM+SadxllzdtXsKDSXE7HPeqIAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.7" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/tldts-core/-/tldts-core-7.4.7.tgz", + "integrity": "sha512-rNlAI8fKn/JckBMUSbNL/ES2kmDiurWaE49l+ikwEc9A6lFR7gMx9AhgQMQKBK4H5w4pKLH64JzZfB99uRsGNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://package-mirror.liara.ir/repository/npm/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://package-mirror.liara.ir/repository/npm/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.1.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "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.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://package-mirror.liara.ir/repository/npm/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f4d46a0 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "spatial-image-enhancer-pro", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0", + "test": "vitest run" + }, + "dependencies": { + "@vitejs/plugin-react": "latest", + "vite": "latest", + "react": "latest", + "react-dom": "latest", + "react-quick-pinch-zoom": "latest", + "recharts": "latest", + "lucide-react": "latest", + "prop-types": "latest" + }, + "devDependencies": { + "tailwindcss": "3.4.17", + "postcss": "8.4.49", + "autoprefixer": "10.4.20", + "vitest": "latest", + "@testing-library/react": "latest", + "@testing-library/jest-dom": "latest", + "jsdom": "latest" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..ba80730 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..7f3a374 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,152 @@ +import { useEffect, useMemo, useState } from "react"; +import CanvasPane from "./components/CanvasPane.jsx"; +import Controls from "./components/Controls.jsx"; +import HistogramPanel from "./components/HistogramPanel.jsx"; +import { createBatch, getJob, processImage, uploadImage } from "./lib/api.js"; +import { useDebouncedEffect } from "./lib/debounce.js"; + +export default function App() { + const [session, setSession] = useState(null); + const [processed, setProcessed] = useState(null); + const [batchSessions, setBatchSessions] = useState([]); + const [operation, setOperation] = useState("gamma"); + const [params, setParams] = useState({ gamma: 1 }); + const [status, setStatus] = useState("Upload an image to begin."); + const [busy, setBusy] = useState(false); + const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 }); + + const originalHistogram = session?.original_histogram; + const processedHistogram = processed?.processed_histogram || processed?.result_histogram; + + async function handleUpload(file) { + if (!file) return; + setBusy(true); + setStatus("Uploading image..."); + try { + const payload = await uploadImage(file); + setSession(payload); + setProcessed(null); + setBatchSessions([payload.session_id]); + setTransform({ x: 0, y: 0, scale: 1 }); + setStatus(`${payload.width} x ${payload.height} ${payload.color_mode} image loaded.`); + } catch (error) { + setStatus(error.message); + } finally { + setBusy(false); + } + } + + async function handleBatchUpload(file) { + if (!file) return; + setBusy(true); + setStatus("Uploading batch image..."); + try { + const payload = await uploadImage(file); + setBatchSessions((current) => [...current, payload.session_id]); + setStatus("Batch image added."); + } catch (error) { + setStatus(error.message); + } finally { + setBusy(false); + } + } + + async function runBatch(kind) { + setBusy(true); + setStatus(`Starting ${kind} job...`); + try { + const job = await createBatch(kind, batchSessions); + const result = await pollJob(job.job_id); + setProcessed(result); + setStatus(`${kind} complete.`); + } catch (error) { + setStatus(error.message); + } finally { + setBusy(false); + } + } + + async function pollJob(jobId) { + for (let attempt = 0; attempt < 80; attempt += 1) { + const job = await getJob(jobId); + setStatus(`Job ${job.status}: ${job.progress}%`); + if (job.status === "complete") return job; + if (job.status === "failed") throw new Error(job.error || "Batch job failed"); + await new Promise((resolve) => window.setTimeout(resolve, 1000)); + } + throw new Error("Batch job timed out."); + } + + useDebouncedEffect( + () => { + if (!session?.session_id || !operation) return; + let cancelled = false; + async function run() { + setBusy(true); + setStatus(`Processing ${operation}...`); + try { + const payload = await processImage(session.session_id, operation, params); + if (!cancelled) { + setProcessed(payload); + setStatus(`${operation} complete in ${payload.elapsed_ms} ms.`); + } + } catch (error) { + if (!cancelled) setStatus(error.message); + } finally { + if (!cancelled) setBusy(false); + } + } + run(); + return () => { + cancelled = true; + }; + }, + [session?.session_id, operation, JSON.stringify(params)], + 300 + ); + + const processedImage = processed?.image_data || session?.image_data; + const originalImage = session?.image_data; + + const viewportTitle = useMemo(() => { + if (!session) return "No image"; + return `${session.width} x ${session.height} ${session.color_mode}`; + }, [session]); + + return ( +
+ { + setOperation(nextOperation); + setParams(nextParams); + }} + onParamChange={(key, value) => setParams((current) => ({ ...current, [key]: value }))} + onUpload={handleUpload} + onBatchUpload={handleBatchUpload} + batchCount={batchSessions.length} + onBatchRun={runBatch} + disabled={!session || busy} + busy={busy} + /> + +
+
+
+

Professional Dark Studio

+

{viewportTitle}

+
+
{busy ? "Working..." : status}
+
+ +
+ + +
+ + +
+
+ ); +} diff --git a/frontend/src/App.real-render.test.jsx b/frontend/src/App.real-render.test.jsx new file mode 100644 index 0000000..1ee3abd --- /dev/null +++ b/frontend/src/App.real-render.test.jsx @@ -0,0 +1,9 @@ +import { render, screen } from "@testing-library/react"; +import App from "./App.jsx"; + +describe("App real render", () => { + it("mounts without mocking third-party components", () => { + render(); + expect(screen.getByText("Spatial Image Enhancer Pro")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/App.test.jsx b/frontend/src/App.test.jsx new file mode 100644 index 0000000..edd2157 --- /dev/null +++ b/frontend/src/App.test.jsx @@ -0,0 +1,15 @@ +import { render, screen } from "@testing-library/react"; +import App from "./App.jsx"; + +vi.mock("react-quick-pinch-zoom", () => ({ + default: ({ children }) =>
{children}
+})); + +describe("App", () => { + it("renders the processing studio immediately", () => { + render(); + expect(screen.getByText("Spatial Image Enhancer Pro")).toBeInTheDocument(); + expect(screen.getAllByText("Original").length).toBeGreaterThan(0); + expect(screen.getAllByText("Processed").length).toBeGreaterThan(0); + }); +}); diff --git a/frontend/src/components/CanvasPane.jsx b/frontend/src/components/CanvasPane.jsx new file mode 100644 index 0000000..6819d61 --- /dev/null +++ b/frontend/src/components/CanvasPane.jsx @@ -0,0 +1,49 @@ +import { useEffect, useRef } from "react"; +import QuickPinchZoom from "react-quick-pinch-zoom"; + +function drawToCanvas(canvas, imageData) { + if (!canvas || !imageData) return; + const context = canvas.getContext("2d"); + const image = new Image(); + image.onload = () => { + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + context.clearRect(0, 0, canvas.width, canvas.height); + context.drawImage(image, 0, 0); + }; + image.src = imageData; +} + +export default function CanvasPane({ title, imageData, histogram, transform, onTransform }) { + const canvasRef = useRef(null); + const holderRef = useRef(null); + + useEffect(() => { + drawToCanvas(canvasRef.current, imageData); + }, [imageData]); + + useEffect(() => { + if (!holderRef.current) return; + holderRef.current.style.transform = `translate3d(${transform.x}px, ${transform.y}px, 0) scale(${transform.scale})`; + }, [transform]); + + return ( +
+
+

{title}

+ {histogram ? "p(r_k) ready" : "No histogram"} +
+
+ {imageData ? ( + +
+ +
+
+ ) : ( +
Upload an image to start processing.
+ )} +
+
+ ); +} diff --git a/frontend/src/components/Controls.jsx b/frontend/src/components/Controls.jsx new file mode 100644 index 0000000..4bc17f9 --- /dev/null +++ b/frontend/src/components/Controls.jsx @@ -0,0 +1,167 @@ +import { Activity, Aperture, Blend, Layers, SlidersHorizontal, Upload } from "lucide-react"; + +const groups = [ + { + title: "Intensity", + icon: SlidersHorizontal, + operations: [ + { id: "negative", label: "Negative", params: [] }, + { id: "log", label: "Log", params: [{ key: "c", label: "c", min: 0.1, max: 3, step: 0.05, default: 1.44 }] }, + { id: "gamma", label: "Gamma", params: [{ key: "gamma", label: "Gamma", min: 0.1, max: 4, step: 0.05, default: 1 }] }, + { + id: "contrast_stretch", + label: "Contrast Stretch", + params: [ + { key: "low", label: "Low", min: 0, max: 254, step: 1, default: 30 }, + { key: "high", label: "High", min: 1, max: 255, step: 1, default: 220 } + ] + }, + { + id: "gray_slice", + label: "Gray Slice", + params: [ + { key: "start", label: "Start", min: 0, max: 255, step: 1, default: 96 }, + { key: "end", label: "End", min: 0, max: 255, step: 1, default: 160 } + ] + }, + { id: "bit_plane", label: "Bit Plane", params: [{ key: "bit", label: "Bit", min: 0, max: 7, step: 1, default: 7 }] } + ] + }, + { + title: "Histogram", + icon: Activity, + operations: [ + { id: "hist_equalization", label: "Global Equalization", params: [] }, + { id: "hist_match", label: "Match Uniform", params: [] }, + { id: "local_equalization", label: "Local Equalization", params: [{ key: "size", label: "Window", min: 3, max: 31, step: 2, default: 7 }] } + ] + }, + { + title: "Spatial Filters", + icon: Aperture, + operations: [ + { id: "box_filter", label: "Box", params: [{ key: "size", label: "Size", min: 3, max: 35, step: 2, default: 3 }] }, + { id: "weighted_average", label: "Weighted Avg", params: [{ key: "size", label: "Size", min: 3, max: 35, step: 2, default: 3 }] }, + { id: "median_filter", label: "Median", params: [{ key: "size", label: "Size", min: 3, max: 15, step: 2, default: 3 }] }, + { id: "laplacian", label: "Laplacian", params: [] }, + { + id: "high_boost", + label: "High Boost", + params: [ + { key: "amplification", label: "A", min: 1, max: 5, step: 0.1, default: 1.5 }, + { key: "size", label: "Size", min: 3, max: 35, step: 2, default: 3 } + ] + }, + { id: "sobel", label: "Sobel", params: [] }, + { id: "roberts", label: "Roberts", params: [] } + ] + }, + { + title: "Color", + icon: Blend, + operations: [ + { id: "pseudo_color_slices", label: "Intensity Slices", params: [] }, + { id: "gray_to_color_sinusoidal", label: "HSI Sinusoids", params: [{ key: "hue_frequency", label: "Hue Freq", min: 0.2, max: 4, step: 0.1, default: 1 }] }, + { id: "hsi_intensity_filter", label: "HSI Smooth I", params: [{ key: "size", label: "Size", min: 3, max: 15, step: 2, default: 3 }] } + ] + } +]; + +function initialParams(operation) { + return Object.fromEntries(operation.params.map((param) => [param.key, param.default])); +} + +export default function Controls({ + selected, + params, + onOperationChange, + onParamChange, + onUpload, + onBatchUpload, + batchCount, + onBatchRun, + disabled, + busy +}) { + return ( + + ); +} diff --git a/frontend/src/components/HistogramPanel.jsx b/frontend/src/components/HistogramPanel.jsx new file mode 100644 index 0000000..2ad1802 --- /dev/null +++ b/frontend/src/components/HistogramPanel.jsx @@ -0,0 +1,36 @@ +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; + +function toChartData(original, processed) { + return Array.from({ length: 256 }, (_, level) => ({ + level, + original: original?.[level] ?? 0, + processed: processed?.[level] ?? 0 + })); +} + +export default function HistogramPanel({ original, processed }) { + const data = toChartData(original, processed); + return ( +
+
+

Histogram Analytics

+
+ Original + Processed +
+
+
+ + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js new file mode 100644 index 0000000..6536aa0 --- /dev/null +++ b/frontend/src/lib/api.js @@ -0,0 +1,42 @@ +const API_BASE = import.meta.env.VITE_API_BASE || ""; + +async function parseResponse(response) { + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(payload.detail || "Request failed"); + } + return payload; +} + +export async function uploadImage(file) { + const body = new FormData(); + body.append("image", file); + const response = await fetch(`${API_BASE}/api/images/`, { + method: "POST", + body + }); + return parseResponse(response); +} + +export async function processImage(sessionId, operation, params) { + const response = await fetch(`${API_BASE}/api/process/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session_id: sessionId, operation, params }) + }); + return parseResponse(response); +} + +export async function createBatch(operation, sessionIds, params = {}) { + const response = await fetch(`${API_BASE}/api/batch/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ operation, session_ids: sessionIds, params }) + }); + return parseResponse(response); +} + +export async function getJob(jobId) { + const response = await fetch(`${API_BASE}/api/jobs/${jobId}/`); + return parseResponse(response); +} diff --git a/frontend/src/lib/debounce.js b/frontend/src/lib/debounce.js new file mode 100644 index 0000000..a1a4afc --- /dev/null +++ b/frontend/src/lib/debounce.js @@ -0,0 +1,14 @@ +import { useEffect } from "react"; + +export function useDebouncedEffect(effect, deps, delay = 300) { + useEffect(() => { + let cleanup; + const handle = window.setTimeout(() => { + cleanup = effect(); + }, delay); + return () => { + window.clearTimeout(handle); + if (typeof cleanup === "function") cleanup(); + }; + }, deps); +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..201a174 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; +import "./styles/app.css"; + +createRoot(document.getElementById("root")).render( + + + +); diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css new file mode 100644 index 0000000..19ffdab --- /dev/null +++ b/frontend/src/styles/app.css @@ -0,0 +1,29 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: dark; + background: #09090b; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: #09090b; + color: #e4e4e7; +} + +canvas { + max-width: 100%; + height: auto; + image-rendering: auto; +} + +.studio-grid { + background-image: + linear-gradient(rgba(148, 163, 184, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px); + background-size: 24px 24px; +} diff --git a/frontend/src/test-setup.js b/frontend/src/test-setup.js new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/frontend/src/test-setup.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..a28e615 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,11 @@ +export default { + content: ["./index.html", "./src/**/*.{js,jsx}"], + theme: { + extend: { + fontFamily: { + sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"] + } + } + }, + plugins: [] +}; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..fc1c20a --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + "/api": "http://localhost:8000", + "/media": "http://localhost:8000" + } + }, + test: { + environment: "jsdom", + globals: true, + setupFiles: "./src/test-setup.js" + } +});