diff --git a/.gitignore b/.gitignore index 06104ed..353eafc 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,3 @@ data/email_compare_sample.jsonl # Conda/pip artifacts .env - -# Claude context — BSL 1.1, keep out of version control -CLAUDE.md -docs/superpowers/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..565a484 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,173 @@ +# Avocet — Email Classifier Training Tool + +## What it is + +Shared infrastructure for building and benchmarking email classifiers across the CircuitForge menagerie. +Named for the avocet's sweeping-bill technique — it sweeps through email streams and filters out categories. + +**Pipeline:** +``` +Scrape (IMAP, wide search, multi-account) → data/email_label_queue.jsonl + ↓ +Label (card-stack UI) → data/email_score.jsonl + ↓ +Benchmark (HuggingFace NLI/reranker) → per-model macro-F1 + latency +``` + +## Environment + +- Python env: `conda run -n job-seeker ` for basic use (streamlit, yaml, stdlib only) +- Classifier env: `conda run -n job-seeker-classifiers ` for benchmark (transformers, FlagEmbedding, gliclass) +- Run tests: `/devl/miniconda3/envs/job-seeker/bin/pytest tests/ -v` + (direct binary — `conda run pytest` can spawn runaway processes) +- Create classifier env: `conda env create -f environment.yml` + +## Label Tool (app/label_tool.py) + +Card-stack Streamlit UI for manually labeling recruitment emails. + +``` +conda run -n job-seeker streamlit run app/label_tool.py --server.port 8503 +``` + +- Config: `config/label_tool.yaml` (gitignored — copy from `.example`, or use ⚙️ Settings tab) +- Queue: `data/email_label_queue.jsonl` (gitignored) +- Output: `data/email_score.jsonl` (gitignored) +- Four tabs: 🃏 Label, 📥 Fetch, 📊 Stats, ⚙️ Settings +- Keyboard shortcuts: 1–9 = label, 0 = Other (wildcard, prompts free-text input), S = skip, U = undo +- Dedup: MD5 of `(subject + body[:100])` — cross-account safe + +### Settings Tab (⚙️) +- Add / edit / remove IMAP accounts via form UI — no manual YAML editing required +- Per-account fields: display name, host, port, SSL toggle, username, password (masked), folder, days back +- **🔌 Test connection** button per account — connects, logs in, selects folder, reports message count +- Global: max emails per account per fetch +- **💾 Save** writes `config/label_tool.yaml`; **↩ Reload** discards unsaved changes +- `_sync_settings_to_state()` collects widget values before any add/remove to avoid index-key drift + +## Benchmark (scripts/benchmark_classifier.py) + +``` +# List available models +conda run -n job-seeker-classifiers python scripts/benchmark_classifier.py --list-models + +# Score against labeled JSONL +conda run -n job-seeker-classifiers python scripts/benchmark_classifier.py --score + +# Visual comparison on live IMAP emails +conda run -n job-seeker-classifiers python scripts/benchmark_classifier.py --compare --limit 20 + +# Include slow/large models +conda run -n job-seeker-classifiers python scripts/benchmark_classifier.py --score --include-slow + +# Export DB-labeled emails (⚠️ LLM-generated labels — review first) +conda run -n job-seeker-classifiers python scripts/benchmark_classifier.py --export-db --db /path/to/staging.db +``` + +## Labels (peregrine defaults — configurable per product) + +| Label | Key | Meaning | +|-------|-----|---------| +| `interview_scheduled` | 1 | Phone screen, video call, or on-site invitation | +| `offer_received` | 2 | Formal job offer or offer letter | +| `rejected` | 3 | Application declined or not moving forward | +| `positive_response` | 4 | Recruiter interest or request to connect | +| `survey_received` | 5 | Culture-fit survey or assessment invitation | +| `neutral` | 6 | ATS confirmation (application received, etc.) | +| `event_rescheduled` | 7 | Interview or event moved to a new time | +| `digest` | 8 | Job digest or multi-listing email (scrapeable) | +| `new_lead` | 9 | Unsolicited recruiter outreach or cold contact | +| `hired` | h | Offer accepted, onboarding, welcome email, start date | + +## Model Registry (13 models, 7 defaults) + +See `scripts/benchmark_classifier.py:MODEL_REGISTRY`. +Default models run without `--include-slow`. +Add `--models deberta-small deberta-small-2pass` to test a specific subset. + +## Config Files + +- `config/label_tool.yaml` — gitignored; multi-account IMAP config +- `config/label_tool.yaml.example` — committed template + +## Data Files + +- `data/email_score.jsonl` — gitignored; manually-labeled ground truth +- `data/email_score.jsonl.example` — committed sample for CI +- `data/email_label_queue.jsonl` — gitignored; IMAP fetch queue + +## Key Design Notes + +- `ZeroShotAdapter.load()` instantiates the pipeline object; `classify()` calls the object. + Tests patch `scripts.classifier_adapters.pipeline` (the module-level factory) with a + two-level mock: `mock_factory.return_value = MagicMock(return_value={...})`. +- `two_pass=True` on ZeroShotAdapter: first pass ranks all 6 labels; second pass re-runs + with only top-2, forcing a binary choice. 2× cost, better confidence. +- `--compare` uses the first account in `label_tool.yaml` for live IMAP emails. +- DB export labels are llama3.1:8b-generated — treat as noisy, not gold truth. + +## Vue Label UI (app/api.py + web/) + +FastAPI on port 8503 serves both the REST API and the built Vue SPA (`web/dist/`). + +``` +./manage.sh start-api # build Vue SPA + start FastAPI (binds 0.0.0.0:8503 — LAN accessible) +./manage.sh stop-api +./manage.sh open-api # xdg-open http://localhost:8503 +``` + +Logs: `log/api.log` + +## Email Field Schema — IMPORTANT + +Two schemas exist. The normalization layer in `app/api.py` bridges them automatically. + +### JSONL on-disk schema (written by `label_tool.py` and `label_tool.py`'s IMAP fetch) + +| Field | Type | Notes | +|-------|------|-------| +| `subject` | str | Email subject line | +| `body` | str | Plain-text body, truncated at 800 chars; HTML stripped by `_strip_html()` | +| `from_addr` | str | Sender address string (`"Name "`) | +| `date` | str | Raw RFC 2822 date string | +| `account` | str | Display name of the IMAP account that fetched it | +| *(no `id`)* | — | Dedup key is MD5 of `(subject + body[:100])` — never stored on disk | + +### Vue API schema (returned by `GET /api/queue`, required by POST endpoints) + +| Field | Type | Notes | +|-------|------|-------| +| `id` | str | MD5 content hash, or stored `id` if item has one | +| `subject` | str | Unchanged | +| `body` | str | Unchanged | +| `from` | str | Mapped from `from_addr` (or `from` if already present) | +| `date` | str | Unchanged | +| `source` | str | Mapped from `account` (or `source` if already present) | + +### Normalization layer (`_normalize()` in `app/api.py`) + +`_normalize(item)` handles the mapping and ID generation. All `GET /api/queue` responses +pass through it. Mutating endpoints (`/api/label`, `/api/skip`, `/api/discard`) look up +items via `_normalize(x)["id"]`, so both real data (no `id`, uses content hash) and test +fixtures (explicit `id` field) work transparently. + +### Peregrine integration + +Peregrine's `staging.db` uses different field names again: + +| staging.db column | Maps to avocet JSONL field | +|-------------------|---------------------------| +| `subject` | `subject` | +| `body` | `body` (may contain HTML — run through `_strip_html()` before queuing) | +| `from_address` | `from_addr` | +| `received_date` | `date` | +| `account` or source context | `account` | + +When exporting from Peregrine's DB for avocet labeling, transform to the JSONL schema above +(not the Vue API schema). The `--export-db` flag in `benchmark_classifier.py` does this. +Any new export path should also call `_strip_html()` on the body before writing. + +## Relationship to Peregrine + +Avocet started as `peregrine/tools/label_tool.py` + `peregrine/scripts/classifier_adapters.py`. +Peregrine retains copies during stabilization; once avocet is proven, peregrine will import from here. diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..afc7b9f --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,7 @@ +# Privacy Policy + +CircuitForge LLC's privacy policy applies to this product and is published at: + +**** + +Last reviewed: March 2026. diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..a4c5af2 --- /dev/null +++ b/app/api.py @@ -0,0 +1,569 @@ +"""Avocet — FastAPI REST layer. + +JSONL read/write helpers and FastAPI app instance. +Endpoints and static file serving are added in subsequent tasks. +""" +from __future__ import annotations + +import hashlib +import json +import os +import subprocess as _subprocess +import yaml +from pathlib import Path + +from datetime import datetime, timezone + +from fastapi import FastAPI, HTTPException, Query +from pydantic import BaseModel + +_ROOT = Path(__file__).parent.parent +_DATA_DIR: Path = _ROOT / "data" # overridable in tests via set_data_dir() +_MODELS_DIR: Path = _ROOT / "models" # overridable in tests via set_models_dir() +_CONFIG_DIR: Path | None = None # None = use real path + +# Process registry for running jobs — used by cancel endpoints. +# Keys: "benchmark" | "finetune". Values: the live Popen object. +_running_procs: dict = {} +_cancelled_jobs: set = set() + + +def set_data_dir(path: Path) -> None: + """Override data directory — used by tests.""" + global _DATA_DIR + _DATA_DIR = path + + +def _best_cuda_device() -> str: + """Return the index of the GPU with the most free VRAM as a string. + + Uses nvidia-smi so it works in the job-seeker env (no torch). Returns "" + if nvidia-smi is unavailable or no GPUs are found. Restricting the + training subprocess to a single GPU via CUDA_VISIBLE_DEVICES prevents + PyTorch DataParallel from replicating the model across all GPUs, which + would OOM the GPU with less headroom. + """ + try: + out = _subprocess.check_output( + ["nvidia-smi", "--query-gpu=index,memory.free", + "--format=csv,noheader,nounits"], + text=True, + timeout=5, + ) + best_idx, best_free = "", 0 + for line in out.strip().splitlines(): + parts = line.strip().split(", ") + if len(parts) == 2: + idx, free = parts[0].strip(), int(parts[1].strip()) + if free > best_free: + best_free, best_idx = free, idx + return best_idx + except Exception: + return "" + + +def set_models_dir(path: Path) -> None: + """Override models directory — used by tests.""" + global _MODELS_DIR + _MODELS_DIR = path + + +def set_config_dir(path: Path | None) -> None: + """Override config directory — used by tests.""" + global _CONFIG_DIR + _CONFIG_DIR = path + + +def _config_file() -> Path: + if _CONFIG_DIR is not None: + return _CONFIG_DIR / "label_tool.yaml" + return _ROOT / "config" / "label_tool.yaml" + + +def reset_last_action() -> None: + """Reset undo state — used by tests.""" + global _last_action + _last_action = None + + +def _queue_file() -> Path: + return _DATA_DIR / "email_label_queue.jsonl" + + +def _score_file() -> Path: + return _DATA_DIR / "email_score.jsonl" + + +def _discarded_file() -> Path: + return _DATA_DIR / "discarded.jsonl" + + +def _read_jsonl(path: Path) -> list[dict]: + if not path.exists(): + return [] + lines = path.read_text(encoding="utf-8").splitlines() + return [json.loads(l) for l in lines if l.strip()] + + +def _write_jsonl(path: Path, records: list[dict]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + text = "\n".join(json.dumps(r, ensure_ascii=False) for r in records) + path.write_text(text + "\n" if records else "", encoding="utf-8") + + +def _append_jsonl(path: Path, record: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + +def _item_id(item: dict) -> str: + """Stable content-hash ID — matches label_tool.py _entry_key dedup logic.""" + key = (item.get("subject", "") + (item.get("body", "") or "")[:100]) + return hashlib.md5(key.encode("utf-8", errors="replace")).hexdigest() + + +def _normalize(item: dict) -> dict: + """Normalize JSONL item to the Vue frontend schema. + + label_tool.py stores: subject, body, from_addr, date, account (no id). + The Vue app expects: id, subject, body, from, date, source. + Both old (from_addr/account) and new (from/source) field names are handled. + """ + return { + "id": item.get("id") or _item_id(item), + "subject": item.get("subject", ""), + "body": item.get("body", ""), + "from": item.get("from") or item.get("from_addr", ""), + "date": item.get("date", ""), + "source": item.get("source") or item.get("account", ""), + } + + +app = FastAPI(title="Avocet API") + +# In-memory last-action store (single user, local tool — in-memory is fine) +_last_action: dict | None = None + + +@app.get("/api/queue") +def get_queue(limit: int = Query(default=10, ge=1, le=50)): + items = _read_jsonl(_queue_file()) + return {"items": [_normalize(x) for x in items[:limit]], "total": len(items)} + + +class LabelRequest(BaseModel): + id: str + label: str + + +@app.post("/api/label") +def post_label(req: LabelRequest): + global _last_action + items = _read_jsonl(_queue_file()) + match = next((x for x in items if _normalize(x)["id"] == req.id), None) + if not match: + raise HTTPException(404, f"Item {req.id!r} not found in queue") + record = {**match, "label": req.label, + "labeled_at": datetime.now(timezone.utc).isoformat()} + _append_jsonl(_score_file(), record) + _write_jsonl(_queue_file(), [x for x in items if _normalize(x)["id"] != req.id]) + _last_action = {"type": "label", "item": match, "label": req.label} + return {"ok": True} + + +class SkipRequest(BaseModel): + id: str + + +@app.post("/api/skip") +def post_skip(req: SkipRequest): + global _last_action + items = _read_jsonl(_queue_file()) + match = next((x for x in items if _normalize(x)["id"] == req.id), None) + if not match: + raise HTTPException(404, f"Item {req.id!r} not found in queue") + reordered = [x for x in items if _normalize(x)["id"] != req.id] + [match] + _write_jsonl(_queue_file(), reordered) + _last_action = {"type": "skip", "item": match} + return {"ok": True} + + +class DiscardRequest(BaseModel): + id: str + + +@app.post("/api/discard") +def post_discard(req: DiscardRequest): + global _last_action + items = _read_jsonl(_queue_file()) + match = next((x for x in items if _normalize(x)["id"] == req.id), None) + if not match: + raise HTTPException(404, f"Item {req.id!r} not found in queue") + record = {**match, "label": "__discarded__", + "discarded_at": datetime.now(timezone.utc).isoformat()} + _append_jsonl(_discarded_file(), record) + _write_jsonl(_queue_file(), [x for x in items if _normalize(x)["id"] != req.id]) + _last_action = {"type": "discard", "item": match} + return {"ok": True} + + +@app.delete("/api/label/undo") +def delete_undo(): + global _last_action + if not _last_action: + raise HTTPException(404, "No action to undo") + action = _last_action + item = action["item"] # always the original clean queue item + + # Perform file operations FIRST — only clear _last_action on success + if action["type"] == "label": + records = _read_jsonl(_score_file()) + if not records: + raise HTTPException(409, "Score file is empty — cannot undo label") + _write_jsonl(_score_file(), records[:-1]) + items = _read_jsonl(_queue_file()) + _write_jsonl(_queue_file(), [item] + items) + elif action["type"] == "discard": + records = _read_jsonl(_discarded_file()) + if not records: + raise HTTPException(409, "Discarded file is empty — cannot undo discard") + _write_jsonl(_discarded_file(), records[:-1]) + items = _read_jsonl(_queue_file()) + _write_jsonl(_queue_file(), [item] + items) + elif action["type"] == "skip": + items = _read_jsonl(_queue_file()) + item_id = _normalize(item)["id"] + items = [item] + [x for x in items if _normalize(x)["id"] != item_id] + _write_jsonl(_queue_file(), items) + + # Clear AFTER all file operations succeed + _last_action = None + return {"undone": {"type": action["type"], "item": _normalize(item)}} + + +# Label metadata — 10 labels matching label_tool.py +_LABEL_META = [ + {"name": "interview_scheduled", "emoji": "\U0001f4c5", "color": "#4CAF50", "key": "1"}, + {"name": "offer_received", "emoji": "\U0001f389", "color": "#2196F3", "key": "2"}, + {"name": "rejected", "emoji": "\u274c", "color": "#F44336", "key": "3"}, + {"name": "positive_response", "emoji": "\U0001f44d", "color": "#FF9800", "key": "4"}, + {"name": "survey_received", "emoji": "\U0001f4cb", "color": "#9C27B0", "key": "5"}, + {"name": "neutral", "emoji": "\u2b1c", "color": "#607D8B", "key": "6"}, + {"name": "event_rescheduled", "emoji": "\U0001f504", "color": "#FF5722", "key": "7"}, + {"name": "digest", "emoji": "\U0001f4f0", "color": "#00BCD4", "key": "8"}, + {"name": "new_lead", "emoji": "\U0001f91d", "color": "#009688", "key": "9"}, + {"name": "hired", "emoji": "\U0001f38a", "color": "#FFC107", "key": "h"}, +] + + +@app.get("/api/config/labels") +def get_labels(): + return _LABEL_META + + +@app.get("/api/config") +def get_config(): + f = _config_file() + if not f.exists(): + return {"accounts": [], "max_per_account": 500} + raw = yaml.safe_load(f.read_text(encoding="utf-8")) or {} + return {"accounts": raw.get("accounts", []), "max_per_account": raw.get("max_per_account", 500)} + + +class ConfigPayload(BaseModel): + accounts: list[dict] + max_per_account: int = 500 + + +@app.post("/api/config") +def post_config(payload: ConfigPayload): + f = _config_file() + f.parent.mkdir(parents=True, exist_ok=True) + tmp = f.with_suffix(".tmp") + tmp.write_text(yaml.dump(payload.model_dump(), allow_unicode=True, sort_keys=False), + encoding="utf-8") + tmp.rename(f) + return {"ok": True} + + +@app.get("/api/stats") +def get_stats(): + records = _read_jsonl(_score_file()) + counts: dict[str, int] = {} + for r in records: + lbl = r.get("label", "") + if lbl: + counts[lbl] = counts.get(lbl, 0) + 1 + return { + "total": len(records), + "counts": counts, + "score_file_bytes": _score_file().stat().st_size if _score_file().exists() else 0, + } + + +@app.get("/api/stats/download") +def download_stats(): + from fastapi.responses import FileResponse + if not _score_file().exists(): + raise HTTPException(404, "No score file") + return FileResponse( + str(_score_file()), + filename="email_score.jsonl", + media_type="application/jsonlines", + headers={"Content-Disposition": 'attachment; filename="email_score.jsonl"'}, + ) + + +class AccountTestRequest(BaseModel): + account: dict + + +@app.post("/api/accounts/test") +def test_account(req: AccountTestRequest): + from app.imap_fetch import test_connection + ok, message, count = test_connection(req.account) + return {"ok": ok, "message": message, "count": count} + + +from fastapi.responses import StreamingResponse + + +# --------------------------------------------------------------------------- +# Benchmark endpoints +# --------------------------------------------------------------------------- + +@app.get("/api/benchmark/results") +def get_benchmark_results(): + """Return the most recently saved benchmark results, or an empty envelope.""" + path = _DATA_DIR / "benchmark_results.json" + if not path.exists(): + return {"models": {}, "sample_count": 0, "timestamp": None} + return json.loads(path.read_text()) + + +@app.get("/api/benchmark/run") +def run_benchmark(include_slow: bool = False): + """Spawn the benchmark script and stream stdout as SSE progress events.""" + python_bin = "/devl/miniconda3/envs/job-seeker-classifiers/bin/python" + script = str(_ROOT / "scripts" / "benchmark_classifier.py") + cmd = [python_bin, script, "--score", "--save"] + if include_slow: + cmd.append("--include-slow") + + def generate(): + try: + proc = _subprocess.Popen( + cmd, + stdout=_subprocess.PIPE, + stderr=_subprocess.STDOUT, + text=True, + bufsize=1, + cwd=str(_ROOT), + ) + _running_procs["benchmark"] = proc + _cancelled_jobs.discard("benchmark") # clear any stale flag from a prior run + try: + for line in proc.stdout: + line = line.rstrip() + if line: + yield f"data: {json.dumps({'type': 'progress', 'message': line})}\n\n" + proc.wait() + if proc.returncode == 0: + yield f"data: {json.dumps({'type': 'complete'})}\n\n" + elif "benchmark" in _cancelled_jobs: + _cancelled_jobs.discard("benchmark") + yield f"data: {json.dumps({'type': 'cancelled'})}\n\n" + else: + yield f"data: {json.dumps({'type': 'error', 'message': f'Process exited with code {proc.returncode}'})}\n\n" + finally: + _running_procs.pop("benchmark", None) + except Exception as exc: + yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n" + + return StreamingResponse( + generate(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + +# --------------------------------------------------------------------------- +# Finetune endpoints +# --------------------------------------------------------------------------- + +@app.get("/api/finetune/status") +def get_finetune_status(): + """Scan models/ for training_info.json files. Returns [] if none exist.""" + models_dir = _MODELS_DIR + if not models_dir.exists(): + return [] + results = [] + for sub in models_dir.iterdir(): + if not sub.is_dir(): + continue + info_path = sub / "training_info.json" + if not info_path.exists(): + continue + try: + info = json.loads(info_path.read_text(encoding="utf-8")) + results.append(info) + except Exception: + pass + return results + + +@app.get("/api/finetune/run") +def run_finetune_endpoint( + model: str = "deberta-small", + epochs: int = 5, + score: list[str] = Query(default=[]), +): + """Spawn finetune_classifier.py and stream stdout as SSE progress events.""" + python_bin = "/devl/miniconda3/envs/job-seeker-classifiers/bin/python" + script = str(_ROOT / "scripts" / "finetune_classifier.py") + cmd = [python_bin, script, "--model", model, "--epochs", str(epochs)] + data_root = _DATA_DIR.resolve() + for score_file in score: + resolved = (_DATA_DIR / score_file).resolve() + if not str(resolved).startswith(str(data_root)): + raise HTTPException(400, f"Invalid score path: {score_file!r}") + cmd.extend(["--score", str(resolved)]) + + # Pick the GPU with the most free VRAM. Setting CUDA_VISIBLE_DEVICES to a + # single device prevents DataParallel from replicating the model across all + # GPUs, which would force a full copy onto the more memory-constrained device. + proc_env = {**os.environ, "PYTORCH_ALLOC_CONF": "expandable_segments:True"} + best_gpu = _best_cuda_device() + if best_gpu: + proc_env["CUDA_VISIBLE_DEVICES"] = best_gpu + + gpu_note = f"GPU {best_gpu}" if best_gpu else "CPU (no GPU found)" + + def generate(): + yield f"data: {json.dumps({'type': 'progress', 'message': f'[api] Using {gpu_note} (most free VRAM)'})}\n\n" + try: + proc = _subprocess.Popen( + cmd, + stdout=_subprocess.PIPE, + stderr=_subprocess.STDOUT, + text=True, + bufsize=1, + cwd=str(_ROOT), + env=proc_env, + ) + _running_procs["finetune"] = proc + _cancelled_jobs.discard("finetune") # clear any stale flag from a prior run + try: + for line in proc.stdout: + line = line.rstrip() + if line: + yield f"data: {json.dumps({'type': 'progress', 'message': line})}\n\n" + proc.wait() + if proc.returncode == 0: + yield f"data: {json.dumps({'type': 'complete'})}\n\n" + elif "finetune" in _cancelled_jobs: + _cancelled_jobs.discard("finetune") + yield f"data: {json.dumps({'type': 'cancelled'})}\n\n" + else: + yield f"data: {json.dumps({'type': 'error', 'message': f'Process exited with code {proc.returncode}'})}\n\n" + finally: + _running_procs.pop("finetune", None) + except Exception as exc: + yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n" + + return StreamingResponse( + generate(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + +@app.post("/api/benchmark/cancel") +def cancel_benchmark(): + """Kill the running benchmark subprocess. 404 if none is running.""" + proc = _running_procs.get("benchmark") + if proc is None: + raise HTTPException(404, "No benchmark is running") + _cancelled_jobs.add("benchmark") + proc.terminate() + try: + proc.wait(timeout=3) + except _subprocess.TimeoutExpired: + proc.kill() + return {"status": "cancelled"} + + +@app.post("/api/finetune/cancel") +def cancel_finetune(): + """Kill the running fine-tune subprocess. 404 if none is running.""" + proc = _running_procs.get("finetune") + if proc is None: + raise HTTPException(404, "No finetune is running") + _cancelled_jobs.add("finetune") + proc.terminate() + try: + proc.wait(timeout=3) + except _subprocess.TimeoutExpired: + proc.kill() + return {"status": "cancelled"} + + +@app.get("/api/fetch/stream") +def fetch_stream( + accounts: str = Query(default=""), + days_back: int = Query(default=90, ge=1, le=365), + limit: int = Query(default=150, ge=1, le=1000), + mode: str = Query(default="wide"), +): + from app.imap_fetch import fetch_account_stream + + selected_names = {n.strip() for n in accounts.split(",") if n.strip()} + config = get_config() # reuse existing endpoint logic + selected = [a for a in config["accounts"] if a.get("name") in selected_names] + + def generate(): + known_keys = {_item_id(x) for x in _read_jsonl(_queue_file())} + total_added = 0 + + for acc in selected: + try: + batch_emails: list[dict] = [] + for event in fetch_account_stream(acc, days_back, limit, known_keys): + if event["type"] == "done": + batch_emails = event.pop("emails", []) + total_added += event["added"] + yield f"data: {json.dumps(event)}\n\n" + # Write new emails to queue after each account + if batch_emails: + existing = _read_jsonl(_queue_file()) + _write_jsonl(_queue_file(), existing + batch_emails) + except Exception as exc: + error_event = {"type": "error", "account": acc.get("name", "?"), + "message": str(exc)} + yield f"data: {json.dumps(error_event)}\n\n" + + queue_size = len(_read_jsonl(_queue_file())) + complete = {"type": "complete", "total_added": total_added, "queue_size": queue_size} + yield f"data: {json.dumps(complete)}\n\n" + + return StreamingResponse(generate(), media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}) + + +# Static SPA — MUST be last (catches all unmatched paths) +_DIST = _ROOT / "web" / "dist" +if _DIST.exists(): + from fastapi.responses import FileResponse + from fastapi.staticfiles import StaticFiles + + # Serve index.html with no-cache so browsers always fetch fresh HTML after rebuilds. + # Hashed assets (/assets/index-abc123.js) can be cached forever — they change names + # when content changes (standard Vite cache-busting strategy). + _NO_CACHE = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache"} + + @app.get("/") + def get_spa_root(): + return FileResponse(str(_DIST / "index.html"), headers=_NO_CACHE) + + app.mount("/", StaticFiles(directory=str(_DIST), html=True), name="spa") diff --git a/app/imap_fetch.py b/app/imap_fetch.py new file mode 100644 index 0000000..bcafb7b --- /dev/null +++ b/app/imap_fetch.py @@ -0,0 +1,214 @@ +"""Avocet — IMAP fetch utilities. + +Shared between app/api.py (FastAPI SSE endpoint) and app/label_tool.py (Streamlit). +No Streamlit imports here — stdlib + imaplib only. +""" +from __future__ import annotations + +import email as _email_lib +import hashlib +import imaplib +import re +from datetime import datetime, timedelta +from email.header import decode_header as _raw_decode +from html.parser import HTMLParser +from typing import Any, Iterator + + +# ── HTML → plain text ──────────────────────────────────────────────────────── + +class _TextExtractor(HTMLParser): + def __init__(self): + super().__init__() + self._parts: list[str] = [] + + def handle_data(self, data: str) -> None: + stripped = data.strip() + if stripped: + self._parts.append(stripped) + + def get_text(self) -> str: + return " ".join(self._parts) + + +def strip_html(html_str: str) -> str: + try: + ex = _TextExtractor() + ex.feed(html_str) + return ex.get_text() + except Exception: + return re.sub(r"<[^>]+>", " ", html_str).strip() + + +# ── IMAP decode helpers ─────────────────────────────────────────────────────── + +def _decode_str(value: str | None) -> str: + if not value: + return "" + parts = _raw_decode(value) + out = [] + for part, enc in parts: + if isinstance(part, bytes): + out.append(part.decode(enc or "utf-8", errors="replace")) + else: + out.append(str(part)) + return " ".join(out).strip() + + +def _extract_body(msg: Any) -> str: + if msg.is_multipart(): + html_fallback: str | None = None + for part in msg.walk(): + ct = part.get_content_type() + if ct == "text/plain": + try: + charset = part.get_content_charset() or "utf-8" + return part.get_payload(decode=True).decode(charset, errors="replace") + except Exception: + pass + elif ct == "text/html" and html_fallback is None: + try: + charset = part.get_content_charset() or "utf-8" + raw = part.get_payload(decode=True).decode(charset, errors="replace") + html_fallback = strip_html(raw) + except Exception: + pass + return html_fallback or "" + else: + try: + charset = msg.get_content_charset() or "utf-8" + raw = msg.get_payload(decode=True).decode(charset, errors="replace") + if msg.get_content_type() == "text/html": + return strip_html(raw) + return raw + except Exception: + pass + return "" + + +def entry_key(e: dict) -> str: + """Stable MD5 content-hash for dedup — matches label_tool.py _entry_key.""" + key = (e.get("subject", "") + (e.get("body", "") or "")[:100]) + return hashlib.md5(key.encode("utf-8", errors="replace")).hexdigest() + + +# ── Wide search terms ──────────────────────────────────────────────────────── + +_WIDE_TERMS = [ + "interview", "phone screen", "video call", "zoom link", "schedule a call", + "offer letter", "job offer", "offer of employment", "pleased to offer", + "unfortunately", "not moving forward", "other candidates", "regret to inform", + "no longer", "decided not to", "decided to go with", + "opportunity", "interested in your background", "reached out", "great fit", + "exciting role", "love to connect", + "assessment", "questionnaire", "culture fit", "culture-fit", "online assessment", + "application received", "thank you for applying", "application confirmation", + "you applied", "your application for", + "reschedule", "rescheduled", "new time", "moved to", "postponed", "new date", + "job digest", "jobs you may like", "recommended jobs", "jobs for you", + "new jobs", "job alert", + "came across your profile", "reaching out about", "great fit for a role", + "exciting opportunity", + "welcome to the team", "start date", "onboarding", "first day", "we're excited to have you", + "application", "recruiter", "recruiting", "hiring", "candidate", +] + + +# ── Public API ──────────────────────────────────────────────────────────────── + +def test_connection(acc: dict) -> tuple[bool, str, int | None]: + """Connect, login, select folder. Returns (ok, human_message, message_count|None).""" + host = acc.get("host", "") + port = int(acc.get("port", 993)) + use_ssl = acc.get("use_ssl", True) + username = acc.get("username", "") + password = acc.get("password", "") + folder = acc.get("folder", "INBOX") + if not host or not username or not password: + return False, "Host, username, and password are all required.", None + try: + conn = (imaplib.IMAP4_SSL if use_ssl else imaplib.IMAP4)(host, port) + conn.login(username, password) + _, data = conn.select(folder, readonly=True) + count_raw = data[0].decode() if data and data[0] else "0" + count = int(count_raw) if count_raw.isdigit() else 0 + conn.logout() + return True, f"Connected — {count:,} message(s) in {folder}.", count + except Exception as exc: + return False, str(exc), None + + +def fetch_account_stream( + acc: dict, + days_back: int, + limit: int, + known_keys: set[str], +) -> Iterator[dict]: + """Generator — yields progress dicts while fetching emails via IMAP. + + Mutates `known_keys` in place for cross-account dedup within one fetch session. + + Yields event dicts with "type" key: + {"type": "start", "account": str, "total_uids": int} + {"type": "progress", "account": str, "fetched": int, "total_uids": int} + {"type": "done", "account": str, "added": int, "skipped": int, "emails": list} + """ + name = acc.get("name", acc.get("username", "?")) + host = acc.get("host", "imap.gmail.com") + port = int(acc.get("port", 993)) + use_ssl = acc.get("use_ssl", True) + username = acc["username"] + password = acc["password"] + folder = acc.get("folder", "INBOX") + since = (datetime.now() - timedelta(days=days_back)).strftime("%d-%b-%Y") + + conn = (imaplib.IMAP4_SSL if use_ssl else imaplib.IMAP4)(host, port) + conn.login(username, password) + conn.select(folder, readonly=True) + + seen_uids: dict[bytes, None] = {} + for term in _WIDE_TERMS: + try: + _, data = conn.search(None, f'(SUBJECT "{term}" SINCE "{since}")') + for uid in (data[0] or b"").split(): + seen_uids[uid] = None + except Exception: + pass + + uids = list(seen_uids.keys())[: limit * 3] + yield {"type": "start", "account": name, "total_uids": len(uids)} + + emails: list[dict] = [] + skipped = 0 + for i, uid in enumerate(uids): + if len(emails) >= limit: + break + if i % 5 == 0: + yield {"type": "progress", "account": name, "fetched": len(emails), "total_uids": len(uids)} + try: + _, raw_data = conn.fetch(uid, "(RFC822)") + if not raw_data or not raw_data[0]: + continue + msg = _email_lib.message_from_bytes(raw_data[0][1]) + subj = _decode_str(msg.get("Subject", "")) + from_addr = _decode_str(msg.get("From", "")) + date = _decode_str(msg.get("Date", "")) + body = _extract_body(msg)[:800] + entry = {"subject": subj, "body": body, "from_addr": from_addr, + "date": date, "account": name} + k = entry_key(entry) + if k not in known_keys: + known_keys.add(k) + emails.append(entry) + else: + skipped += 1 + except Exception: + skipped += 1 + + try: + conn.logout() + except Exception: + pass + + yield {"type": "done", "account": name, "added": len(emails), "skipped": skipped, + "emails": emails} diff --git a/app/label_tool.py b/app/label_tool.py index 1340824..c86d09b 100644 --- a/app/label_tool.py +++ b/app/label_tool.py @@ -14,6 +14,7 @@ from __future__ import annotations import email as _email_lib import hashlib import html as _html +from html.parser import HTMLParser import imaplib import json import re @@ -23,6 +24,9 @@ from email.header import decode_header as _raw_decode from pathlib import Path from typing import Any +import os +import subprocess + import streamlit as st import yaml @@ -43,8 +47,9 @@ LABELS = [ "survey_received", "neutral", "event_rescheduled", - "unrelated", "digest", + "new_lead", + "hired", ] _LABEL_META: dict[str, dict] = { @@ -55,8 +60,9 @@ _LABEL_META: dict[str, dict] = { "survey_received": {"emoji": "📋", "color": "#9C27B0", "key": "5"}, "neutral": {"emoji": "⬜", "color": "#607D8B", "key": "6"}, "event_rescheduled": {"emoji": "🔄", "color": "#FF5722", "key": "7"}, - "unrelated": {"emoji": "🗑️", "color": "#757575", "key": "8"}, - "digest": {"emoji": "📰", "color": "#00BCD4", "key": "9"}, + "digest": {"emoji": "📰", "color": "#00BCD4", "key": "8"}, + "new_lead": {"emoji": "🤝", "color": "#009688", "key": "9"}, + "hired": {"emoji": "🎊", "color": "#FFC107", "key": "h"}, } # ── HTML sanitiser ─────────────────────────────────────────────────────────── @@ -78,7 +84,50 @@ def _to_html(text: str, newlines_to_br: bool = False) -> str: return escaped -# ── Wide IMAP search terms (cast a net across all 9 categories) ───────────── +# ── HTML → plain-text extractor ───────────────────────────────────────────── + +class _TextExtractor(HTMLParser): + """Extract visible text from an HTML email body, preserving line breaks.""" + _BLOCK = {"p","div","br","li","tr","h1","h2","h3","h4","h5","h6","blockquote"} + _SKIP = {"script","style","head","noscript"} + + def __init__(self): + super().__init__(convert_charrefs=True) + self._parts: list[str] = [] + self._depth_skip = 0 + + def handle_starttag(self, tag, attrs): + tag = tag.lower() + if tag in self._SKIP: + self._depth_skip += 1 + elif tag in self._BLOCK: + self._parts.append("\n") + + def handle_endtag(self, tag): + if tag.lower() in self._SKIP: + self._depth_skip = max(0, self._depth_skip - 1) + + def handle_data(self, data): + if not self._depth_skip: + self._parts.append(data) + + def get_text(self) -> str: + text = "".join(self._parts) + lines = [ln.strip() for ln in text.splitlines()] + return "\n".join(ln for ln in lines if ln) + + +def _strip_html(html_str: str) -> str: + """Convert HTML email body to plain text. Pure stdlib, no dependencies.""" + try: + extractor = _TextExtractor() + extractor.feed(html_str) + return extractor.get_text() + except Exception: + return re.sub(r"<[^>]+>", " ", html_str).strip() + + +# ── Wide IMAP search terms (cast a net across all 10 categories) ──────────── _WIDE_TERMS = [ # interview_scheduled "interview", "phone screen", "video call", "zoom link", "schedule a call", @@ -100,6 +149,11 @@ _WIDE_TERMS = [ # digest "job digest", "jobs you may like", "recommended jobs", "jobs for you", "new jobs", "job alert", + # new_lead + "came across your profile", "reaching out about", "great fit for a role", + "exciting opportunity", "love to connect", + # hired / onboarding + "welcome to the team", "start date", "onboarding", "first day", "we're excited to have you", # general recruitment "application", "recruiter", "recruiting", "hiring", "candidate", ] @@ -121,18 +175,32 @@ def _decode_str(value: str | None) -> str: def _extract_body(msg: Any) -> str: + """Return plain-text body. Strips HTML when no text/plain part exists.""" if msg.is_multipart(): + html_fallback: str | None = None for part in msg.walk(): - if part.get_content_type() == "text/plain": + ct = part.get_content_type() + if ct == "text/plain": try: charset = part.get_content_charset() or "utf-8" return part.get_payload(decode=True).decode(charset, errors="replace") except Exception: pass + elif ct == "text/html" and html_fallback is None: + try: + charset = part.get_content_charset() or "utf-8" + raw = part.get_payload(decode=True).decode(charset, errors="replace") + html_fallback = _strip_html(raw) + except Exception: + pass + return html_fallback or "" else: try: charset = msg.get_content_charset() or "utf-8" - return msg.get_payload(decode=True).decode(charset, errors="replace") + raw = msg.get_payload(decode=True).decode(charset, errors="replace") + if msg.get_content_type() == "text/html": + return _strip_html(raw) + return raw except Exception: pass return "" @@ -436,7 +504,9 @@ with st.sidebar: # ── Tabs ───────────────────────────────────────────────────────────────────── -tab_label, tab_fetch, tab_stats, tab_settings = st.tabs(["🃏 Label", "📥 Fetch", "📊 Stats", "⚙️ Settings"]) +tab_label, tab_fetch, tab_stats, tab_settings, tab_benchmark = st.tabs( + ["🃏 Label", "📥 Fetch", "📊 Stats", "⚙️ Settings", "🔬 Benchmark"] +) # ══════════════════════════════════════════════════════════════════════════════ @@ -669,19 +739,19 @@ with tab_label: _lbl_r = _r.get("label", "") _counts[_lbl_r] = _counts.get(_lbl_r, 0) + 1 - row1_cols = st.columns(3) - row2_cols = st.columns(3) - row3_cols = st.columns(3) + row1_cols = st.columns(5) + row2_cols = st.columns(5) bucket_pairs = [ (row1_cols[0], "interview_scheduled"), (row1_cols[1], "offer_received"), (row1_cols[2], "rejected"), - (row2_cols[0], "positive_response"), - (row2_cols[1], "survey_received"), - (row2_cols[2], "neutral"), - (row3_cols[0], "event_rescheduled"), - (row3_cols[1], "unrelated"), - (row3_cols[2], "digest"), + (row1_cols[3], "positive_response"), + (row1_cols[4], "survey_received"), + (row2_cols[0], "neutral"), + (row2_cols[1], "event_rescheduled"), + (row2_cols[2], "digest"), + (row2_cols[3], "new_lead"), + (row2_cols[4], "hired"), ] for col, lbl in bucket_pairs: m = _LABEL_META[lbl] @@ -720,7 +790,7 @@ with tab_label: nav_cols = st.columns([2, 1, 1, 1]) remaining = len(unlabeled) - 1 - nav_cols[0].caption(f"**{remaining}** remaining · Keys: 1–9 = label, 0 = other, S = skip, U = undo") + nav_cols[0].caption(f"**{remaining}** remaining · Keys: 1–9, H = label, 0 = other, S = skip, U = undo") if nav_cols[1].button("↩ Undo", disabled=not st.session_state.history, use_container_width=True): prev_idx, prev_label = st.session_state.history.pop() @@ -757,7 +827,7 @@ document.addEventListener('keydown', function(e) { const keyToLabel = { '1':'interview_scheduled','2':'offer_received','3':'rejected', '4':'positive_response','5':'survey_received','6':'neutral', - '7':'event_rescheduled','8':'unrelated','9':'digest' + '7':'event_rescheduled','8':'digest','9':'new_lead' }; const label = keyToLabel[e.key]; if (label) { @@ -772,6 +842,11 @@ document.addEventListener('keydown', function(e) { for (const btn of btns) { if (btn.innerText.includes('Other')) { btn.click(); break; } } + } else if (e.key.toLowerCase() === 'h') { + const btns = window.parent.document.querySelectorAll('button'); + for (const btn of btns) { + if (btn.innerText.toLowerCase().includes('hired')) { btn.click(); break; } + } } else if (e.key.toLowerCase() === 's') { const btns = window.parent.document.querySelectorAll('button'); for (const btn of btns) { @@ -979,3 +1054,133 @@ with tab_settings: if _k in ("settings_accounts", "settings_max") or _k.startswith("s_"): del st.session_state[_k] st.rerun() + + +# ══════════════════════════════════════════════════════════════════════════════ +# BENCHMARK TAB +# ══════════════════════════════════════════════════════════════════════════════ + +with tab_benchmark: + # ── Model selection ─────────────────────────────────────────────────────── + _DEFAULT_MODELS = [ + "deberta-zeroshot", "deberta-small", "gliclass-large", + "bart-mnli", "bge-m3-zeroshot", "deberta-small-2pass", "deberta-base-anli", + ] + _SLOW_MODELS = [ + "deberta-large-ling", "mdeberta-xnli-2m", "bge-reranker", + "deberta-xlarge", "mdeberta-mnli", "xlm-roberta-anli", + ] + + st.subheader("🔬 Benchmark Classifier Models") + + _b_include_slow = st.checkbox("Include slow / large models", value=False, key="b_include_slow") + _b_all_models = _DEFAULT_MODELS + (_SLOW_MODELS if _b_include_slow else []) + _b_selected = st.multiselect( + "Models to run", + options=_b_all_models, + default=_b_all_models, + help="Uncheck models to skip them. Slow models require --include-slow.", + ) + + _n_examples = len(st.session_state.labeled) + st.caption( + f"Scoring against `{_SCORE_FILE.name}` · **{_n_examples} labeled examples**" + f" · Est. time: ~{max(1, len(_b_selected))} – {max(2, len(_b_selected) * 2)} min" + ) + + # Direct binary avoids conda's output interception; -u = unbuffered stdout + _CLASSIFIER_PYTHON = "/devl/miniconda3/envs/job-seeker-classifiers/bin/python" + + if st.button("▶ Run Benchmark", type="primary", disabled=not _b_selected, key="b_run"): + _b_cmd = [ + _CLASSIFIER_PYTHON, "-u", + str(_ROOT / "scripts" / "benchmark_classifier.py"), + "--score", "--score-file", str(_SCORE_FILE), + "--models", *_b_selected, + ] + with st.status("Running benchmark…", expanded=True) as _b_status: + _b_proc = subprocess.Popen( + _b_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, cwd=str(_ROOT), + env={**os.environ, "PYTHONUNBUFFERED": "1"}, + ) + _b_lines: list[str] = [] + _b_area = st.empty() + for _b_line in _b_proc.stdout: + _b_lines.append(_b_line) + _b_area.code("".join(_b_lines[-30:]), language="text") + _b_proc.wait() + _b_full = "".join(_b_lines) + st.session_state["bench_output"] = _b_full + if _b_proc.returncode == 0: + _b_status.update(label="Benchmark complete ✓", state="complete", expanded=False) + else: + _b_status.update(label="Benchmark failed", state="error") + + # ── Results display ─────────────────────────────────────────────────────── + if "bench_output" in st.session_state: + _b_out = st.session_state["bench_output"] + + # Parse summary table rows: name f1 accuracy ms + _b_rows = [] + for _b_l in _b_out.splitlines(): + _b_m = re.match(r"^([\w-]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*$", _b_l.strip()) + if _b_m: + _b_rows.append({ + "Model": _b_m.group(1), + "macro-F1": float(_b_m.group(2)), + "Accuracy": float(_b_m.group(3)), + "ms/email": float(_b_m.group(4)), + }) + + if _b_rows: + import pandas as _pd + _b_df = _pd.DataFrame(_b_rows).sort_values("macro-F1", ascending=False).reset_index(drop=True) + st.dataframe( + _b_df, + column_config={ + "macro-F1": st.column_config.ProgressColumn( + "macro-F1", min_value=0, max_value=1, format="%.3f", + ), + "Accuracy": st.column_config.ProgressColumn( + "Accuracy", min_value=0, max_value=1, format="%.3f", + ), + "ms/email": st.column_config.NumberColumn("ms/email", format="%.1f"), + }, + use_container_width=True, hide_index=True, + ) + + with st.expander("Full benchmark output"): + st.code(_b_out, language="text") + + st.divider() + + # ── Tests ───────────────────────────────────────────────────────────────── + st.subheader("🧪 Run Tests") + st.caption("Runs `pytest tests/ -v` in the job-seeker env (no model downloads required).") + + if st.button("▶ Run Tests", key="b_run_tests"): + _t_cmd = [ + "/devl/miniconda3/envs/job-seeker/bin/pytest", "tests/", "-v", "--tb=short", + ] + with st.status("Running tests…", expanded=True) as _t_status: + _t_proc = subprocess.Popen( + _t_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, cwd=str(_ROOT), + ) + _t_lines: list[str] = [] + _t_area = st.empty() + for _t_line in _t_proc.stdout: + _t_lines.append(_t_line) + _t_area.code("".join(_t_lines[-30:]), language="text") + _t_proc.wait() + _t_full = "".join(_t_lines) + st.session_state["test_output"] = _t_full + _t_summary = [l for l in _t_lines if "passed" in l or "failed" in l or "error" in l.lower()] + _t_label = _t_summary[-1].strip() if _t_summary else "Done" + _t_state = "error" if _t_proc.returncode != 0 else "complete" + _t_status.update(label=_t_label, state=_t_state, expanded=False) + + if "test_output" in st.session_state: + with st.expander("Full test output", expanded=True): + st.code(st.session_state["test_output"], language="text") diff --git a/docs/plans/2026-03-08-anime-animation-design.md b/docs/plans/2026-03-08-anime-animation-design.md new file mode 100644 index 0000000..75b3d39 --- /dev/null +++ b/docs/plans/2026-03-08-anime-animation-design.md @@ -0,0 +1,95 @@ +# Anime.js Animation Integration — Design + +**Date:** 2026-03-08 +**Status:** Approved +**Branch:** feat/vue-label-tab + +## Problem + +The current animation system mixes CSS keyframes, CSS transitions, and imperative inline-style bindings across three files. The seams between systems produce: + +- Abrupt ball pickup (instant scale/borderRadius jump) +- No spring snap-back on release to no target +- Rigid CSS dismissals with no timing control +- Bucket grid and badge pop on basic `@keyframes` + +## Decision + +Integrate **Anime.js v4** as a single animation layer. Vue reactive state is unchanged; Anime.js owns all DOM motion imperatively. + +## Architecture + +One new composable, minimal changes to two existing files, CSS cleanup in two files. + +``` +web/src/composables/useCardAnimation.ts ← NEW +web/src/components/EmailCardStack.vue ← modify +web/src/views/LabelView.vue ← modify +``` + +**Data flow:** +``` +pointer events → Vue refs (isHeld, deltaX, deltaY, dismissType) + ↓ watched by + useCardAnimation(cardEl, stackEl, isHeld, ...) + ↓ imperatively drives + Anime.js → DOM transforms +``` + +`useCardAnimation` is a pure side-effect composable — returns nothing to the template. The `cardStyle` computed in `EmailCardStack.vue` is removed; Anime.js owns the element's transform directly. + +## Animation Surfaces + +### Pickup morph +``` +animate(cardEl, { scale: 0.55, borderRadius: '50%', y: -80 }, { duration: 200, ease: spring(1, 80, 10) }) +``` +Replaces the instant CSS transform jump on `onPointerDown`. + +### Drag tracking +Raw `cardEl.style.translate` update on `onPointerMove` — no animation, just position. Easing only at boundaries (pickup / release), not during active drag. + +### Snap-back +``` +animate(cardEl, { x: 0, y: 0, scale: 1, borderRadius: '1rem' }, { ease: spring(1, 80, 10) }) +``` +Fires on `onPointerUp` when no zone/bucket target was hit. + +### Dismissals (replace CSS `@keyframes`) +- **fileAway** — `animate(cardEl, { y: '-120%', scale: 0.85, opacity: 0 }, { duration: 280, ease: 'out(3)' })` +- **crumple** — 2-step timeline: shrink + redden → `scale(0)` + rotate +- **slideUnder** — `animate(cardEl, { x: '110%', rotate: 5, opacity: 0 }, { duration: 260 })` + +### Bucket grid rise +`animate(gridEl, { y: -8, opacity: 0.45 })` on `isHeld` → true; reversed on false. Spring easing. + +### Badge pop +`animate(badgeEl, { scale: [0.6, 1], opacity: [0, 1] }, { ease: spring(1.5, 80, 8), duration: 300 })` triggered on badge mount via Vue's `onMounted` lifecycle hook in a `BadgePop` wrapper component or `v-enter-active` transition hook. + +## Constraints + +### Reduced motion +`useCardAnimation` checks `motion.rich.value` before firing any Anime.js call. If false, all animations are skipped — instant state changes only. Consistent with existing `useMotion` pattern. + +### Bundle size +Anime.js v4 core ~17KB gzipped. Only `animate`, `spring`, and `createTimeline` are imported — Vite ESM tree-shaking keeps footprint minimal. The `draggable` module is not used. + +### Tests +Existing `EmailCardStack.test.ts` tests emit behavior, not animation — they remain passing. Anime.js mocked at module level in Vitest via `vi.mock('animejs')` where needed. + +### CSS cleanup +Remove from `EmailCardStack.vue` and `LabelView.vue`: +- `@keyframes fileAway`, `crumple`, `slideUnder` +- `@keyframes badge-pop` +- `.dismiss-label`, `.dismiss-skip`, `.dismiss-discard` classes (Anime.js fires on element refs directly) +- The `dismissClass` computed in `EmailCardStack.vue` + +## Files Changed + +| File | Change | +|------|--------| +| `web/package.json` | Add `animejs` dependency | +| `web/src/composables/useCardAnimation.ts` | New — all Anime.js animation logic | +| `web/src/components/EmailCardStack.vue` | Remove `cardStyle` computed + dismiss classes; call `useCardAnimation` | +| `web/src/views/LabelView.vue` | Badge pop + bucket grid rise via Anime.js | +| `web/src/assets/avocet.css` | Remove any global animation keyframes if present | diff --git a/docs/plans/2026-03-08-anime-animation-plan.md b/docs/plans/2026-03-08-anime-animation-plan.md new file mode 100644 index 0000000..9ef48f7 --- /dev/null +++ b/docs/plans/2026-03-08-anime-animation-plan.md @@ -0,0 +1,573 @@ +# Anime.js Animation Integration — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the current mixed CSS keyframes / inline-style animation system with Anime.js v4 for all card motion — pickup morph, drag tracking, spring snap-back, dismissals, bucket grid rise, and badge pop. + +**Architecture:** A new `useCardAnimation` composable owns all Anime.js calls imperatively against DOM refs. Vue reactive state (`isHeld`, `deltaX`, `deltaY`, `dismissType`) is unchanged. `cardStyle` computed and `dismissClass` computed are deleted; Anime.js writes to the element directly. + +**Tech Stack:** Anime.js v4 (`animejs`), Vue 3 Composition API, `@vue/test-utils` + Vitest for tests. + +--- + +## Task 1: Install Anime.js + +**Files:** +- Modify: `web/package.json` + +**Step 1: Install the package** + +```bash +cd /Library/Development/CircuitForge/avocet/web +npm install animejs +``` + +**Step 2: Verify the import resolves** + +Create a throwaway check — open `web/src/main.ts` briefly and confirm: +```ts +import { animate, spring } from 'animejs' +``` +resolves without error in the editor (TypeScript types ship with animejs v4). +Remove the import immediately after verifying — do not commit it. + +**Step 3: Commit** + +```bash +cd /Library/Development/CircuitForge/avocet/web +git add package.json package-lock.json +git commit -m "feat(avocet): add animejs v4 dependency" +``` + +--- + +## Task 2: Create `useCardAnimation` composable + +**Files:** +- Create: `web/src/composables/useCardAnimation.ts` +- Create: `web/src/composables/useCardAnimation.test.ts` + +**Background — Anime.js v4 transform model:** +Anime.js v4 tracks `x`, `y`, `scale`, `rotate`, etc. as separate transform components internally. +Use `utils.set(el, props)` for instant (no-animation) property updates — this keeps the internal cache consistent. +Never mix direct `el.style.transform = "..."` with Anime.js on the same element, or the cache desyncs. + +**Step 1: Write the failing tests** + +`web/src/composables/useCardAnimation.test.ts`: +```ts +import { ref, nextTick } from 'vue' +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// Mock animejs before importing the composable +vi.mock('animejs', () => ({ + animate: vi.fn(), + spring: vi.fn(() => 'mock-spring'), + utils: { set: vi.fn() }, +})) + +import { useCardAnimation } from './useCardAnimation' +import { animate, utils } from 'animejs' + +const mockAnimate = animate as ReturnType +const mockSet = utils.set as ReturnType + +function makeEl() { + return document.createElement('div') +} + +describe('useCardAnimation', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('pickup() calls animate with ball shape', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { pickup } = useCardAnimation(cardEl, motion) + pickup() + expect(mockAnimate).toHaveBeenCalledWith( + el, + expect.objectContaining({ scale: 0.55, borderRadius: '50%' }), + expect.anything(), + ) + }) + + it('pickup() is a no-op when motion.rich is false', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(false) } + const { pickup } = useCardAnimation(cardEl, motion) + pickup() + expect(mockAnimate).not.toHaveBeenCalled() + }) + + it('setDragPosition() calls utils.set with translated coords', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { setDragPosition } = useCardAnimation(cardEl, motion) + setDragPosition(50, 30) + expect(mockSet).toHaveBeenCalledWith(el, expect.objectContaining({ x: 50, y: -50 })) + // y = deltaY - 80 = 30 - 80 = -50 + }) + + it('snapBack() calls animate returning to card shape', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { snapBack } = useCardAnimation(cardEl, motion) + snapBack() + expect(mockAnimate).toHaveBeenCalledWith( + el, + expect.objectContaining({ x: 0, y: 0, scale: 1 }), + expect.anything(), + ) + }) + + it('animateDismiss("label") calls animate', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('label') + expect(mockAnimate).toHaveBeenCalled() + }) + + it('animateDismiss("discard") calls animate', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('discard') + expect(mockAnimate).toHaveBeenCalled() + }) + + it('animateDismiss("skip") calls animate', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('skip') + expect(mockAnimate).toHaveBeenCalled() + }) + + it('animateDismiss is a no-op when motion.rich is false', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(false) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('label') + expect(mockAnimate).not.toHaveBeenCalled() + }) +}) +``` + +**Step 2: Run tests to confirm they fail** + +```bash +cd /Library/Development/CircuitForge/avocet/web +npm test -- useCardAnimation +``` + +Expected: FAIL — "Cannot find module './useCardAnimation'" + +**Step 3: Implement the composable** + +`web/src/composables/useCardAnimation.ts`: +```ts +import { type Ref } from 'vue' +import { animate, spring, utils } from 'animejs' + +const BALL_SCALE = 0.55 +const BALL_RADIUS = '50%' +const CARD_RADIUS = '1rem' +const PICKUP_Y_OFFSET = 80 // px above finger +const PICKUP_DURATION = 200 +// NOTE: animejs v4 — spring() takes an object, not positional args +const SNAP_SPRING = spring({ mass: 1, stiffness: 80, damping: 10 }) + +interface Motion { rich: Ref } + +export function useCardAnimation( + cardEl: Ref, + motion: Motion, +) { + function pickup() { + if (!motion.rich.value || !cardEl.value) return + // NOTE: animejs v4 — animate() is 2-arg; timing options merge into the params object + animate(cardEl.value, { + scale: BALL_SCALE, + borderRadius: BALL_RADIUS, + y: -PICKUP_Y_OFFSET, + duration: PICKUP_DURATION, + ease: SNAP_SPRING, + }) + } + + function setDragPosition(dx: number, dy: number) { + if (!cardEl.value) return + utils.set(cardEl.value, { x: dx, y: dy - PICKUP_Y_OFFSET }) + } + + function snapBack() { + if (!motion.rich.value || !cardEl.value) return + // No duration — spring physics determines settling time + animate(cardEl.value, { + x: 0, + y: 0, + scale: 1, + borderRadius: CARD_RADIUS, + ease: SNAP_SPRING, + }) + } + + function animateDismiss(type: 'label' | 'skip' | 'discard') { + if (!motion.rich.value || !cardEl.value) return + const el = cardEl.value + if (type === 'label') { + animate(el, { y: '-120%', scale: 0.85, opacity: 0, duration: 280, ease: 'out(3)' }) + } else if (type === 'discard') { + // Two-step: crumple then shrink (keyframes array in params object) + animate(el, { keyframes: [ + { scale: 0.95, rotate: 2, filter: 'brightness(0.6) sepia(1) hue-rotate(-20deg)', duration: 140 }, + { scale: 0, rotate: 8, opacity: 0, duration: 210 }, + ]) + } else if (type === 'skip') { + animate(el, { x: '110%', rotate: 5, opacity: 0 }, { duration: 260, ease: 'out(2)' }) + } + } + + return { pickup, setDragPosition, snapBack, animateDismiss } +} +``` + +**Step 4: Run tests — expect pass** + +```bash +npm test -- useCardAnimation +``` + +Expected: All 8 tests PASS. + +**Step 5: Commit** + +```bash +git add web/src/composables/useCardAnimation.ts web/src/composables/useCardAnimation.test.ts +git commit -m "feat(avocet): add useCardAnimation composable with Anime.js" +``` + +--- + +## Task 3: Wire `useCardAnimation` into `EmailCardStack.vue` + +**Files:** +- Modify: `web/src/components/EmailCardStack.vue` +- Modify: `web/src/components/EmailCardStack.test.ts` + +**What changes:** +- Remove `cardStyle` computed and `:style="cardStyle"` binding +- Remove `dismissClass` computed and `:class="[dismissClass, ...]"` binding (keep `is-held`) +- Remove `deltaX`, `deltaY` reactive refs (position now owned by Anime.js) +- Call `pickup()` in `onPointerDown`, `setDragPosition()` in `onPointerMove`, `snapBack()` in `onPointerUp` (no-target path) +- Watch `props.dismissType` and call `animateDismiss()` +- Remove CSS `@keyframes fileAway`, `crumple`, `slideUnder` and their `.dismiss-*` rule blocks from `

visible

") + assert ".foo" not in result + assert "visible" in result + + +def test_strip_html_handles_br_as_newline(): + result = _strip_html("line1
line2") + assert "line1" in result + assert "line2" in result + + +def test_strip_html_decodes_entities(): + # convert_charrefs=True on HTMLParser handles & etc. + result = _strip_html("

Hello & welcome

") + assert "&" not in result + assert "Hello" in result + assert "welcome" in result + + +def test_strip_html_empty_string(): + assert _strip_html("") == "" + + +def test_strip_html_plain_text_passthrough(): + assert _strip_html("no tags here") == "no tags here" + + +# ── _extract_body ──────────────────────────────────────────────────────────── + +def test_extract_body_prefers_plain_over_html(): + msg = MIMEMultipart("alternative") + msg.attach(MIMEText("plain body", "plain")) + msg.attach(MIMEText("html body", "html")) + assert _extract_body(msg) == "plain body" + + +def test_extract_body_falls_back_to_html_when_no_plain(): + msg = MIMEMultipart("alternative") + msg.attach(MIMEText("

HTML only email

", "html")) + result = _extract_body(msg) + assert "HTML only email" in result + assert "<" not in result # no raw HTML tags leaked through + + +def test_extract_body_non_multipart_html_stripped(): + msg = MIMEText("

Solo HTML

", "html") + result = _extract_body(msg) + assert "Solo HTML" in result + assert "" not in result + + +def test_extract_body_non_multipart_plain_unchanged(): + msg = MIMEText("just plain text", "plain") + assert _extract_body(msg) == "just plain text" + + +def test_extract_body_empty_message(): + msg = MIMEText("", "plain") + assert _extract_body(msg) == "" + + +def test_extract_body_multipart_empty_returns_empty(): + msg = MIMEMultipart("alternative") + assert _extract_body(msg) == "" diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/web/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..e16603c --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,4939 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@fontsource/atkinson-hyperlegible": "^5.2.8", + "@fontsource/fraunces": "^5.2.9", + "@fontsource/jetbrains-mono": "^5.2.8", + "@vueuse/core": "^14.2.1", + "@vueuse/integrations": "^14.2.1", + "animejs": "^4.3.6", + "pinia": "^3.0.4", + "vue": "^3.5.25", + "vue-router": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@unocss/preset-attributify": "^66.6.4", + "@unocss/preset-wind": "^66.6.4", + "@vitejs/plugin-vue": "^6.0.2", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.8.1", + "jsdom": "^28.1.0", + "typescript": "~5.9.3", + "unocss": "^66.6.4", + "vite": "^7.3.1", + "vitest": "^4.0.18", + "vue-tsc": "^3.1.5" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.29.tgz", + "integrity": "sha512-jx9GjkkP5YHuTmko2eWAvpPnb0mB4mGRr2U7XwVNwevm8nlpobZEVk+GNmiYMk2VuA75v+plfXWyroWKmICZXg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "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/@fontsource/atkinson-hyperlegible": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/atkinson-hyperlegible/-/atkinson-hyperlegible-5.2.8.tgz", + "integrity": "sha512-HciLcJ5DIK/OVOdo71EbEN4NnvDFlp6/SpAxtcbWf2aAdcsOuPqITxj5KNEXb48qSPSdnnZdGGnSJChPKi3/bA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/fraunces": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@fontsource/fraunces/-/fraunces-5.2.9.tgz", + "integrity": "sha512-XDzuddBtoC7BZgZdBn6b7hsFZY2+V1hgN7yca5fBTKuHjb/lOd45a0Ji8dTUgFhPoL7RdGupo+bC2BFSt6UH8Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.115.0.tgz", + "integrity": "sha512-VoB2rhgoqgYf64d6Qs5emONQW8ASiTc0xp+aUE4JUhxjX+0pE3gblTYDO0upcN5vt9UlBNmUhAwfSifkfre7nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.115.0.tgz", + "integrity": "sha512-lWRX75u+gqfB4TF3pWCHuvhaeneAmRl2b2qNBcl4S6yJ0HtnT4VXOMEZrq747i4Zby1ZTxj6mtOe678Bg8gRLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.115.0.tgz", + "integrity": "sha512-ii/oOZjfGY1aszXTy29Z5DRyCEnBOrAXDVCvfdfXFQsOZlbbOa7NMHD7D+06YFe5qdxfmbWAYv4yn6QJi/0d2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.115.0.tgz", + "integrity": "sha512-R/sW/p8l77wglbjpMcF+h/3rWbp9zk1mRP3U14mxTYIC2k3m+aLBpXXgk2zksqf9qKk5mcc4GIYsuCn9l8TgDg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.115.0.tgz", + "integrity": "sha512-CSJ5ldNm9wIGGkhaIJeGmxRMZbgxThRN+X1ufYQQUNi5jZDV/U3C2QDMywpP93fczNBj961hXtcUPO/oVGq4Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.115.0.tgz", + "integrity": "sha512-uWFwssE5dHfQ8lH+ktrsD9JA49+Qa0gtxZHUs62z1e91NgGz6O7jefHGI6aygNyKNS45pnnBSDSP/zV977MsOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.115.0.tgz", + "integrity": "sha512-fZbqt8y/sKQ+v6bBCuv/mYYFoC0+fZI3mGDDEemmDOhT78+aUs2+4ZMdbd2btlXmnLaScl37r8IRbhnok5Ka9w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.115.0.tgz", + "integrity": "sha512-1ej/MjuTY9tJEunU/hUPIFmgH5PqgMQoRjNOvOkibtJ3Zqlw/+Lc+HGHDNET8sjbgIkWzdhX+p4J96A5CPdbag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.115.0.tgz", + "integrity": "sha512-HjsZbJPH9mMd4swJRywVMsDZsJX0hyKb1iNHo5ijRl5yhtbO3lj7ImSrrL1oZ1VEg0te4iKmDGGz/6YPLd1G8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.115.0.tgz", + "integrity": "sha512-zhhePoBrd7kQx3oClX/W6NldsuCbuMqaN9rRsY+6/WoorAb4j490PG/FjqgAXscWp2uSW2WV9L+ksn0wHrvsrg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.115.0.tgz", + "integrity": "sha512-t/IRojvUE9XrKu+/H1b8YINug+7Q6FLls5rsm2lxB5mnS8GN/eYAYrPgHkcg9/1SueRDSzGpDYu3lGWTObk1zw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.115.0.tgz", + "integrity": "sha512-79jBHSSh/YpQRAmvYoaCfpyToRbJ/HBrdB7hxK2ku2JMehjopTVo+xMJss/RV7/ZYqeezgjvKDQzapJbgcjVZA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.115.0.tgz", + "integrity": "sha512-nA1TpxkhNTIOMMyiSSsa7XIVJVoOU/SsVrHIz3gHvWweB5PHCQfO7w+Lb2EP0lBWokv7HtA/KbF7aLDoXzmuMw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.115.0.tgz", + "integrity": "sha512-9iVX789DoC3SaOOG+X6NcF/tVChgLp2vcHffzOC2/Z1JTPlz6bMG2ogvcW6/9s0BG2qvhNQImd+gbWYeQbOwVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.115.0.tgz", + "integrity": "sha512-RmQmk+mjCB0nMNfEYhaCxwofLo1Z95ebHw1AGvRiWGCd4zhCNOyskgCbMogIcQzSB3SuEKWgkssyaiQYVAA4hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.115.0.tgz", + "integrity": "sha512-viigraWWQhhDvX5aGq+wrQq58k00Xq3MHz/0R4AFMxGlZ8ogNonpEfNc73Q5Ly87Z6sU9BvxEdG0dnYTfVnmew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.115.0.tgz", + "integrity": "sha512-IzGCrMwXhpb4kTXy/8lnqqqwjI7eOvy+r9AhVw+hsr8t1ecBBEHprcNy0aKatFHN6hsX7UMHHQmBAQjVvL/p1A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.115.0.tgz", + "integrity": "sha512-/ym+Absk/TLFvbhh3se9XYuI1D7BrUVHw4RaG/2dmWKgBenrZHaJsgnRb7NJtaOyjEOLIPtULx1wDdVL0SX2eg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.115.0.tgz", + "integrity": "sha512-AQSZjIR+b+Te7uaO/hGTMjT8/oxlYrvKrOTi4KTHF/O6osjHEatUQ3y6ZW2+8+lJxy20zIcGz6iQFmFq/qDKkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.115.0.tgz", + "integrity": "sha512-oxUl82N+fIO9jIaXPph8SPPHQXrA08BHokBBJW8ct9F/x6o6bZE6eUAhUtWajbtvFhL8UYcCWRMba+kww6MBlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@unocss/cli": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/cli/-/cli-66.6.4.tgz", + "integrity": "sha512-jSeGL9a7tchoKvGQAsEdtjmvEu1axdikK5fdvmQnDOnLSM5Vo5wCthGYtsIIpQvb9HFBe0NupAJNwpjRBGiCaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@unocss/config": "66.6.4", + "@unocss/core": "66.6.4", + "@unocss/preset-wind3": "66.6.4", + "@unocss/preset-wind4": "66.6.4", + "@unocss/transformer-directives": "66.6.4", + "cac": "^6.7.14", + "chokidar": "^5.0.0", + "colorette": "^2.0.20", + "consola": "^3.4.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "tinyglobby": "^0.2.15", + "unplugin-utils": "^0.3.1" + }, + "bin": { + "unocss": "bin/unocss.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/cli/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@unocss/config": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/config/-/config-66.6.4.tgz", + "integrity": "sha512-iwHl5FG81cOAMalqigjw21Z2tMa0xjN0doQxnGOLx8KP+BllruXSjBj8CRk3m6Ny9fDxfpFY0ruYbIBA5AGwDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "colorette": "^2.0.20", + "consola": "^3.4.2", + "unconfig": "^7.5.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/core": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/core/-/core-66.6.4.tgz", + "integrity": "sha512-Fii3lhVJVFrKUz6hMGAkq3sXBfNnXB2G8bldNHuBHJpDAoP1F0oO/SU/oSqSjCYvtcD5RtOn8qwzcHuuN3B/mg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/extractor-arbitrary-variants": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-66.6.4.tgz", + "integrity": "sha512-l827c/UdE2FUBiaXDde5f/IjW41TflhtnjgQr3tJoCw7v9VuokDJFl+iOTyaH6AwMKpMeSBB+DU5Ippj4IOs9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/inspector": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/inspector/-/inspector-66.6.4.tgz", + "integrity": "sha512-q5oplYKCyO6YHN1MFQadkjs4fTTOKgsw0tXoSft6RLXowo8Utv6nBmED4yWb6Y6iYFmFU5RZ8VavxZvfghOlmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/rule-utils": "66.6.4", + "colorette": "^2.0.20", + "gzip-size": "^6.0.0", + "sirv": "^3.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-attributify": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-attributify/-/preset-attributify-66.6.4.tgz", + "integrity": "sha512-pksrugV/GqfgyUonHycxDvxUPVI3H9LiRcOEf1mZweD2qAqT6lH9qE1AHHddiZpWAcics4CkUkDpgXRwgt+wJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-icons": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-icons/-/preset-icons-66.6.4.tgz", + "integrity": "sha512-Xz8EQdPkANHlHUmWDw5/ehWTcn4tJeNltB4OnxI5vsi0hiqpLJxxKUE/vLUVH1I4GnVFCF4bBg7fmHanEcL0/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/utils": "^3.1.0", + "@unocss/core": "66.6.4", + "ofetch": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-mini": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-mini/-/preset-mini-66.6.4.tgz", + "integrity": "sha512-8xUXf/Bbi1/h98ldL56OxOnWUgWy0el0/xCGDLKYtBRUYGvZgrV+ys9UxY1/z+w7q+T+PZi+3qhc0O06nJ8wUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/extractor-arbitrary-variants": "66.6.4", + "@unocss/rule-utils": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-tagify": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-tagify/-/preset-tagify-66.6.4.tgz", + "integrity": "sha512-eWu9fH6c6gZH1FswMVPaX0kMS8Jw6dqDvlVLbjZgWraAHTon53lOnB2365bXgsl5zXYg30JGMzP/k171FJQWig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-typography": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-typography/-/preset-typography-66.6.4.tgz", + "integrity": "sha512-APtMRFUPA4e5S1Yyc3LWTqiy+XMq/SEMStkcGM6Rroy8Rzx+ItfqV/UrOWdg8gFYFPK8tVOvNG+40qlZy5Keww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/rule-utils": "66.6.4" + } + }, + "node_modules/@unocss/preset-uno": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-uno/-/preset-uno-66.6.4.tgz", + "integrity": "sha512-9BAprWrx6/leMaRBzH91vGYl4mEgIX/BP1h8ucEJ3aAo6dFrfmpC56HG7wOHNGMr4/uxm4aD7uI2SUpN+CBEEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/preset-wind3": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-web-fonts": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-web-fonts/-/preset-web-fonts-66.6.4.tgz", + "integrity": "sha512-N2qqeKf0W1mDXDBlXBdN32Dm6pLEbTFQsRe6WpX9SH5pCrEvuJG8cnIBPPpATLC+Qf2EWOepg1fIX+iWoF4Cyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "ofetch": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind/-/preset-wind-66.6.4.tgz", + "integrity": "sha512-OGeLXvcGQROuFrFmu/WOY8sbBvNBzAyi0firMY5AZhSkGmX/q4aBEJGGs3eiuMwg6JIhPg4QXzLjL7uWZJ0ZgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/preset-wind3": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind3": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind3/-/preset-wind3-66.6.4.tgz", + "integrity": "sha512-RxPR5czvE3RJ+eJoMM2AkPews7z4vSOeqTX8OIILzvEUFG1fRUvxMLaHGb4qstGPtHBJKrwNmvYjMozoiU2EgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/preset-mini": "66.6.4", + "@unocss/rule-utils": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind4": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind4/-/preset-wind4-66.6.4.tgz", + "integrity": "sha512-MvI3bxoOafEADiFJSHr7WB8nT01ZQvjsfWEuRNtNeRSTBVZ2QuJW8imL2sr9fk1qHoHmzN/3HefpTQoxiQWVcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/extractor-arbitrary-variants": "66.6.4", + "@unocss/rule-utils": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/rule-utils": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/rule-utils/-/rule-utils-66.6.4.tgz", + "integrity": "sha512-n/vCodRuzKtRBpZqd4OLVujDEJlPl11Iw5AtxB4GYsRT4AED/JY//XHLb5ubdLa1j3m84OAfnkT9Gr9sMWcwig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "^66.6.4", + "magic-string": "^0.30.21" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-attributify-jsx": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-66.6.4.tgz", + "integrity": "sha512-Rw9g3Ed/Et1W68znIuCod4OTLlOmuPpt2/6ZsylzNPEgGdJCHGYOdNs6Ai5IlbjrlOE4XfwK0O0iJQdk01V6FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "oxc-parser": "^0.115.0", + "oxc-walker": "^0.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-compile-class": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/transformer-compile-class/-/transformer-compile-class-66.6.4.tgz", + "integrity": "sha512-sZrPIp28xPnroT+BTX6onHfIXwjBkuPDyO3oKyciuCRZxGgTkV6GXV6lSGSu2EHFRjCmzzuCWgo33gU55TtklA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-directives": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/transformer-directives/-/transformer-directives-66.6.4.tgz", + "integrity": "sha512-IIczs0NZeEOIa/X28gkJevT6FtCWoMT3OmnMFDRi9plu3d7BYuQuBkBUYVyT7lIspn+iENCaXFl3e1l60e/xpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4", + "@unocss/rule-utils": "66.6.4", + "css-tree": "^3.1.0" + } + }, + "node_modules/@unocss/transformer-variant-group": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/transformer-variant-group/-/transformer-variant-group-66.6.4.tgz", + "integrity": "sha512-evAbg2fKuhJ0en71Y8iHJYbuED0SSiqg7BIajSbk0BQvy8N70wbu19Ljpjfc7JfcWV/vSWgNIklOr/TsYJhU6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/vite": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/@unocss/vite/-/vite-66.6.4.tgz", + "integrity": "sha512-qLSfJ2a0iDMhM/d3zpg9RQ7RW22tnP5hXARo430m9UK7bK1SmAbMAS70Wv2/FuRScBGLeMfluIuePghtuzgOLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@unocss/config": "66.6.4", + "@unocss/core": "66.6.4", + "@unocss/inspector": "66.6.4", + "chokidar": "^5.0.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.15", + "unplugin-utils": "^0.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "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-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", + "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz", + "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "vue": "3.5.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz", + "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/animejs": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/animejs/-/animejs-4.3.6.tgz", + "integrity": "sha512-rzZ4bDc8JAtyx6hYwxj7s5M/yWfnM5qqY4hZDnhy1cWFvMb6H5/necHS2sbCY3WQTDbRLuZL10dPXSxSCFOr/w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/juliangarnier" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", + "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "ast-kit": "^2.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/local-pkg/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/local-pkg/node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-regexp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz", + "integrity": "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12", + "mlly": "^1.7.2", + "regexp-tree": "^0.1.27", + "type-level-regexp": "~0.1.17", + "ufo": "^1.5.4", + "unplugin": "^2.0.0" + } + }, + "node_modules/magic-regexp/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/oxc-parser": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.115.0.tgz", + "integrity": "sha512-2w7Xn3CbS/zwzSY82S5WLemrRu3CT57uF7Lx8llrE/2bul6iMTcJE4Rbls7GDNbLn3ttATI68PfOz2Pt3KZ2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.115.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.115.0", + "@oxc-parser/binding-android-arm64": "0.115.0", + "@oxc-parser/binding-darwin-arm64": "0.115.0", + "@oxc-parser/binding-darwin-x64": "0.115.0", + "@oxc-parser/binding-freebsd-x64": "0.115.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.115.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.115.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.115.0", + "@oxc-parser/binding-linux-arm64-musl": "0.115.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.115.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.115.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.115.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.115.0", + "@oxc-parser/binding-linux-x64-gnu": "0.115.0", + "@oxc-parser/binding-linux-x64-musl": "0.115.0", + "@oxc-parser/binding-openharmony-arm64": "0.115.0", + "@oxc-parser/binding-wasm32-wasi": "0.115.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.115.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.115.0", + "@oxc-parser/binding-win32-x64-msvc": "0.115.0" + } + }, + "node_modules/oxc-walker": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-0.7.0.tgz", + "integrity": "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-regexp": "^0.10.0" + }, + "peerDependencies": { + "oxc-parser": ">=0.98.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.24", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.24.tgz", + "integrity": "sha512-1r6vQTTt1rUiJkI5vX7KG8PR342Ru/5Oh13kEQP2SMbRSZpOey9SrBe27IDxkoWulx8ShWu4K6C0BkctP8Z1bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.24" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.24", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.24.tgz", + "integrity": "sha512-pj7yygNMoMRqG7ML2SDQ0xNIOfN3IBDUcPVM2Sg6hP96oFNN2nqnzHreT3z9xLq85IWJyNTvD38O002DdOrPMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-level-regexp": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", + "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/unconfig": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.5.0.tgz", + "integrity": "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "defu": "^6.1.4", + "jiti": "^2.6.1", + "quansync": "^1.0.0", + "unconfig-core": "7.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unocss": { + "version": "66.6.4", + "resolved": "https://registry.npmjs.org/unocss/-/unocss-66.6.4.tgz", + "integrity": "sha512-W7BfUX2pw4cvUB8kq5CZro/TWM0LcXTjgwwmjowK5B/KVs0Sgc8vTaCr5wuyqNcDLLGAe/9oNPGsVgVBJQN6kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/cli": "66.6.4", + "@unocss/core": "66.6.4", + "@unocss/preset-attributify": "66.6.4", + "@unocss/preset-icons": "66.6.4", + "@unocss/preset-mini": "66.6.4", + "@unocss/preset-tagify": "66.6.4", + "@unocss/preset-typography": "66.6.4", + "@unocss/preset-uno": "66.6.4", + "@unocss/preset-web-fonts": "66.6.4", + "@unocss/preset-wind": "66.6.4", + "@unocss/preset-wind3": "66.6.4", + "@unocss/preset-wind4": "66.6.4", + "@unocss/transformer-attributify-jsx": "66.6.4", + "@unocss/transformer-compile-class": "66.6.4", + "@unocss/transformer-directives": "66.6.4", + "@unocss/transformer-variant-group": "66.6.4", + "@unocss/vite": "66.6.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@unocss/astro": "66.6.4", + "@unocss/postcss": "66.6.4", + "@unocss/webpack": "66.6.4" + }, + "peerDependenciesMeta": { + "@unocss/astro": { + "optional": true + }, + "@unocss/postcss": { + "optional": true + }, + "@unocss/webpack": { + "optional": true + } + } + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.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.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz", + "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.6", + "@vue-macros/common": "^3.1.1", + "@vue/devtools-api": "^8.0.6", + "ast-walker-scope": "^0.8.3", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1", + "yaml": "^2.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.17", + "pinia": "^3.0.4", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.7.tgz", + "integrity": "sha512-tc1TXAxclsn55JblLkFVcIRG7MeSJC4fWsPjfM7qu/IcmPUYnQ5Q8vzWwBpyDY24ZjmZTUCCwjRSNbx58IhlAA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.7" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-kit": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.7.tgz", + "integrity": "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.7", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-shared": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.7.tgz", + "integrity": "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", + "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.5" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..8d3abec --- /dev/null +++ b/web/package.json @@ -0,0 +1,38 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@fontsource/atkinson-hyperlegible": "^5.2.8", + "@fontsource/fraunces": "^5.2.9", + "@fontsource/jetbrains-mono": "^5.2.8", + "@vueuse/core": "^14.2.1", + "@vueuse/integrations": "^14.2.1", + "animejs": "^4.3.6", + "pinia": "^3.0.4", + "vue": "^3.5.25", + "vue-router": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@unocss/preset-attributify": "^66.6.4", + "@unocss/preset-wind": "^66.6.4", + "@vitejs/plugin-vue": "^6.0.2", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.8.1", + "jsdom": "^28.1.0", + "typescript": "~5.9.3", + "unocss": "^66.6.4", + "vite": "^7.3.1", + "vitest": "^4.0.18", + "vue-tsc": "^3.1.5" + } +} diff --git a/web/public/vite.svg b/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..f15fb5c --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/web/src/assets/avocet.css b/web/src/assets/avocet.css new file mode 100644 index 0000000..7a91dac --- /dev/null +++ b/web/src/assets/avocet.css @@ -0,0 +1,71 @@ +/* web/src/assets/avocet.css + Avocet token overrides — imports AFTER theme.css. + Only overrides what is genuinely different from the CircuitForge base theme. + Pattern mirrors peregrine.css — see peregrine/docs/plans/2026-03-03-nuxt-design-system.md. + + App colors: + Primary — Slate Teal (#2A6080) — inspired by avocet's slate-blue back plumage + deep water + Accent — Russet (#B8622A) — inspired by avocet's vivid orange-russet head +*/ + +/* ── Page-level overrides — must be in avocet.css (applied after theme.css base) ── */ +html { + /* Prevent Mac Chrome's horizontal swipe-to-navigate page animation + from triggering when the user scrolls near the viewport edge */ + overscroll-behavior-x: none; + /* clip (not hidden) — prevents overflowing content from expanding the html layout + width beyond the viewport. Without this, body's overflow-x:hidden propagates to + the viewport and body has no BFC, so long email URLs inflate the layout and + margin:0 auto centering drifts rightward as fonts load. */ + overflow-x: clip; +} + +body { + /* Prevent horizontal scroll from card swipe animations */ + overflow-x: hidden; +} + + +/* ── Light mode (default) ──────────────────────────── */ +:root { + /* Aliases bridging avocet component vars to CircuitForge base theme vars */ + --color-bg: var(--color-surface); /* App.vue body bg → #eaeff8 in light */ + --color-text-secondary: var(--color-text-muted); /* muted label text */ + /* Primary — Slate Teal */ + --app-primary: #2A6080; /* 4.8:1 on light surface #eaeff8 — ✅ AA */ + --app-primary-hover: #1E4D66; /* darker for hover */ + --app-primary-light: #E4F0F7; /* subtle bg tint — background use only */ + + /* Accent — Russet */ + --app-accent: #B8622A; /* 4.6:1 on light surface — ✅ AA */ + --app-accent-hover: #9A4E1F; /* darker for hover */ + --app-accent-light: #FAF0E8; /* subtle bg tint — background use only */ + + /* Text on accent buttons — dark navy, NOT white (russet bg only ~2.8:1 with white) */ + --app-accent-text: #1a2338; + + /* Avocet motion tokens */ + --swipe-exit: 300ms; + --swipe-spring: 400ms cubic-bezier(0.34, 1.56, 0.64, 1); /* card gestures */ + --bucket-expand: 250ms cubic-bezier(0.34, 1.56, 0.64, 1); /* label→bucket transform */ + --card-dismiss: 350ms ease-in; /* fileAway / crumple */ + --card-skip: 300ms ease-out; /* slideUnder */ +} + +/* ── Dark mode ─────────────────────────────────────── */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="hacker"]) { + /* Primary — lighter for legibility on dark surfaces */ + --app-primary: #5A9DBF; /* 6.2:1 on dark surface #16202e — ✅ AA */ + --app-primary-hover: #74B5D8; /* lighter for hover */ + --app-primary-light: #0D1F2D; /* subtle bg tint */ + + /* Accent — lighter russet */ + --app-accent: #D4854A; /* 5.4:1 on dark surface — ✅ AA */ + --app-accent-hover: #E8A060; /* lighter for hover */ + --app-accent-light: #2D1A08; /* subtle bg tint */ + + /* Dark text still needed on accent bg (dark russet bg + dark text ≈ 1.5:1 — use light) */ + --app-accent-text: #1a2338; /* in dark mode, russet is darker so dark text still works */ + } +} diff --git a/web/src/assets/theme.css b/web/src/assets/theme.css new file mode 100644 index 0000000..4bf7491 --- /dev/null +++ b/web/src/assets/theme.css @@ -0,0 +1,268 @@ +/* assets/styles/theme.css — CENTRAL THEME FILE + Accessible Solarpunk: warm, earthy, humanist, trustworthy. + Hacker mode: terminal green circuit-trace dark (Konami code). + ALL color/font/spacing tokens live here — nowhere else. +*/ + +/* ── Accessible Solarpunk — light (default) ──────── */ +:root { + /* Brand */ + --color-primary: #2d5a27; + --color-primary-hover: #234820; + --color-primary-light: #e8f2e7; + + /* Surfaces — cool blue-slate, crisp and legible */ + --color-surface: #eaeff8; + --color-surface-alt: #dde4f0; + --color-surface-raised: #f5f7fc; + + /* Borders — cool blue-gray */ + --color-border: #a8b8d0; + --color-border-light: #ccd5e6; + + /* Text — dark navy, cool undertone */ + --color-text: #1a2338; + --color-text-muted: #4a5c7a; + --color-text-inverse: #eaeff8; + + /* Accent — amber/terracotta (action, links, CTAs) */ + --color-accent: #c4732a; + --color-accent-hover: #a85c1f; + --color-accent-light: #fdf0e4; + + /* Semantic */ + --color-success: #3a7a32; + --color-error: #c0392b; + --color-warning: #d4891a; + --color-info: #1e6091; + + /* Typography */ + --font-display: 'Fraunces', Georgia, serif; /* Headings — optical humanist serif */ + --font-body: 'Atkinson Hyperlegible', system-ui, sans-serif; /* Body — designed for accessibility */ + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; /* Code, hacker mode */ + + /* Spacing scale */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-12: 3rem; + --space-16: 4rem; + --space-24: 6rem; + + /* Radii */ + --radius-sm: 0.25rem; + --radius-md: 0.5rem; + --radius-lg: 1rem; + --radius-full: 9999px; + + /* Shadows — cool blue-navy base */ + --shadow-sm: 0 1px 3px rgba(26, 35, 56, 0.08), 0 1px 2px rgba(26, 35, 56, 0.04); + --shadow-md: 0 4px 12px rgba(26, 35, 56, 0.1), 0 2px 4px rgba(26, 35, 56, 0.06); + --shadow-lg: 0 10px 30px rgba(26, 35, 56, 0.12), 0 4px 8px rgba(26, 35, 56, 0.06); + + /* Transitions */ + --transition: 200ms ease; + --transition-slow: 400ms ease; + + /* Header */ + --header-height: 4rem; + --header-border: 2px solid var(--color-border); +} + +/* ── Accessible Solarpunk — dark (system dark mode) ─ + Activates when OS/browser is in dark mode. + Uses :not([data-theme="hacker"]) so the Konami easter + egg always wins over the system preference. */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="hacker"]) { + /* Brand — lighter greens readable on dark surfaces */ + --color-primary: #6ab870; + --color-primary-hover: #7ecb84; + --color-primary-light: #162616; + + /* Surfaces — deep blue-slate, not pure black */ + --color-surface: #16202e; + --color-surface-alt: #1e2a3a; + --color-surface-raised: #263547; + + /* Borders */ + --color-border: #2d4060; + --color-border-light: #233352; + + /* Text */ + --color-text: #e4eaf5; + --color-text-muted: #8da0bc; + --color-text-inverse: #16202e; + + /* Accent — lighter amber for dark bg contrast (WCAG AA) */ + --color-accent: #e8a84a; + --color-accent-hover: #f5bc60; + --color-accent-light: #2d1e0a; + + /* Semantic */ + --color-success: #5eb85e; + --color-error: #e05252; + --color-warning: #e8a84a; + --color-info: #4da6e8; + + /* Shadows — darker base for dark bg */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.2); + --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.4), 0 4px 8px rgba(0, 0, 0, 0.2); + } +} + +/* ── Hacker/maker easter egg theme ──────────────── */ +/* Activated by Konami code: ↑↑↓↓←→←→BA */ +/* Stored in localStorage: 'cf-hacker-mode' */ +/* Applied: document.documentElement.dataset.theme */ +[data-theme="hacker"] { + --color-primary: #00ff41; + --color-primary-hover: #00cc33; + --color-primary-light: #001a00; + + --color-surface: #0a0c0a; + --color-surface-alt: #0d120d; + --color-surface-raised: #111811; + + --color-border: #1a3d1a; + --color-border-light: #123012; + + --color-text: #b8f5b8; + --color-text-muted: #5a9a5a; + --color-text-inverse: #0a0c0a; + + --color-accent: #00ff41; + --color-accent-hover: #00cc33; + --color-accent-light: #001a0a; + + --color-success: #00ff41; + --color-error: #ff3333; + --color-warning: #ffaa00; + --color-info: #00aaff; + + /* Hacker mode: mono font everywhere */ + --font-display: 'JetBrains Mono', monospace; + --font-body: 'JetBrains Mono', monospace; + + --shadow-sm: 0 1px 3px rgba(0, 255, 65, 0.08); + --shadow-md: 0 4px 12px rgba(0, 255, 65, 0.12); + --shadow-lg: 0 10px 30px rgba(0, 255, 65, 0.15); + + --header-border: 2px solid var(--color-border); + + /* Hacker glow variants — for box-shadow, text-shadow, bg overlays */ + --color-accent-glow-xs: rgba(0, 255, 65, 0.08); + --color-accent-glow-sm: rgba(0, 255, 65, 0.15); + --color-accent-glow-md: rgba(0, 255, 65, 0.4); + --color-accent-glow-lg: rgba(0, 255, 65, 0.6); +} + +/* ── Base resets ─────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } + +html { + font-family: var(--font-body); + color: var(--color-text); + background: var(--color-surface); + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { margin: 0; min-height: 100vh; } + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-display); + color: var(--color-primary); + line-height: 1.2; + margin: 0; +} + +/* Focus visible — keyboard nav — accessibility requirement */ +:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 3px; + border-radius: var(--radius-sm); +} + +/* Respect reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +/* ── Prose — CMS rich text ───────────────────────── */ +.prose { + font-family: var(--font-body); + line-height: 1.75; + color: var(--color-text); + max-width: 65ch; +} +.prose h2 { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 700; + margin: 2rem 0 0.75rem; + color: var(--color-primary); +} +.prose h3 { + font-family: var(--font-display); + font-size: 1.2rem; + font-weight: 600; + margin: 1.5rem 0 0.5rem; + color: var(--color-primary); +} +.prose p { margin: 0 0 1rem; } +.prose ul, .prose ol { margin: 0 0 1rem; padding-left: 1.5rem; } +.prose li { margin-bottom: 0.4rem; } +.prose a { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; } +.prose strong { font-weight: 700; } +.prose code { + font-family: var(--font-mono); + font-size: 0.875em; + background: var(--color-surface-alt); + border: 1px solid var(--color-border-light); + padding: 0.1em 0.35em; + border-radius: var(--radius-sm); +} +.prose blockquote { + border-left: 3px solid var(--color-accent); + margin: 1.5rem 0; + padding: 0.5rem 0 0.5rem 1.25rem; + color: var(--color-text-muted); + font-style: italic; +} + +/* ── Utility: screen reader only ────────────────── */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +.sr-only:focus-visible { + position: fixed; + top: 0.5rem; + left: 0.5rem; + width: auto; + height: auto; + padding: 0.5rem 1rem; + clip: auto; + white-space: normal; + background: var(--color-accent); + color: var(--color-text-inverse); + border-radius: var(--radius-md); + font-weight: 600; + z-index: 9999; +} diff --git a/web/src/assets/vue.svg b/web/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/web/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/AppSidebar.vue b/web/src/components/AppSidebar.vue new file mode 100644 index 0000000..f691c9b --- /dev/null +++ b/web/src/components/AppSidebar.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/web/src/components/EmailCard.test.ts b/web/src/components/EmailCard.test.ts new file mode 100644 index 0000000..8d83fa5 --- /dev/null +++ b/web/src/components/EmailCard.test.ts @@ -0,0 +1,39 @@ +import { mount } from '@vue/test-utils' +import EmailCard from './EmailCard.vue' +import { describe, it, expect } from 'vitest' + +const item = { + id: 'abc', subject: 'Interview Invitation', + body: 'Hi there, we would like to schedule a phone screen with you. This will be a 30-minute call.', + from: 'recruiter@acme.com', date: '2026-03-01', source: 'imap:test', +} + +describe('EmailCard', () => { + it('renders subject', () => { + const w = mount(EmailCard, { props: { item } }) + expect(w.text()).toContain('Interview Invitation') + }) + + it('renders from and date', () => { + const w = mount(EmailCard, { props: { item } }) + expect(w.text()).toContain('recruiter@acme.com') + expect(w.text()).toContain('2026-03-01') + }) + + it('renders truncated body by default', () => { + const w = mount(EmailCard, { props: { item } }) + expect(w.text()).toContain('Hi there') + }) + + it('emits expand on button click', async () => { + const w = mount(EmailCard, { props: { item } }) + await w.find('[data-testid="expand-btn"]').trigger('click') + expect(w.emitted('expand')).toBeTruthy() + }) + + it('shows collapse button when expanded', () => { + const w = mount(EmailCard, { props: { item, expanded: true } }) + expect(w.find('[data-testid="collapse-btn"]').exists()).toBe(true) + expect(w.find('[data-testid="expand-btn"]').exists()).toBe(false) + }) +}) diff --git a/web/src/components/EmailCard.vue b/web/src/components/EmailCard.vue new file mode 100644 index 0000000..beb994d --- /dev/null +++ b/web/src/components/EmailCard.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/web/src/components/EmailCardStack.test.ts b/web/src/components/EmailCardStack.test.ts new file mode 100644 index 0000000..1ffa968 --- /dev/null +++ b/web/src/components/EmailCardStack.test.ts @@ -0,0 +1,183 @@ +import { mount } from '@vue/test-utils' +import EmailCardStack from './EmailCardStack.vue' +import { describe, it, expect, vi } from 'vitest' + +vi.mock('../composables/useCardAnimation', () => ({ + useCardAnimation: vi.fn(() => ({ + pickup: vi.fn(), + setDragPosition: vi.fn(), + snapBack: vi.fn(), + animateDismiss: vi.fn(), + updateAura: vi.fn(), + reset: vi.fn(), + })), +})) + +import { useCardAnimation } from '../composables/useCardAnimation' +import { nextTick } from 'vue' + +const item = { + id: 'abc', + subject: 'Interview at Acme', + body: 'We would like to schedule...', + from: 'hr@acme.com', + date: '2026-03-01', + source: 'imap:test', +} + +describe('EmailCardStack', () => { + it('renders the email subject', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + expect(w.text()).toContain('Interview at Acme') + }) + + it('renders shadow cards for depth effect', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + expect(w.findAll('.card-shadow')).toHaveLength(2) + }) + + it('calls animateDismiss with type when dismissType prop changes', async () => { + ;(useCardAnimation as ReturnType).mockClear() + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: null } }) + const { animateDismiss } = (useCardAnimation as ReturnType).mock.results[0].value + await w.setProps({ dismissType: 'label' }) + await nextTick() + expect(animateDismiss).toHaveBeenCalledWith('label') + }) + + // JSDOM doesn't implement setPointerCapture — mock it on the element. + // Also use dispatchEvent(new PointerEvent) directly because @vue/test-utils + // .trigger() tries to assign clientX on a MouseEvent (read-only in JSDOM). + function mockPointerCapture(element: Element) { + ;(element as any).setPointerCapture = vi.fn() + ;(element as any).releasePointerCapture = vi.fn() + } + + function fire(element: Element, type: string, init: PointerEventInit) { + element.dispatchEvent(new PointerEvent(type, { bubbles: true, ...init })) + } + + it('emits drag-start on pointerdown', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + fire(el, 'pointerdown', { pointerId: 1, clientX: 200, clientY: 300 }) + await w.vm.$nextTick() + expect(w.emitted('drag-start')).toBeTruthy() + }) + + it('emits drag-end on pointerup', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + fire(el, 'pointerdown', { pointerId: 1, clientX: 200, clientY: 300 }) + fire(el, 'pointerup', { pointerId: 1, clientX: 200, clientY: 300 }) + await w.vm.$nextTick() + expect(w.emitted('drag-end')).toBeTruthy() + }) + + it('emits discard when released in left zone (x < 7% viewport)', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + // JSDOM window.innerWidth defaults to 1024; 7% = 71.7px + fire(el, 'pointerdown', { pointerId: 1, clientX: 512, clientY: 300 }) + fire(el, 'pointermove', { pointerId: 1, clientX: 30, clientY: 300 }) + fire(el, 'pointerup', { pointerId: 1, clientX: 30, clientY: 300 }) + await w.vm.$nextTick() + expect(w.emitted('discard')).toBeTruthy() + }) + + it('emits skip when released in right zone (x > 93% viewport)', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + // JSDOM window.innerWidth defaults to 1024; 93% = 952px + fire(el, 'pointerdown', { pointerId: 1, clientX: 512, clientY: 300 }) + fire(el, 'pointermove', { pointerId: 1, clientX: 1000, clientY: 300 }) + fire(el, 'pointerup', { pointerId: 1, clientX: 1000, clientY: 300 }) + await w.vm.$nextTick() + expect(w.emitted('skip')).toBeTruthy() + }) + + it('does not emit action on pointerup without movement past zone', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + fire(el, 'pointerdown', { pointerId: 1, clientX: 512, clientY: 300 }) + fire(el, 'pointerup', { pointerId: 1, clientX: 512, clientY: 300 }) + await w.vm.$nextTick() + expect(w.emitted('discard')).toBeFalsy() + expect(w.emitted('skip')).toBeFalsy() + expect(w.emitted('label')).toBeFalsy() + }) + + // Fling tests — mock performance.now() to control timestamps between events + it('emits discard on fast leftward fling (option B: speed + alignment)', async () => { + let mockTime = 0 + vi.spyOn(performance, 'now').mockImplementation(() => mockTime) + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + mockTime = 0; fire(el, 'pointerdown', { pointerId: 1, clientX: 512, clientY: 300 }) + mockTime = 30; fire(el, 'pointermove', { pointerId: 1, clientX: 400, clientY: 310 }) + mockTime = 50; fire(el, 'pointermove', { pointerId: 1, clientX: 288, clientY: 320 }) + // vx = (288-400)/(50-30)*1000 = -5600 px/s, vy ≈ 500 px/s + // speed ≈ 5622 px/s > 600, alignment = 5600/5622 ≈ 0.996 > 0.707 ✓ + fire(el, 'pointerup', { pointerId: 1, clientX: 288, clientY: 320 }) + await w.vm.$nextTick() + expect(w.emitted('discard')).toBeTruthy() + vi.restoreAllMocks() + }) + + it('emits skip on fast rightward fling', async () => { + let mockTime = 0 + vi.spyOn(performance, 'now').mockImplementation(() => mockTime) + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + mockTime = 0; fire(el, 'pointerdown', { pointerId: 1, clientX: 512, clientY: 300 }) + mockTime = 30; fire(el, 'pointermove', { pointerId: 1, clientX: 624, clientY: 310 }) + mockTime = 50; fire(el, 'pointermove', { pointerId: 1, clientX: 736, clientY: 320 }) + // vx = (736-624)/(50-30)*1000 = 5600 px/s — mirror of discard case + fire(el, 'pointerup', { pointerId: 1, clientX: 736, clientY: 320 }) + await w.vm.$nextTick() + expect(w.emitted('skip')).toBeTruthy() + vi.restoreAllMocks() + }) + + it('does not fling on diagonal swipe (alignment < 0.707)', async () => { + let mockTime = 0 + vi.spyOn(performance, 'now').mockImplementation(() => mockTime) + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + mockTime = 0; fire(el, 'pointerdown', { pointerId: 1, clientX: 512, clientY: 300 }) + mockTime = 30; fire(el, 'pointermove', { pointerId: 1, clientX: 400, clientY: 150 }) + mockTime = 50; fire(el, 'pointermove', { pointerId: 1, clientX: 288, clientY: 0 }) + // vx = -5600 px/s, vy = -7500 px/s, speed ≈ 9356 px/s + // alignment = 5600/9356 ≈ 0.598 < 0.707 — too diagonal ✓ + fire(el, 'pointerup', { pointerId: 1, clientX: 288, clientY: 0 }) + await w.vm.$nextTick() + expect(w.emitted('discard')).toBeFalsy() + expect(w.emitted('skip')).toBeFalsy() + vi.restoreAllMocks() + }) + + it('does not fling on slow movement (speed < threshold)', async () => { + let mockTime = 0 + vi.spyOn(performance, 'now').mockImplementation(() => mockTime) + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + const el = w.find('.card-wrapper').element + mockPointerCapture(el) + mockTime = 0; fire(el, 'pointerdown', { pointerId: 1, clientX: 512, clientY: 300 }) + mockTime = 100; fire(el, 'pointermove', { pointerId: 1, clientX: 480, clientY: 300 }) + mockTime = 200; fire(el, 'pointermove', { pointerId: 1, clientX: 450, clientY: 300 }) + // vx = (450-480)/(200-100)*1000 = -300 px/s < 600 threshold + fire(el, 'pointerup', { pointerId: 1, clientX: 450, clientY: 300 }) + await w.vm.$nextTick() + expect(w.emitted('discard')).toBeFalsy() + expect(w.emitted('skip')).toBeFalsy() + vi.restoreAllMocks() + }) +}) diff --git a/web/src/components/EmailCardStack.vue b/web/src/components/EmailCardStack.vue new file mode 100644 index 0000000..2abc6c3 --- /dev/null +++ b/web/src/components/EmailCardStack.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/web/src/components/HelloWorld.vue b/web/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/web/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/web/src/components/LabelBucketGrid.test.ts b/web/src/components/LabelBucketGrid.test.ts new file mode 100644 index 0000000..fb09e8c --- /dev/null +++ b/web/src/components/LabelBucketGrid.test.ts @@ -0,0 +1,52 @@ +import { mount } from '@vue/test-utils' +import LabelBucketGrid from './LabelBucketGrid.vue' +import { describe, it, expect } from 'vitest' + +const labels = [ + { name: 'interview_scheduled', emoji: '🗓️', color: '#4CAF50', key: '1' }, + { name: 'offer_received', emoji: '🎉', color: '#2196F3', key: '2' }, + { name: 'rejected', emoji: '❌', color: '#F44336', key: '3' }, +] + +describe('LabelBucketGrid', () => { + it('renders all labels', () => { + const w = mount(LabelBucketGrid, { props: { labels, isBucketMode: false } }) + expect(w.findAll('[data-testid="label-btn"]')).toHaveLength(3) + }) + + it('emits label event on click', async () => { + const w = mount(LabelBucketGrid, { props: { labels, isBucketMode: false } }) + await w.find('[data-testid="label-btn"]').trigger('click') + expect(w.emitted('label')?.[0]).toEqual(['interview_scheduled']) + }) + + it('applies bucket-mode class when isBucketMode is true', () => { + const w = mount(LabelBucketGrid, { props: { labels, isBucketMode: true } }) + expect(w.find('.label-grid').classes()).toContain('bucket-mode') + }) + + it('shows key hint and emoji', () => { + const w = mount(LabelBucketGrid, { props: { labels, isBucketMode: false } }) + const btn = w.find('[data-testid="label-btn"]') + expect(btn.text()).toContain('1') + expect(btn.text()).toContain('🗓️') + }) + + it('marks button as drop-target when hoveredBucket matches label name', () => { + const w = mount(LabelBucketGrid, { + props: { labels, isBucketMode: true, hoveredBucket: 'interview_scheduled' }, + }) + const btns = w.findAll('[data-testid="label-btn"]') + expect(btns[0].classes()).toContain('is-drop-target') + expect(btns[1].classes()).not.toContain('is-drop-target') + }) + + it('no button marked as drop-target when hoveredBucket is null', () => { + const w = mount(LabelBucketGrid, { + props: { labels, isBucketMode: false, hoveredBucket: null }, + }) + w.findAll('[data-testid="label-btn"]').forEach(btn => { + expect(btn.classes()).not.toContain('is-drop-target') + }) + }) +}) diff --git a/web/src/components/LabelBucketGrid.vue b/web/src/components/LabelBucketGrid.vue new file mode 100644 index 0000000..ebd1500 --- /dev/null +++ b/web/src/components/LabelBucketGrid.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/web/src/components/UndoToast.test.ts b/web/src/components/UndoToast.test.ts new file mode 100644 index 0000000..b5a2b30 --- /dev/null +++ b/web/src/components/UndoToast.test.ts @@ -0,0 +1,89 @@ +import { mount } from '@vue/test-utils' +import UndoToast from './UndoToast.vue' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock requestAnimationFrame for jsdom +beforeEach(() => { + vi.stubGlobal('requestAnimationFrame', (fn: FrameRequestCallback) => { + // Call with a fake timestamp to simulate one frame + setTimeout(() => fn(16), 0) + return 1 + }) + vi.stubGlobal('cancelAnimationFrame', vi.fn()) +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +const labelAction = { + type: 'label' as const, + item: { id: 'abc', subject: 'Interview at Acme', body: '...', from: 'hr@acme.com', date: '2026-03-01', source: 'imap:test' }, + label: 'interview_scheduled', +} + +const skipAction = { + type: 'skip' as const, + item: { id: 'xyz', subject: 'Cold Outreach', body: '...', from: 'recruiter@x.com', date: '2026-03-01', source: 'imap:test' }, +} + +const discardAction = { + type: 'discard' as const, + item: { id: 'def', subject: 'Spam Email', body: '...', from: 'spam@spam.com', date: '2026-03-01', source: 'imap:test' }, +} + +describe('UndoToast', () => { + it('renders subject for a label action', () => { + const w = mount(UndoToast, { props: { action: labelAction } }) + expect(w.text()).toContain('Interview at Acme') + expect(w.text()).toContain('interview_scheduled') + }) + + it('renders subject for a skip action', () => { + const w = mount(UndoToast, { props: { action: skipAction } }) + expect(w.text()).toContain('Cold Outreach') + expect(w.text()).toContain('Skipped') + }) + + it('renders subject for a discard action', () => { + const w = mount(UndoToast, { props: { action: discardAction } }) + expect(w.text()).toContain('Spam Email') + expect(w.text()).toContain('Discarded') + }) + + it('has undo button', () => { + const w = mount(UndoToast, { props: { action: labelAction } }) + expect(w.find('.undo-btn').exists()).toBe(true) + }) + + it('emits undo when button clicked', async () => { + const w = mount(UndoToast, { props: { action: labelAction } }) + await w.find('.undo-btn').trigger('click') + expect(w.emitted('undo')).toBeTruthy() + }) + + it('has timer bar element', () => { + const w = mount(UndoToast, { props: { action: labelAction } }) + expect(w.find('.timer-bar').exists()).toBe(true) + }) + + it('has accessible role=status', () => { + const w = mount(UndoToast, { props: { action: labelAction } }) + expect(w.find('[role="status"]').exists()).toBe(true) + }) + + it('emits expire when tick fires with timestamp beyond DURATION', async () => { + let capturedTick: FrameRequestCallback | null = null + vi.stubGlobal('requestAnimationFrame', (fn: FrameRequestCallback) => { + capturedTick = fn + return 1 + }) + vi.spyOn(performance, 'now').mockReturnValue(0) + const w = mount(UndoToast, { props: { action: labelAction } }) + await import('vue').then(v => v.nextTick()) + // Simulate a tick timestamp 6 seconds in — beyond the 5-second DURATION + if (capturedTick) capturedTick(6000) + await import('vue').then(v => v.nextTick()) + expect(w.emitted('expire')).toBeTruthy() + }) +}) diff --git a/web/src/components/UndoToast.vue b/web/src/components/UndoToast.vue new file mode 100644 index 0000000..aa02a04 --- /dev/null +++ b/web/src/components/UndoToast.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/web/src/composables/useApi.ts b/web/src/composables/useApi.ts new file mode 100644 index 0000000..2965d8e --- /dev/null +++ b/web/src/composables/useApi.ts @@ -0,0 +1,50 @@ +export type ApiError = + | { kind: 'network'; message: string } + | { kind: 'http'; status: number; detail: string } + +export async function useApiFetch( + url: string, + opts?: RequestInit, +): Promise<{ data: T | null; error: ApiError | null }> { + try { + const res = await fetch(url, opts) + if (!res.ok) { + const detail = await res.text().catch(() => '') + return { data: null, error: { kind: 'http', status: res.status, detail } } + } + const data = await res.json() as T + return { data, error: null } + } catch (e) { + return { data: null, error: { kind: 'network', message: String(e) } } + } +} + +/** + * Open an SSE connection. Returns a cleanup function. + * onEvent receives each parsed JSON payload. + * onComplete is called when the server sends a {"type":"complete"} event. + * onError is called on connection error. + */ +export function useApiSSE( + url: string, + onEvent: (data: Record) => void, + onComplete?: () => void, + onError?: (e: Event) => void, +): () => void { + const es = new EventSource(url) + es.onmessage = (e) => { + try { + const data = JSON.parse(e.data) as Record + onEvent(data) + if (data.type === 'complete') { + es.close() + onComplete?.() + } + } catch { /* ignore malformed events */ } + } + es.onerror = (e) => { + onError?.(e) + es.close() + } + return () => es.close() +} diff --git a/web/src/composables/useCardAnimation.test.ts b/web/src/composables/useCardAnimation.test.ts new file mode 100644 index 0000000..fb11a25 --- /dev/null +++ b/web/src/composables/useCardAnimation.test.ts @@ -0,0 +1,142 @@ +import { ref } from 'vue' +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// Mock animejs before importing the composable +vi.mock('animejs', () => ({ + animate: vi.fn(), + spring: vi.fn(() => 'mock-spring'), + utils: { set: vi.fn() }, +})) + +import { useCardAnimation } from './useCardAnimation' +import { animate, utils } from 'animejs' + +const mockAnimate = animate as ReturnType +const mockSet = utils.set as ReturnType + +function makeEl() { + return document.createElement('div') +} + +describe('useCardAnimation', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('pickup() calls animate with ball shape', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { pickup } = useCardAnimation(cardEl, motion) + pickup() + expect(mockAnimate).toHaveBeenCalledWith( + el, + expect.objectContaining({ scale: 0.55, borderRadius: '50%' }), + ) + }) + + it('pickup() is a no-op when motion.rich is false', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(false) } + const { pickup } = useCardAnimation(cardEl, motion) + pickup() + expect(mockAnimate).not.toHaveBeenCalled() + }) + + it('setDragPosition() calls utils.set with translated coords', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { setDragPosition } = useCardAnimation(cardEl, motion) + setDragPosition(50, 30) + expect(mockSet).toHaveBeenCalledWith(el, expect.objectContaining({ x: 50, y: -50 })) + // y = deltaY - 80 = 30 - 80 = -50 + }) + + it('snapBack() calls animate returning to card shape', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { snapBack } = useCardAnimation(cardEl, motion) + snapBack() + expect(mockAnimate).toHaveBeenCalledWith( + el, + expect.objectContaining({ x: 0, y: 0, scale: 1 }), + ) + }) + + it('animateDismiss("label") calls animate', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('label') + expect(mockAnimate).toHaveBeenCalled() + }) + + it('animateDismiss("discard") calls animate', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('discard') + expect(mockAnimate).toHaveBeenCalled() + }) + + it('animateDismiss("skip") calls animate', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('skip') + expect(mockAnimate).toHaveBeenCalled() + }) + + it('animateDismiss is a no-op when motion.rich is false', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(false) } + const { animateDismiss } = useCardAnimation(cardEl, motion) + animateDismiss('label') + expect(mockAnimate).not.toHaveBeenCalled() + }) + + describe('updateAura', () => { + it('sets red background for discard zone', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { updateAura } = useCardAnimation(cardEl, motion) + updateAura('discard', null) + expect(mockSet).toHaveBeenCalledWith(el, expect.objectContaining({ background: 'rgba(244, 67, 54, 0.25)' })) + }) + + it('sets orange background for skip zone', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { updateAura } = useCardAnimation(cardEl, motion) + updateAura('skip', null) + expect(mockSet).toHaveBeenCalledWith(el, expect.objectContaining({ background: 'rgba(255, 152, 0, 0.25)' })) + }) + + it('sets blue background for bucket hover', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { updateAura } = useCardAnimation(cardEl, motion) + updateAura(null, 'interview_scheduled') + expect(mockSet).toHaveBeenCalledWith(el, expect.objectContaining({ background: 'rgba(42, 96, 128, 0.20)' })) + }) + + it('sets transparent background when no zone/bucket', () => { + const el = makeEl() + const cardEl = ref(el) + const motion = { rich: ref(true) } + const { updateAura } = useCardAnimation(cardEl, motion) + updateAura(null, null) + expect(mockSet).toHaveBeenCalledWith(el, expect.objectContaining({ background: 'transparent' })) + }) + }) +}) diff --git a/web/src/composables/useCardAnimation.ts b/web/src/composables/useCardAnimation.ts new file mode 100644 index 0000000..c5e3e45 --- /dev/null +++ b/web/src/composables/useCardAnimation.ts @@ -0,0 +1,99 @@ +import { type Ref } from 'vue' +import { animate, spring, utils } from 'animejs' + +const BALL_SCALE = 0.55 +const BALL_RADIUS = '50%' +const CARD_RADIUS = '1rem' +const PICKUP_Y_OFFSET = 80 // px above finger +const PICKUP_DURATION = 200 + +// Anime.js v4: spring() takes an object { mass, stiffness, damping, velocity } +const SNAP_SPRING = spring({ mass: 1, stiffness: 80, damping: 10 }) + +interface Motion { rich: Ref } + +export function useCardAnimation( + cardEl: Ref, + motion: Motion, +) { + function pickup() { + if (!motion.rich.value || !cardEl.value) return + // Anime.js v4: animate(target, params) — all props + timing in one object + animate(cardEl.value, { + scale: BALL_SCALE, + borderRadius: BALL_RADIUS, + y: -PICKUP_Y_OFFSET, + duration: PICKUP_DURATION, + ease: SNAP_SPRING, + }) + } + + function setDragPosition(dx: number, dy: number) { + if (!cardEl.value) return + // utils.set() for instant (no-animation) position update — keeps Anime cache consistent + utils.set(cardEl.value, { x: dx, y: dy - PICKUP_Y_OFFSET }) + } + + function snapBack() { + if (!motion.rich.value || !cardEl.value) return + animate(cardEl.value, { + x: 0, + y: 0, + scale: 1, + borderRadius: CARD_RADIUS, + ease: SNAP_SPRING, + }) + } + + function animateDismiss(type: 'label' | 'skip' | 'discard') { + if (!motion.rich.value || !cardEl.value) return + const el = cardEl.value + if (type === 'label') { + animate(el, { y: '-120%', scale: 0.85, opacity: 0, duration: 280, ease: 'out(3)' }) + } else if (type === 'discard') { + // Anime.js v4 keyframe array: array of param objects, each can have its own duration + animate(el, { + keyframes: [ + { scale: 0.95, rotate: 2, filter: 'brightness(0.6) sepia(1) hue-rotate(-20deg)', duration: 140 }, + { scale: 0, rotate: 8, opacity: 0, duration: 210 }, + ], + }) + } else if (type === 'skip') { + animate(el, { x: '110%', rotate: 5, opacity: 0, duration: 260, ease: 'out(2)' }) + } + } + + const AURA_COLORS = { + discard: 'rgba(244, 67, 54, 0.25)', + skip: 'rgba(255, 152, 0, 0.25)', + bucket: 'rgba(42, 96, 128, 0.20)', + none: 'transparent', + } as const + + function updateAura(zone: 'discard' | 'skip' | null, bucket: string | null) { + if (!cardEl.value) return + const color = + zone === 'discard' ? AURA_COLORS.discard : + zone === 'skip' ? AURA_COLORS.skip : + bucket ? AURA_COLORS.bucket : + AURA_COLORS.none + utils.set(cardEl.value, { background: color }) + } + + function reset() { + if (!cardEl.value) return + // Instantly restore initial card state — called when a new item loads into the same element + utils.set(cardEl.value, { + x: 0, + y: 0, + scale: 1, + opacity: 1, + rotate: 0, + borderRadius: CARD_RADIUS, + background: 'transparent', + filter: 'none', + }) + } + + return { pickup, setDragPosition, snapBack, animateDismiss, updateAura, reset } +} diff --git a/web/src/composables/useEasterEgg.ts b/web/src/composables/useEasterEgg.ts new file mode 100644 index 0000000..87d069b --- /dev/null +++ b/web/src/composables/useEasterEgg.ts @@ -0,0 +1,160 @@ +import { onMounted, onUnmounted } from 'vue' + +const KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'] +const KONAMI_AB = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','a','b'] + +export function useKeySequence(sequence: string[], onActivate: () => void) { + let pos = 0 + + function handler(e: KeyboardEvent) { + if (e.key === sequence[pos]) { + pos++ + if (pos === sequence.length) { + pos = 0 + onActivate() + } + } else { + pos = 0 + } + } + + onMounted(() => window.addEventListener('keydown', handler)) + onUnmounted(() => window.removeEventListener('keydown', handler)) +} + +export function useKonamiCode(onActivate: () => void) { + useKeySequence(KONAMI, onActivate) + useKeySequence(KONAMI_AB, onActivate) +} + +export function useHackerMode() { + function toggle() { + const root = document.documentElement + if (root.dataset.theme === 'hacker') { + delete root.dataset.theme + localStorage.removeItem('cf-hacker-mode') + } else { + root.dataset.theme = 'hacker' + localStorage.setItem('cf-hacker-mode', 'true') + } + } + + function restore() { + if (localStorage.getItem('cf-hacker-mode') === 'true') { + document.documentElement.dataset.theme = 'hacker' + } + } + + return { toggle, restore } +} + +/** Fire a confetti burst from a given x,y position. Pure canvas, no dependencies. */ +export function fireConfetti(originX = window.innerWidth / 2, originY = window.innerHeight / 2) { + if (typeof requestAnimationFrame === 'undefined') return + + const canvas = document.createElement('canvas') + canvas.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999;' + canvas.width = window.innerWidth + canvas.height = window.innerHeight + document.body.appendChild(canvas) + const ctx = canvas.getContext('2d')! + + const COLORS = ['#2A6080','#B8622A','#5A9DBF','#D4854A','#FFC107','#4CAF50'] + const particles = Array.from({ length: 80 }, () => ({ + x: originX, + y: originY, + vx: (Math.random() - 0.5) * 14, + vy: (Math.random() - 0.6) * 12, + color: COLORS[Math.floor(Math.random() * COLORS.length)], + size: 5 + Math.random() * 6, + angle: Math.random() * Math.PI * 2, + spin: (Math.random() - 0.5) * 0.3, + life: 1.0, + })) + + let raf = 0 + function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height) + let alive = false + for (const p of particles) { + p.x += p.vx + p.y += p.vy + p.vy += 0.35 // gravity + p.vx *= 0.98 // air friction + p.angle += p.spin + p.life -= 0.016 + if (p.life <= 0) continue + alive = true + ctx.save() + ctx.globalAlpha = p.life + ctx.fillStyle = p.color + ctx.translate(p.x, p.y) + ctx.rotate(p.angle) + ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6) + ctx.restore() + } + if (alive) { + raf = requestAnimationFrame(draw) + } else { + cancelAnimationFrame(raf) + canvas.remove() + } + } + raf = requestAnimationFrame(draw) +} + +/** Enable cursor trail in hacker mode — returns a cleanup function. */ +export function useCursorTrail() { + const DOT_COUNT = 10 + const dots: HTMLElement[] = [] + let positions: { x: number; y: number }[] = [] + let mouseX = 0 + let mouseY = 0 + let raf = 0 + + for (let i = 0; i < DOT_COUNT; i++) { + const dot = document.createElement('div') + dot.style.cssText = [ + 'position:fixed', + 'pointer-events:none', + 'z-index:9998', + 'border-radius:50%', + 'background:#5A9DBF', + 'transition:opacity 0.1s', + ].join(';') + document.body.appendChild(dot) + dots.push(dot) + } + + function onMouseMove(e: MouseEvent) { + mouseX = e.clientX + mouseY = e.clientY + } + + function animate() { + positions.unshift({ x: mouseX, y: mouseY }) + if (positions.length > DOT_COUNT) positions = positions.slice(0, DOT_COUNT) + + dots.forEach((dot, i) => { + const pos = positions[i] + if (!pos) { dot.style.opacity = '0'; return } + const scale = 1 - i / DOT_COUNT + const size = Math.round(8 * scale) + dot.style.left = `${pos.x - size / 2}px` + dot.style.top = `${pos.y - size / 2}px` + dot.style.width = `${size}px` + dot.style.height = `${size}px` + dot.style.opacity = `${(1 - i / DOT_COUNT) * 0.7}` + }) + raf = requestAnimationFrame(animate) + } + + window.addEventListener('mousemove', onMouseMove) + raf = requestAnimationFrame(animate) + + return function cleanup() { + window.removeEventListener('mousemove', onMouseMove) + cancelAnimationFrame(raf) + dots.forEach(d => d.remove()) + } +} diff --git a/web/src/composables/useHaptics.ts b/web/src/composables/useHaptics.ts new file mode 100644 index 0000000..4406dd9 --- /dev/null +++ b/web/src/composables/useHaptics.ts @@ -0,0 +1,18 @@ +import { useMotion } from './useMotion' + +export function useHaptics() { + const { rich } = useMotion() + + function vibrate(pattern: number | number[]) { + if (rich.value && typeof navigator !== 'undefined' && 'vibrate' in navigator) { + navigator.vibrate(pattern) + } + } + + return { + label: () => vibrate(40), + discard: () => vibrate([40, 30, 40]), + skip: () => vibrate(15), + undo: () => vibrate([20, 20, 60]), + } +} diff --git a/web/src/composables/useLabelKeyboard.test.ts b/web/src/composables/useLabelKeyboard.test.ts new file mode 100644 index 0000000..fcb61ff --- /dev/null +++ b/web/src/composables/useLabelKeyboard.test.ts @@ -0,0 +1,106 @@ +import { useLabelKeyboard } from './useLabelKeyboard' +import { describe, it, expect, vi, afterEach } from 'vitest' + +const LABELS = [ + { name: 'interview_scheduled', key: '1', emoji: '🗓️', color: '#4CAF50' }, + { name: 'offer_received', key: '2', emoji: '🎉', color: '#2196F3' }, + { name: 'rejected', key: '3', emoji: '❌', color: '#F44336' }, +] + +describe('useLabelKeyboard', () => { + const cleanups: (() => void)[] = [] + + afterEach(() => { + cleanups.forEach(fn => fn()) + cleanups.length = 0 + }) + + it('calls onLabel when digit key pressed', () => { + const onLabel = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel, onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() }) + cleanups.push(cleanup) + window.dispatchEvent(new KeyboardEvent('keydown', { key: '1' })) + expect(onLabel).toHaveBeenCalledWith('interview_scheduled') + }) + + it('calls onLabel for key 2', () => { + const onLabel = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel, onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() }) + cleanups.push(cleanup) + window.dispatchEvent(new KeyboardEvent('keydown', { key: '2' })) + expect(onLabel).toHaveBeenCalledWith('offer_received') + }) + + it('calls onLabel("hired") when h pressed', () => { + const onLabel = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel, onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() }) + cleanups.push(cleanup) + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'h' })) + expect(onLabel).toHaveBeenCalledWith('hired') + }) + + it('calls onSkip when s pressed', () => { + const onSkip = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel: vi.fn(), onSkip, onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() }) + cleanups.push(cleanup) + window.dispatchEvent(new KeyboardEvent('keydown', { key: 's' })) + expect(onSkip).toHaveBeenCalled() + }) + + it('calls onDiscard when d pressed', () => { + const onDiscard = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel: vi.fn(), onSkip: vi.fn(), onDiscard, onUndo: vi.fn(), onHelp: vi.fn() }) + cleanups.push(cleanup) + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'd' })) + expect(onDiscard).toHaveBeenCalled() + }) + + it('calls onUndo when u pressed', () => { + const onUndo = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel: vi.fn(), onSkip: vi.fn(), onDiscard: vi.fn(), onUndo, onHelp: vi.fn() }) + cleanups.push(cleanup) + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'u' })) + expect(onUndo).toHaveBeenCalled() + }) + + it('calls onHelp when ? pressed', () => { + const onHelp = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel: vi.fn(), onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp }) + cleanups.push(cleanup) + window.dispatchEvent(new KeyboardEvent('keydown', { key: '?' })) + expect(onHelp).toHaveBeenCalled() + }) + + it('ignores keydown when target is an input', () => { + const onLabel = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel, onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() }) + cleanups.push(cleanup) + const input = document.createElement('input') + document.body.appendChild(input) + input.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true })) + expect(onLabel).not.toHaveBeenCalled() + document.body.removeChild(input) + }) + + it('cleanup removes the listener', () => { + const onLabel = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: LABELS, onLabel, onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() }) + cleanup() + window.dispatchEvent(new KeyboardEvent('keydown', { key: '1' })) + expect(onLabel).not.toHaveBeenCalled() + }) + + it('evaluates labels getter on each keypress', () => { + const labelList: { name: string; key: string; emoji: string; color: string }[] = [] + const onLabel = vi.fn() + const { cleanup } = useLabelKeyboard({ labels: () => labelList, onLabel, onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() }) + cleanups.push(cleanup) + // Before labels loaded — pressing '1' does nothing + window.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true })) + expect(onLabel).not.toHaveBeenCalled() + // Add a label (simulating async load) + labelList.push({ name: 'interview_scheduled', key: '1', emoji: '🗓️', color: '#4CAF50' }) + window.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true })) + expect(onLabel).toHaveBeenCalledWith('interview_scheduled') + }) +}) diff --git a/web/src/composables/useLabelKeyboard.ts b/web/src/composables/useLabelKeyboard.ts new file mode 100644 index 0000000..1ca80ed --- /dev/null +++ b/web/src/composables/useLabelKeyboard.ts @@ -0,0 +1,41 @@ +import { onUnmounted, getCurrentInstance } from 'vue' + +interface Label { name: string; key: string; emoji: string; color: string } + +interface Options { + labels: Label[] | (() => Label[]) + onLabel: (name: string) => void + onSkip: () => void + onDiscard: () => void + onUndo: () => void + onHelp: () => void +} + +export function useLabelKeyboard(opts: Options) { + function handler(e: KeyboardEvent) { + if (e.target instanceof HTMLInputElement) return + if (e.target instanceof HTMLTextAreaElement) return + const k = e.key.toLowerCase() + // Evaluate labels lazily so reactive updates work + const labelList = typeof opts.labels === 'function' ? opts.labels() : opts.labels + const keyMap = new Map(labelList.map(l => [l.key.toLowerCase(), l.name])) + if (keyMap.has(k)) { opts.onLabel(keyMap.get(k)!); return } + if (k === 'h') { opts.onLabel('hired'); return } + if (k === 's') { opts.onSkip(); return } + if (k === 'd') { opts.onDiscard(); return } + if (k === 'u') { opts.onUndo(); return } + if (k === '?') { opts.onHelp(); return } + } + + // Add listener immediately (composable is called in setup, not in onMounted) + window.addEventListener('keydown', handler) + + const cleanup = () => window.removeEventListener('keydown', handler) + + // In component context: auto-cleanup on unmount + if (getCurrentInstance()) { + onUnmounted(cleanup) + } + + return { cleanup } +} diff --git a/web/src/composables/useMotion.ts b/web/src/composables/useMotion.ts new file mode 100644 index 0000000..eee0ae1 --- /dev/null +++ b/web/src/composables/useMotion.ts @@ -0,0 +1,28 @@ +import { computed, ref } from 'vue' + +const STORAGE_KEY = 'cf-avocet-rich-motion' + +// OS-level prefers-reduced-motion — checked once at module load +const OS_REDUCED = typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false + +// Reactive ref so toggling localStorage triggers re-reads in the same session +const _richOverride = ref( + typeof window !== 'undefined' + ? localStorage.getItem(STORAGE_KEY) + : null +) + +export function useMotion() { + const rich = computed(() => + !OS_REDUCED && _richOverride.value !== 'false' + ) + + function setRich(enabled: boolean) { + localStorage.setItem(STORAGE_KEY, enabled ? 'true' : 'false') + _richOverride.value = enabled ? 'true' : 'false' + } + + return { rich, setRich } +} diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..79f43d6 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,20 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import { router } from './router' +// Self-hosted fonts — no Google Fonts CDN (privacy requirement) +import '@fontsource/fraunces/400.css' +import '@fontsource/fraunces/700.css' +import '@fontsource/atkinson-hyperlegible/400.css' +import '@fontsource/atkinson-hyperlegible/700.css' +import '@fontsource/jetbrains-mono/400.css' +import 'virtual:uno.css' +import './assets/theme.css' +import './assets/avocet.css' +import App from './App.vue' + +if ('scrollRestoration' in history) history.scrollRestoration = 'manual' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/web/src/router/index.ts b/web/src/router/index.ts new file mode 100644 index 0000000..9e64197 --- /dev/null +++ b/web/src/router/index.ts @@ -0,0 +1,19 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import LabelView from '../views/LabelView.vue' + +// Views are lazy-loaded to keep initial bundle small +const FetchView = () => import('../views/FetchView.vue') +const StatsView = () => import('../views/StatsView.vue') +const BenchmarkView = () => import('../views/BenchmarkView.vue') +const SettingsView = () => import('../views/SettingsView.vue') + +export const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { path: '/', component: LabelView, meta: { title: 'Label' } }, + { path: '/fetch', component: FetchView, meta: { title: 'Fetch' } }, + { path: '/stats', component: StatsView, meta: { title: 'Stats' } }, + { path: '/benchmark', component: BenchmarkView, meta: { title: 'Benchmark' } }, + { path: '/settings', component: SettingsView, meta: { title: 'Settings' } }, + ], +}) diff --git a/web/src/smoke.test.ts b/web/src/smoke.test.ts new file mode 100644 index 0000000..a601b38 --- /dev/null +++ b/web/src/smoke.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' + +describe('scaffold', () => { + it('vitest works', () => { + expect(1 + 1).toBe(2) + }) +}) + +describe('composable imports', () => { + it('useApi imports', async () => { + const { useApiFetch } = await import('./composables/useApi') + expect(typeof useApiFetch).toBe('function') + }) + + it('useMotion imports', async () => { + const { useMotion } = await import('./composables/useMotion') + expect(typeof useMotion).toBe('function') + }) + + it('useHaptics imports', async () => { + const { useHaptics } = await import('./composables/useHaptics') + expect(typeof useHaptics).toBe('function') + }) + + it('useEasterEgg imports', async () => { + const { useKonamiCode, useHackerMode } = await import('./composables/useEasterEgg') + expect(typeof useKonamiCode).toBe('function') + expect(typeof useHackerMode).toBe('function') + }) +}) diff --git a/web/src/stores/label.test.ts b/web/src/stores/label.test.ts new file mode 100644 index 0000000..63ae543 --- /dev/null +++ b/web/src/stores/label.test.ts @@ -0,0 +1,62 @@ +// src/stores/label.test.ts +import { setActivePinia, createPinia } from 'pinia' +import { useLabelStore } from './label' +import { beforeEach, describe, it, expect } from 'vitest' + +const MOCK_ITEM = { + id: 'abc', subject: 'Test', body: 'Body', from: 'a@b.com', + date: '2026-03-01', source: 'imap:test', +} + +describe('label store', () => { + beforeEach(() => setActivePinia(createPinia())) + + it('starts with empty queue', () => { + const store = useLabelStore() + expect(store.queue).toEqual([]) + expect(store.current).toBeNull() + }) + + it('current returns first item', () => { + const store = useLabelStore() + store.queue = [MOCK_ITEM] + expect(store.current).toEqual(MOCK_ITEM) + }) + + it('removeCurrentFromQueue removes first item', () => { + const store = useLabelStore() + store.queue = [MOCK_ITEM, { ...MOCK_ITEM, id: 'def' }] + store.removeCurrentFromQueue() + expect(store.queue[0].id).toBe('def') + }) + + it('tracks lastAction', () => { + const store = useLabelStore() + store.queue = [MOCK_ITEM] + store.setLastAction('label', MOCK_ITEM, 'interview_scheduled') + expect(store.lastAction?.type).toBe('label') + expect(store.lastAction?.label).toBe('interview_scheduled') + }) + + it('incrementLabeled increases sessionLabeled', () => { + const store = useLabelStore() + store.incrementLabeled() + store.incrementLabeled() + expect(store.sessionLabeled).toBe(2) + }) + + it('restoreItem adds to front of queue', () => { + const store = useLabelStore() + store.queue = [{ ...MOCK_ITEM, id: 'def' }] + store.restoreItem(MOCK_ITEM) + expect(store.queue[0].id).toBe('abc') + expect(store.queue[1].id).toBe('def') + }) + + it('clearLastAction nulls lastAction', () => { + const store = useLabelStore() + store.setLastAction('skip', MOCK_ITEM) + store.clearLastAction() + expect(store.lastAction).toBeNull() + }) +}) diff --git a/web/src/stores/label.ts b/web/src/stores/label.ts new file mode 100644 index 0000000..f5b66f5 --- /dev/null +++ b/web/src/stores/label.ts @@ -0,0 +1,53 @@ +// src/stores/label.ts +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +export interface QueueItem { + id: string + subject: string + body: string + from: string + date: string + source: string +} + +export interface LastAction { + type: 'label' | 'skip' | 'discard' + item: QueueItem + label?: string +} + +export const useLabelStore = defineStore('label', () => { + const queue = ref([]) + const totalRemaining = ref(0) + const lastAction = ref(null) + const sessionLabeled = ref(0) // for easter eggs + + const current = computed(() => queue.value[0] ?? null) + + function removeCurrentFromQueue() { + queue.value.shift() + } + + function setLastAction(type: LastAction['type'], item: QueueItem, label?: string) { + lastAction.value = { type, item, label } + } + + function clearLastAction() { + lastAction.value = null + } + + function restoreItem(item: QueueItem) { + queue.value.unshift(item) + } + + function incrementLabeled() { + sessionLabeled.value++ + } + + return { + queue, totalRemaining, lastAction, sessionLabeled, current, + removeCurrentFromQueue, setLastAction, clearLastAction, + restoreItem, incrementLabeled, + } +}) diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 0000000..f691315 --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/web/src/test-setup.ts b/web/src/test-setup.ts new file mode 100644 index 0000000..5021a5e --- /dev/null +++ b/web/src/test-setup.ts @@ -0,0 +1,17 @@ +// jsdom does not implement window.matchMedia — stub it so composables that +// check prefers-reduced-motion can import without throwing. +if (typeof window !== 'undefined' && !window.matchMedia) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }), + }) +} diff --git a/web/src/views/BenchmarkView.vue b/web/src/views/BenchmarkView.vue new file mode 100644 index 0000000..7351b42 --- /dev/null +++ b/web/src/views/BenchmarkView.vue @@ -0,0 +1,846 @@ + + + + + diff --git a/web/src/views/FetchView.vue b/web/src/views/FetchView.vue new file mode 100644 index 0000000..273efd6 --- /dev/null +++ b/web/src/views/FetchView.vue @@ -0,0 +1,459 @@ + + + + + diff --git a/web/src/views/LabelView.test.ts b/web/src/views/LabelView.test.ts new file mode 100644 index 0000000..525a0c9 --- /dev/null +++ b/web/src/views/LabelView.test.ts @@ -0,0 +1,92 @@ +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import LabelView from './LabelView.vue' +import EmailCardStack from '../components/EmailCardStack.vue' +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// Mock fetch globally +beforeEach(() => { + setActivePinia(createPinia()) + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ items: [], total: 0 }), + text: async () => '', + })) +}) + +describe('LabelView', () => { + it('shows loading state initially', () => { + const w = mount(LabelView, { + global: { plugins: [createPinia()] }, + }) + // Should show skeleton while loading + expect(w.find('.skeleton-card').exists()).toBe(true) + }) + + it('shows empty state when queue is empty after load', async () => { + const w = mount(LabelView, { + global: { plugins: [createPinia()] }, + }) + // Let all promises resolve + await new Promise(r => setTimeout(r, 0)) + await w.vm.$nextTick() + expect(w.find('.empty-state').exists()).toBe(true) + }) + + it('renders header with action buttons', async () => { + const w = mount(LabelView, { + global: { plugins: [createPinia()] }, + }) + await new Promise(r => setTimeout(r, 0)) + await w.vm.$nextTick() + expect(w.find('.lv-header').exists()).toBe(true) + expect(w.text()).toContain('Undo') + expect(w.text()).toContain('Skip') + expect(w.text()).toContain('Discard') + }) + + const queueItem = { + id: 'test-1', subject: 'Test Email', body: 'Test body', + from: 'test@test.com', date: '2026-03-05', source: 'test', + } + + // Return queue items for /api/queue, empty array for /api/config/labels + function mockFetchWithQueue() { + vi.stubGlobal('fetch', vi.fn().mockImplementation((url: string) => + Promise.resolve({ + ok: true, + json: async () => (url as string).includes('/api/queue') + ? { items: [queueItem], total: 1 } + : [], + text: async () => '', + }) + )) + } + + it('renders toss zone overlays when isHeld is true (after drag-start)', async () => { + mockFetchWithQueue() + const w = mount(LabelView, { global: { plugins: [createPinia()] } }) + await new Promise(r => setTimeout(r, 0)) + await w.vm.$nextTick() + // Zone overlays should not exist before drag + expect(w.find('.toss-zone-left').exists()).toBe(false) + // Emit drag-start from EmailCardStack child + const cardStack = w.findComponent(EmailCardStack) + cardStack.vm.$emit('drag-start') + await w.vm.$nextTick() + expect(w.find('.toss-zone-left').exists()).toBe(true) + expect(w.find('.toss-zone-right').exists()).toBe(true) + }) + + it('bucket-grid-footer has grid-active class while card is held', async () => { + mockFetchWithQueue() + const w = mount(LabelView, { global: { plugins: [createPinia()] } }) + await new Promise(r => setTimeout(r, 0)) + await w.vm.$nextTick() + expect(w.find('.bucket-grid-footer').classes()).not.toContain('grid-active') + const cardStack = w.findComponent(EmailCardStack) + cardStack.vm.$emit('drag-start') + await w.vm.$nextTick() + expect(w.find('.bucket-grid-footer').classes()).toContain('grid-active') + }) +}) diff --git a/web/src/views/LabelView.vue b/web/src/views/LabelView.vue new file mode 100644 index 0000000..98dd2fb --- /dev/null +++ b/web/src/views/LabelView.vue @@ -0,0 +1,506 @@ + + + + + diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue new file mode 100644 index 0000000..f09c924 --- /dev/null +++ b/web/src/views/SettingsView.vue @@ -0,0 +1,431 @@ + + + + + diff --git a/web/src/views/StatsView.vue b/web/src/views/StatsView.vue new file mode 100644 index 0000000..d1d3cd8 --- /dev/null +++ b/web/src/views/StatsView.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..8d16e42 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/uno.config.ts b/web/uno.config.ts new file mode 100644 index 0000000..29b9a7e --- /dev/null +++ b/web/uno.config.ts @@ -0,0 +1,5 @@ +import { defineConfig, presetWind, presetAttributify } from 'unocss' + +export default defineConfig({ + presets: [presetWind(), presetAttributify()], +}) diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..c22afdb --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import UnoCSS from 'unocss/vite' + +export default defineConfig({ + plugins: [vue(), UnoCSS()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/test-setup.ts'], + }, +})