From f06114e648aecb3c3403089853f5b8d3b28bfcab Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 13:22:17 -0800 Subject: [PATCH 01/99] feat(avocet): FastAPI skeleton + JSONL helpers --- app/api.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_api.py | 8 +++++++ 2 files changed, 67 insertions(+) create mode 100644 app/api.py create mode 100644 tests/test_api.py diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..7148d9c --- /dev/null +++ b/app/api.py @@ -0,0 +1,59 @@ +"""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 json +from pathlib import Path + +from fastapi import FastAPI + +_ROOT = Path(__file__).parent.parent +_DATA_DIR: Path = _ROOT / "data" # overridable in tests via set_data_dir() + + +def set_data_dir(path: Path) -> None: + """Override data directory — used by tests.""" + global _DATA_DIR + _DATA_DIR = path + + +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) + path.write_text( + "\n".join(json.dumps(r, ensure_ascii=False) for r in records) + "\n", + 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") + + +app = FastAPI(title="Avocet API") + +# In-memory last-action store (single user, local tool — in-memory is fine) +_last_action: dict | None = None diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..311e8f7 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,8 @@ +# tests/test_api.py +import json, pytest +from pathlib import Path + +# We'll import helpers once they exist +# For now just verify the file can be imported +def test_import(): + from app import api # noqa: F401 From d36d0be166d955b51820797e2fb5e4b77bfede0c Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 14:36:18 -0800 Subject: [PATCH 02/99] fix(avocet): _write_jsonl empty-list writes empty file; add reset_last_action helper --- app/api.py | 12 ++++++++---- tests/test_api.py | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/api.py b/app/api.py index 7148d9c..bebbe68 100644 --- a/app/api.py +++ b/app/api.py @@ -20,6 +20,12 @@ def set_data_dir(path: Path) -> None: _DATA_DIR = path +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" @@ -41,10 +47,8 @@ def _read_jsonl(path: Path) -> list[dict]: def _write_jsonl(path: Path, records: list[dict]) -> None: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - "\n".join(json.dumps(r, ensure_ascii=False) for r in records) + "\n", - encoding="utf-8", - ) + 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: diff --git a/tests/test_api.py b/tests/test_api.py index 311e8f7..50d316e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,8 +1,15 @@ -# tests/test_api.py -import json, pytest -from pathlib import Path +import pytest +from app import api as api_module # noqa: F401 + + +@pytest.fixture(autouse=True) +def reset_globals(tmp_path): + from app import api + api.set_data_dir(tmp_path) + api.reset_last_action() + yield + api.reset_last_action() + -# We'll import helpers once they exist -# For now just verify the file can be imported def test_import(): from app import api # noqa: F401 From 8898258055cb33ee594eb1529e5b45c592061db0 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:00:59 -0800 Subject: [PATCH 03/99] feat(avocet): GET /api/queue endpoint --- app/api.py | 8 +++++++- tests/test_api.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/api.py b/app/api.py index bebbe68..5e9f97d 100644 --- a/app/api.py +++ b/app/api.py @@ -8,7 +8,7 @@ from __future__ import annotations import json from pathlib import Path -from fastapi import FastAPI +from fastapi import FastAPI, Query _ROOT = Path(__file__).parent.parent _DATA_DIR: Path = _ROOT / "data" # overridable in tests via set_data_dir() @@ -61,3 +61,9 @@ 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": items[:limit], "total": len(items)} diff --git a/tests/test_api.py b/tests/test_api.py index 50d316e..637a7ca 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,3 +1,5 @@ +import json + import pytest from app import api as api_module # noqa: F401 @@ -13,3 +15,40 @@ def reset_globals(tmp_path): def test_import(): from app import api # noqa: F401 + + +from fastapi.testclient import TestClient + + +@pytest.fixture +def client(): + from app.api import app + return TestClient(app) + + +@pytest.fixture +def queue_with_items(tmp_path): + """Write 3 test emails to the queue file.""" + from app import api as api_module + items = [ + {"id": f"id{i}", "subject": f"Subject {i}", "body": f"Body {i}", + "from": "test@example.com", "date": "2026-03-01", "source": "imap:test"} + for i in range(3) + ] + queue_path = tmp_path / "email_label_queue.jsonl" + queue_path.write_text("\n".join(json.dumps(x) for x in items) + "\n") + return items + + +def test_queue_returns_items(client, queue_with_items): + r = client.get("/api/queue?limit=2") + assert r.status_code == 200 + data = r.json() + assert len(data["items"]) == 2 + assert data["total"] == 3 + + +def test_queue_empty_when_no_file(client): + r = client.get("/api/queue") + assert r.status_code == 200 + assert r.json() == {"items": [], "total": 0} From 6556e3fef02d919cbd616b9b4aeddbf951bba508 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:03:57 -0800 Subject: [PATCH 04/99] fix(avocet): queue_with_items fixture uses api._DATA_DIR to avoid implicit tmp_path coupling --- tests/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 637a7ca..ac6ecb1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -27,7 +27,7 @@ def client(): @pytest.fixture -def queue_with_items(tmp_path): +def queue_with_items(): """Write 3 test emails to the queue file.""" from app import api as api_module items = [ @@ -35,7 +35,7 @@ def queue_with_items(tmp_path): "from": "test@example.com", "date": "2026-03-01", "source": "imap:test"} for i in range(3) ] - queue_path = tmp_path / "email_label_queue.jsonl" + queue_path = api_module._DATA_DIR / "email_label_queue.jsonl" queue_path.write_text("\n".join(json.dumps(x) for x in items) + "\n") return items From ce202d97ea05bf9d15d79458bf8c5144b33cb722 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:14:04 -0800 Subject: [PATCH 05/99] feat(avocet): POST /api/label endpoint --- app/api.py | 25 ++++++++++++++++++++++++- tests/test_api.py | 21 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/api.py b/app/api.py index 5e9f97d..077da14 100644 --- a/app/api.py +++ b/app/api.py @@ -8,7 +8,10 @@ from __future__ import annotations import json from pathlib import Path -from fastapi import FastAPI, Query +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() @@ -67,3 +70,23 @@ _last_action: dict | None = None def get_queue(limit: int = Query(default=10, ge=1, le=50)): items = _read_jsonl(_queue_file()) return {"items": 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 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 x["id"] != req.id]) + _last_action = {"type": "label", "item": record} + return {"ok": True} diff --git a/tests/test_api.py b/tests/test_api.py index ac6ecb1..ad6275a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -52,3 +52,24 @@ def test_queue_empty_when_no_file(client): r = client.get("/api/queue") assert r.status_code == 200 assert r.json() == {"items": [], "total": 0} + + +def test_label_appends_to_score(client, queue_with_items): + from app import api as api_module + r = client.post("/api/label", json={"id": "id0", "label": "interview_scheduled"}) + assert r.status_code == 200 + records = api_module._read_jsonl(api_module._score_file()) + assert len(records) == 1 + assert records[0]["id"] == "id0" + assert records[0]["label"] == "interview_scheduled" + assert "labeled_at" in records[0] + +def test_label_removes_from_queue(client, queue_with_items): + from app import api as api_module + client.post("/api/label", json={"id": "id0", "label": "rejected"}) + queue = api_module._read_jsonl(api_module._queue_file()) + assert not any(x["id"] == "id0" for x in queue) + +def test_label_unknown_id_returns_404(client, queue_with_items): + r = client.post("/api/label", json={"id": "unknown", "label": "neutral"}) + assert r.status_code == 404 From efd9d6969279c00b79de7ba566882c1a5f949cd6 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:16:54 -0800 Subject: [PATCH 06/99] fix(avocet): store original item in _last_action; add requirements.txt --- app/api.py | 2 +- requirements.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/app/api.py b/app/api.py index 077da14..dac1efb 100644 --- a/app/api.py +++ b/app/api.py @@ -88,5 +88,5 @@ def post_label(req: LabelRequest): "labeled_at": datetime.now(timezone.utc).isoformat()} _append_jsonl(_score_file(), record) _write_jsonl(_queue_file(), [x for x in items if x["id"] != req.id]) - _last_action = {"type": "label", "item": record} + _last_action = {"type": "label", "item": match, "label": req.label} return {"ok": True} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b1b82c2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.100.0 +pydantic>=2.0.0 +uvicorn[standard]>=0.20.0 +httpx>=0.24.0 +pytest>=7.0.0 From 5912b737052a5893225c632554d1e9c161af12f2 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:21:32 -0800 Subject: [PATCH 07/99] feat(avocet): POST /api/skip endpoint --- app/api.py | 17 +++++++++++++++++ tests/test_api.py | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/app/api.py b/app/api.py index dac1efb..7f591da 100644 --- a/app/api.py +++ b/app/api.py @@ -90,3 +90,20 @@ def post_label(req: LabelRequest): _write_jsonl(_queue_file(), [x for x in items if 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 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 x["id"] != req.id] + [match] + _write_jsonl(_queue_file(), reordered) + _last_action = {"type": "skip", "item": match} + return {"ok": True} diff --git a/tests/test_api.py b/tests/test_api.py index ad6275a..ebfb59d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -73,3 +73,15 @@ def test_label_removes_from_queue(client, queue_with_items): def test_label_unknown_id_returns_404(client, queue_with_items): r = client.post("/api/label", json={"id": "unknown", "label": "neutral"}) assert r.status_code == 404 + +def test_skip_moves_to_back(client, queue_with_items): + from app import api as api_module + r = client.post("/api/skip", json={"id": "id0"}) + assert r.status_code == 200 + queue = api_module._read_jsonl(api_module._queue_file()) + assert queue[-1]["id"] == "id0" + assert queue[0]["id"] == "id1" + +def test_skip_unknown_id_returns_404(client, queue_with_items): + r = client.post("/api/skip", json={"id": "nope"}) + assert r.status_code == 404 From f4facc6484e4dc4316502485976c3c30d5c71f11 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:35:01 -0800 Subject: [PATCH 08/99] =?UTF-8?q?feat(avocet):=20discard,=20undo,=20labels?= =?UTF-8?q?=20config,=20static=20serving=20=E2=80=94=20backend=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_api.py | 64 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/app/api.py b/app/api.py index 7f591da..86a3e12 100644 --- a/app/api.py +++ b/app/api.py @@ -107,3 +107,80 @@ def post_skip(req: SkipRequest): _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 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 x["id"] != req.id]) + _last_action = {"type": "discard", "item": match} # store ORIGINAL match, not enriched record + 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 + _last_action = None + + item = action["item"] # always the original clean queue item + + if action["type"] == "label": + # Remove last entry from score file + records = _read_jsonl(_score_file()) + _write_jsonl(_score_file(), records[:-1]) + elif action["type"] == "discard": + # Remove last entry from discarded file + records = _read_jsonl(_discarded_file()) + _write_jsonl(_discarded_file(), records[:-1]) + elif action["type"] == "skip": + # Item is at back of queue — move it to front + items = _read_jsonl(_queue_file()) + reordered = [item] + [x for x in items if x["id"] != item["id"]] + _write_jsonl(_queue_file(), reordered) + return {"undone": {"type": action["type"], "item": item}} + + # For label and discard: restore item to front of queue + items = _read_jsonl(_queue_file()) + _write_jsonl(_queue_file(), [item] + items) + return {"undone": {"type": action["type"], "item": 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 + + +# Static SPA — MUST be last (catches all unmatched paths) +_DIST = _ROOT / "web" / "dist" +if _DIST.exists(): + from fastapi.staticfiles import StaticFiles + app.mount("/", StaticFiles(directory=str(_DIST), html=True), name="spa") diff --git a/tests/test_api.py b/tests/test_api.py index ebfb59d..0a9ea23 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -85,3 +85,67 @@ def test_skip_moves_to_back(client, queue_with_items): def test_skip_unknown_id_returns_404(client, queue_with_items): r = client.post("/api/skip", json={"id": "nope"}) assert r.status_code == 404 + + +# --- Part A: POST /api/discard --- + +def test_discard_writes_to_discarded_file(client, queue_with_items): + from app import api as api_module + r = client.post("/api/discard", json={"id": "id1"}) + assert r.status_code == 200 + discarded = api_module._read_jsonl(api_module._discarded_file()) + assert len(discarded) == 1 + assert discarded[0]["id"] == "id1" + assert discarded[0]["label"] == "__discarded__" + +def test_discard_removes_from_queue(client, queue_with_items): + from app import api as api_module + client.post("/api/discard", json={"id": "id1"}) + queue = api_module._read_jsonl(api_module._queue_file()) + assert not any(x["id"] == "id1" for x in queue) + + +# --- Part B: DELETE /api/label/undo --- + +def test_undo_label_removes_from_score(client, queue_with_items): + from app import api as api_module + client.post("/api/label", json={"id": "id0", "label": "neutral"}) + r = client.delete("/api/label/undo") + assert r.status_code == 200 + data = r.json() + assert data["undone"]["type"] == "label" + score = api_module._read_jsonl(api_module._score_file()) + assert score == [] + +def test_undo_discard_removes_from_discarded(client, queue_with_items): + from app import api as api_module + client.post("/api/discard", json={"id": "id0"}) + r = client.delete("/api/label/undo") + assert r.status_code == 200 + discarded = api_module._read_jsonl(api_module._discarded_file()) + assert discarded == [] + +def test_undo_skip_restores_to_front(client, queue_with_items): + from app import api as api_module + client.post("/api/skip", json={"id": "id0"}) + r = client.delete("/api/label/undo") + assert r.status_code == 200 + queue = api_module._read_jsonl(api_module._queue_file()) + assert queue[0]["id"] == "id0" + +def test_undo_with_no_action_returns_404(client): + r = client.delete("/api/label/undo") + assert r.status_code == 404 + + +# --- Part C: GET /api/config/labels --- + +def test_config_labels_returns_metadata(client): + r = client.get("/api/config/labels") + assert r.status_code == 200 + labels = r.json() + assert len(labels) == 10 + assert labels[0]["key"] == "1" + assert "emoji" in labels[0] + assert "color" in labels[0] + assert "name" in labels[0] From 01cc908eab2162838ae7fc96d3e97f80d8f84663 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:41:58 -0800 Subject: [PATCH 09/99] =?UTF-8?q?fix(avocet):=20undo=20=E2=80=94=20commit-?= =?UTF-8?q?then-clear=20order,=20empty-records=20guard,=20skip=20dedup,=20?= =?UTF-8?q?stronger=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api.py | 34 +++++++++++++++++++--------------- tests/test_api.py | 3 +++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/api.py b/app/api.py index 86a3e12..ccbefd1 100644 --- a/app/api.py +++ b/app/api.py @@ -134,28 +134,32 @@ def delete_undo(): if not _last_action: raise HTTPException(404, "No action to undo") action = _last_action - _last_action = None - item = action["item"] # always the original clean queue item + # Perform file operations FIRST — only clear _last_action on success if action["type"] == "label": - # Remove last entry from score file records = _read_jsonl(_score_file()) + if not records: + raise HTTPException(409, "Score file is empty — cannot undo label") _write_jsonl(_score_file(), records[:-1]) - elif action["type"] == "discard": - # Remove last entry from discarded file - records = _read_jsonl(_discarded_file()) - _write_jsonl(_discarded_file(), records[:-1]) - elif action["type"] == "skip": - # Item is at back of queue — move it to front items = _read_jsonl(_queue_file()) - reordered = [item] + [x for x in items if x["id"] != item["id"]] - _write_jsonl(_queue_file(), reordered) - return {"undone": {"type": action["type"], "item": item}} + _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()) + # Remove the item wherever it sits (guards against duplicate insertion), + # then prepend it to the front — restoring it to position 0. + items = [item] + [x for x in items if x["id"] != item["id"]] + _write_jsonl(_queue_file(), items) - # For label and discard: restore item to front of queue - items = _read_jsonl(_queue_file()) - _write_jsonl(_queue_file(), [item] + items) + # Clear AFTER all file operations succeed + _last_action = None return {"undone": {"type": action["type"], "item": item}} diff --git a/tests/test_api.py b/tests/test_api.py index 0a9ea23..2d5b8bc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -116,6 +116,9 @@ def test_undo_label_removes_from_score(client, queue_with_items): assert data["undone"]["type"] == "label" score = api_module._read_jsonl(api_module._score_file()) assert score == [] + # Item should be restored to front of queue + queue = api_module._read_jsonl(api_module._queue_file()) + assert queue[0]["id"] == "id0" def test_undo_discard_removes_from_discarded(client, queue_with_items): from app import api as api_module From 02efd5fb1d1b44eeed21048ac526d354134e8190 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:46:58 -0800 Subject: [PATCH 10/99] feat(avocet): Vite + Vue 3 + UnoCSS + Vitest scaffold --- web/.gitignore | 24 + web/.vscode/extensions.json | 3 + web/README.md | 5 + web/index.html | 13 + web/package-lock.json | 4632 +++++++++++++++++++++++++++++ web/package.json | 33 + web/public/vite.svg | 1 + web/src/App.vue | 30 + web/src/assets/vue.svg | 1 + web/src/components/HelloWorld.vue | 41 + web/src/main.ts | 5 + web/src/smoke.test.ts | 7 + web/src/style.css | 79 + web/tsconfig.app.json | 16 + web/tsconfig.json | 7 + web/tsconfig.node.json | 26 + web/uno.config.ts | 5 + web/vite.config.ts | 11 + 18 files changed, 4939 insertions(+) create mode 100644 web/.gitignore create mode 100644 web/.vscode/extensions.json create mode 100644 web/README.md create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/vite.svg create mode 100644 web/src/App.vue create mode 100644 web/src/assets/vue.svg create mode 100644 web/src/components/HelloWorld.vue create mode 100644 web/src/main.ts create mode 100644 web/src/smoke.test.ts create mode 100644 web/src/style.css create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/uno.config.ts create mode 100644 web/vite.config.ts 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..8ab41c2 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,4632 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@vueuse/core": "^14.2.1", + "@vueuse/integrations": "^14.2.1", + "pinia": "^3.0.4", + "vue": "^3.5.25" + }, + "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/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/@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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.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/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==", + "dev": true, + "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/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/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==", + "dev": true, + "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==", + "dev": true, + "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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "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/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/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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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-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==", + "dev": true, + "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" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..fcc22dd --- /dev/null +++ b/web/package.json @@ -0,0 +1,33 @@ +{ + "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": { + "@vueuse/core": "^14.2.1", + "@vueuse/integrations": "^14.2.1", + "pinia": "^3.0.4", + "vue": "^3.5.25" + }, + "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..58b0f21 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,30 @@ + + + + + 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/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/main.ts b/web/src/main.ts new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/web/src/smoke.test.ts b/web/src/smoke.test.ts new file mode 100644 index 0000000..119b9e0 --- /dev/null +++ b/web/src/smoke.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest' + +describe('scaffold', () => { + it('vitest works', () => { + expect(1 + 1).toBe(2) + }) +}) 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/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..00529c8 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,11 @@ +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, + }, +}) From a9e5c0d8f32a76c0d7223ec8f7320be265885153 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:49:07 -0800 Subject: [PATCH 11/99] feat(avocet): CircuitForge base theme + Avocet Slate Teal/Russet colors --- web/src/assets/avocet.css | 8 ++ web/src/assets/theme.css | 268 ++++++++++++++++++++++++++++++++++++++ web/src/main.ts | 9 +- 3 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 web/src/assets/avocet.css create mode 100644 web/src/assets/theme.css diff --git a/web/src/assets/avocet.css b/web/src/assets/avocet.css new file mode 100644 index 0000000..e04cd6c --- /dev/null +++ b/web/src/assets/avocet.css @@ -0,0 +1,8 @@ +/* Avocet app color overrides — Slate Teal + Russet */ +/* These override the base --app-primary/--app-accent from theme.css */ +:root { + --app-primary: #2A6080; /* Slate Teal — "deep water" */ + --app-primary-dark: #5A9DBF; + --app-accent: #B8622A; /* Russet — avocet's orange head */ + --app-accent-dark: #D4854A; +} 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/main.ts b/web/src/main.ts index 2425c0f..65ff817 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,5 +1,10 @@ import { createApp } from 'vue' -import './style.css' +import { createPinia } from 'pinia' +import 'virtual:uno.css' +import './assets/theme.css' +import './assets/avocet.css' import App from './App.vue' -createApp(App).mount('#app') +const app = createApp(App) +app.use(createPinia()) +app.mount('#app') From b10c3372148d7ddb80b6e451073f32dd4c924776 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:52:38 -0800 Subject: [PATCH 12/99] =?UTF-8?q?fix(avocet):=20align=20theme=20with=20Per?= =?UTF-8?q?egrine=20design=20system=20=E2=80=94=20full=20token=20set,=20da?= =?UTF-8?q?rk=20mode,=20self-hosted=20fonts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/package-lock.json | 30 ++++++++++++++++++++++ web/package.json | 3 +++ web/src/assets/avocet.css | 54 ++++++++++++++++++++++++++++++++++----- web/src/main.ts | 6 +++++ 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 8ab41c2..f2fbbcd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,9 @@ "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", "pinia": "^3.0.4", @@ -772,6 +775,33 @@ } } }, + "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", diff --git a/web/package.json b/web/package.json index fcc22dd..297d155 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,9 @@ "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", "pinia": "^3.0.4", diff --git a/web/src/assets/avocet.css b/web/src/assets/avocet.css index e04cd6c..3ad8a01 100644 --- a/web/src/assets/avocet.css +++ b/web/src/assets/avocet.css @@ -1,8 +1,50 @@ -/* Avocet app color overrides — Slate Teal + Russet */ -/* These override the base --app-primary/--app-accent from theme.css */ +/* 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 +*/ + +/* ── Light mode (default) ──────────────────────────── */ :root { - --app-primary: #2A6080; /* Slate Teal — "deep water" */ - --app-primary-dark: #5A9DBF; - --app-accent: #B8622A; /* Russet — avocet's orange head */ - --app-accent-dark: #D4854A; + /* 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/main.ts b/web/src/main.ts index 65ff817..4d50956 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,5 +1,11 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' +// 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' From 50dd4c2f4559c3b83390533abfb10674ab47b781 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:54:44 -0800 Subject: [PATCH 13/99] feat(avocet): Pinia label store with queue, lastAction, easter egg counter --- web/src/stores/label.test.ts | 62 ++++++++++++++++++++++++++++++++++++ web/src/stores/label.ts | 53 ++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 web/src/stores/label.test.ts create mode 100644 web/src/stores/label.ts 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, + } +}) From 7edcb089a9325814b6c4ce3b41a2c969896c067d Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:58:43 -0800 Subject: [PATCH 14/99] feat(avocet): useApi, useMotion, useHaptics, useEasterEgg (Konami/hacker mode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useApiFetch: typed fetch wrapper with network/http error discrimination - useMotion: reactive localStorage override for rich-animation toggle, respects OS prefers-reduced-motion - useHaptics: label/discard/skip/undo vibration patterns, gated on rich mode - useKonamiCode + useHackerMode: 10-key Konami sequence → hacker theme, persisted in localStorage - test-setup.ts: jsdom matchMedia stub so useMotion imports cleanly in Vitest - smoke.test.ts: import smoke tests for all 4 composables (12 tests, all passing) --- web/src/composables/useApi.ts | 20 ++++++++++++++ web/src/composables/useEasterEgg.ts | 43 +++++++++++++++++++++++++++++ web/src/composables/useHaptics.ts | 18 ++++++++++++ web/src/composables/useMotion.ts | 28 +++++++++++++++++++ web/src/smoke.test.ts | 23 +++++++++++++++ web/src/test-setup.ts | 17 ++++++++++++ web/vite.config.ts | 1 + 7 files changed, 150 insertions(+) create mode 100644 web/src/composables/useApi.ts create mode 100644 web/src/composables/useEasterEgg.ts create mode 100644 web/src/composables/useHaptics.ts create mode 100644 web/src/composables/useMotion.ts create mode 100644 web/src/test-setup.ts diff --git a/web/src/composables/useApi.ts b/web/src/composables/useApi.ts new file mode 100644 index 0000000..d677091 --- /dev/null +++ b/web/src/composables/useApi.ts @@ -0,0 +1,20 @@ +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) } } + } +} diff --git a/web/src/composables/useEasterEgg.ts b/web/src/composables/useEasterEgg.ts new file mode 100644 index 0000000..62c48e7 --- /dev/null +++ b/web/src/composables/useEasterEgg.ts @@ -0,0 +1,43 @@ +import { onMounted, onUnmounted } from 'vue' + +const KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'] + +export function useKonamiCode(onActivate: () => void) { + let pos = 0 + + function handler(e: KeyboardEvent) { + if (e.key === KONAMI[pos]) { + pos++ + if (pos === KONAMI.length) { + pos = 0 + onActivate() + } + } else { + pos = 0 + } + } + + onMounted(() => window.addEventListener('keydown', handler)) + onUnmounted(() => window.removeEventListener('keydown', handler)) +} + +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 } +} 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/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/smoke.test.ts b/web/src/smoke.test.ts index 119b9e0..a601b38 100644 --- a/web/src/smoke.test.ts +++ b/web/src/smoke.test.ts @@ -5,3 +5,26 @@ describe('scaffold', () => { 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/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/vite.config.ts b/web/vite.config.ts index 00529c8..c22afdb 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -7,5 +7,6 @@ export default defineConfig({ test: { environment: 'jsdom', globals: true, + setupFiles: ['./src/test-setup.ts'], }, }) From f5aca77ff63bb32a5b9cba23b9ea7d610d670265 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:03:01 -0800 Subject: [PATCH 15/99] =?UTF-8?q?feat(avocet):=20EmailCard=20component=20?= =?UTF-8?q?=E2=80=94=20subject,=20from/date,=20body=20preview,=20expand/co?= =?UTF-8?q?llapse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/EmailCard.test.ts | 39 +++++++++ web/src/components/EmailCard.vue | 113 +++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 web/src/components/EmailCard.test.ts create mode 100644 web/src/components/EmailCard.vue 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..2180df2 --- /dev/null +++ b/web/src/components/EmailCard.vue @@ -0,0 +1,113 @@ + + + + + From 452730bb981ec35f95ee650377b509304e9f9f4a Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:04:31 -0800 Subject: [PATCH 16/99] =?UTF-8?q?feat(avocet):=20LabelBucketGrid=20?= =?UTF-8?q?=E2=80=94=20numpad=20layout,=20bucket-mode=20expansion,=20drag?= =?UTF-8?q?=20drop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/LabelBucketGrid.test.ts | 34 ++++++ web/src/components/LabelBucketGrid.vue | 118 +++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 web/src/components/LabelBucketGrid.test.ts create mode 100644 web/src/components/LabelBucketGrid.vue diff --git a/web/src/components/LabelBucketGrid.test.ts b/web/src/components/LabelBucketGrid.test.ts new file mode 100644 index 0000000..d1d6e27 --- /dev/null +++ b/web/src/components/LabelBucketGrid.test.ts @@ -0,0 +1,34 @@ +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('🗓️') + }) +}) diff --git a/web/src/components/LabelBucketGrid.vue b/web/src/components/LabelBucketGrid.vue new file mode 100644 index 0000000..9e6bfa1 --- /dev/null +++ b/web/src/components/LabelBucketGrid.vue @@ -0,0 +1,118 @@ + + + + + From 2fd101f382523f53c0f999fce048dd0073a49de5 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:12:58 -0800 Subject: [PATCH 17/99] =?UTF-8?q?feat(avocet):=20useLabelKeyboard=20?= =?UTF-8?q?=E2=80=94=201-9,=20h,=20S,=20D,=20U,=20=3F=20shortcuts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/composables/useLabelKeyboard.test.ts | 92 ++++++++++++++++++++ web/src/composables/useLabelKeyboard.ts | 40 +++++++++ 2 files changed, 132 insertions(+) create mode 100644 web/src/composables/useLabelKeyboard.test.ts create mode 100644 web/src/composables/useLabelKeyboard.ts diff --git a/web/src/composables/useLabelKeyboard.test.ts b/web/src/composables/useLabelKeyboard.test.ts new file mode 100644 index 0000000..7e7d650 --- /dev/null +++ b/web/src/composables/useLabelKeyboard.test.ts @@ -0,0 +1,92 @@ +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() + }) +}) diff --git a/web/src/composables/useLabelKeyboard.ts b/web/src/composables/useLabelKeyboard.ts new file mode 100644 index 0000000..146528b --- /dev/null +++ b/web/src/composables/useLabelKeyboard.ts @@ -0,0 +1,40 @@ +import { onUnmounted, getCurrentInstance } from 'vue' + +interface Label { name: string; key: string; emoji: string; color: string } + +interface Options { + labels: Label[] + onLabel: (name: string) => void + onSkip: () => void + onDiscard: () => void + onUndo: () => void + onHelp: () => void +} + +export function useLabelKeyboard(opts: Options) { + const keyMap = new Map(opts.labels.map(l => [l.key.toLowerCase(), l.name])) + + function handler(e: KeyboardEvent) { + if (e.target instanceof HTMLInputElement) return + if (e.target instanceof HTMLTextAreaElement) return + const k = e.key.toLowerCase() + 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 } +} From 5f03db20dc656d9a9c6d0eea9bbc9564e04eea87 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:13:02 -0800 Subject: [PATCH 18/99] =?UTF-8?q?feat(avocet):=20UndoToast=20=E2=80=94=205?= =?UTF-8?q?-second=20countdown,=20undo=20button,=20accessible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/UndoToast.test.ts | 74 +++++++++++++++++++ web/src/components/UndoToast.vue | 105 +++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 web/src/components/UndoToast.test.ts create mode 100644 web/src/components/UndoToast.vue diff --git a/web/src/components/UndoToast.test.ts b/web/src/components/UndoToast.test.ts new file mode 100644 index 0000000..3378504 --- /dev/null +++ b/web/src/components/UndoToast.test.ts @@ -0,0 +1,74 @@ +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) + }) +}) diff --git a/web/src/components/UndoToast.vue b/web/src/components/UndoToast.vue new file mode 100644 index 0000000..18e7f57 --- /dev/null +++ b/web/src/components/UndoToast.vue @@ -0,0 +1,105 @@ + + + + + From 97437f39c98a1e9ac872d8d19cdbcb49db99d45e Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:16:09 -0800 Subject: [PATCH 19/99] =?UTF-8?q?feat(avocet):=20EmailCardStack=20?= =?UTF-8?q?=E2=80=94=20swipe=20gestures,=20depth=20shadows,=20dismissal=20?= =?UTF-8?q?classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/EmailCardStack.test.ts | 59 +++++++++ web/src/components/EmailCardStack.vue | 138 ++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 web/src/components/EmailCardStack.test.ts create mode 100644 web/src/components/EmailCardStack.vue diff --git a/web/src/components/EmailCardStack.test.ts b/web/src/components/EmailCardStack.test.ts new file mode 100644 index 0000000..6bb0aaa --- /dev/null +++ b/web/src/components/EmailCardStack.test.ts @@ -0,0 +1,59 @@ +import { mount } from '@vue/test-utils' +import EmailCardStack from './EmailCardStack.vue' +import { describe, it, expect } from 'vitest' + +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('applies dismiss-label class when dismissType is label', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: 'label' } }) + expect(w.find('.card-wrapper').classes()).toContain('dismiss-label') + }) + + it('applies dismiss-discard class when dismissType is discard', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: 'discard' } }) + expect(w.find('.card-wrapper').classes()).toContain('dismiss-discard') + }) + + it('applies dismiss-skip class when dismissType is skip', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: 'skip' } }) + expect(w.find('.card-wrapper').classes()).toContain('dismiss-skip') + }) + + it('no dismiss class when dismissType is null', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: null } }) + const wrapperClasses = w.find('.card-wrapper').classes() + expect(wrapperClasses).not.toContain('dismiss-label') + expect(wrapperClasses).not.toContain('dismiss-discard') + expect(wrapperClasses).not.toContain('dismiss-skip') + }) + + it('emits drag-start on dragstart event', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + await w.find('.card-stack').trigger('dragstart') + expect(w.emitted('drag-start')).toBeTruthy() + }) + + it('emits drag-end on dragend event', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + await w.find('.card-stack').trigger('dragend') + expect(w.emitted('drag-end')).toBeTruthy() + }) +}) diff --git a/web/src/components/EmailCardStack.vue b/web/src/components/EmailCardStack.vue new file mode 100644 index 0000000..078d325 --- /dev/null +++ b/web/src/components/EmailCardStack.vue @@ -0,0 +1,138 @@ + + + + + From 05d12a1417d36ec3760485e95dfb20a6e667fd2a Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:19:29 -0800 Subject: [PATCH 20/99] =?UTF-8?q?feat(avocet):=20LabelBucketGrid=20bucket-?= =?UTF-8?q?mode=20CSS=20=E2=80=94=20spring=20expansion,=20glow=20on=20drop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/LabelBucketGrid.vue | 79 +++++++++++++------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/web/src/components/LabelBucketGrid.vue b/web/src/components/LabelBucketGrid.vue index 9e6bfa1..87d2186 100644 --- a/web/src/components/LabelBucketGrid.vue +++ b/web/src/components/LabelBucketGrid.vue @@ -40,11 +40,11 @@ function onDrop(name: string) { .label-grid { display: grid; grid-template-columns: repeat(5, 1fr); - gap: var(--space-2); - transition: all var(--bucket-expand); + gap: 0.5rem; + transition: all var(--bucket-expand, 250ms cubic-bezier(0.34, 1.56, 0.64, 1)); } -/* Mobile: 3×3 numpad layout + hired at bottom */ +/* Mobile: 3-column numpad layout */ @media (max-width: 480px) { .label-grid { grid-template-columns: repeat(3, 1fr); @@ -52,67 +52,70 @@ function onDrop(name: string) { } .label-grid.bucket-mode { - gap: var(--space-4); - padding: var(--space-3); + gap: 1rem; + padding: 1rem; } .label-btn { display: flex; flex-direction: column; align-items: center; - gap: var(--space-1); - padding: var(--space-2) var(--space-1); - border: 2px solid var(--label-color, var(--color-border)); - border-radius: var(--radius-md); + justify-content: center; + gap: 0.25rem; + min-height: 44px; /* Touch target */ + padding: 0.5rem 0.25rem; + border-radius: 0.5rem; + border: 2px solid var(--label-color, #607D8B); background: transparent; - color: var(--color-text); + color: var(--color-text, #1a2338); cursor: pointer; - font-family: var(--font-body); - transition: all var(--bucket-expand); - min-height: 44px; /* touch target */ + transition: all var(--bucket-expand, 250ms cubic-bezier(0.34, 1.56, 0.64, 1)); + font-family: var(--font-body, sans-serif); } .label-grid.bucket-mode .label-btn { - padding: var(--space-6) var(--space-2); - font-size: 1.15rem; -} - -.label-btn:hover, -.label-btn:focus-visible { - background: color-mix(in srgb, var(--label-color) 12%, transparent); -} - -.label-btn:focus-visible { - outline: 2px solid var(--label-color); - outline-offset: 2px; + min-height: 80px; + padding: 1rem 0.5rem; + border-width: 3px; + font-size: 1.1rem; } .label-btn.is-drop-target { - background: var(--label-color); - color: var(--color-text-inverse, #fff); + background: var(--label-color, #607D8B); + color: #fff; transform: scale(1.08); - box-shadow: 0 0 16px color-mix(in srgb, var(--label-color) 60%, transparent); + box-shadow: 0 0 16px color-mix(in srgb, var(--label-color, #607D8B) 60%, transparent); +} + +.label-btn:hover:not(.is-drop-target) { + background: color-mix(in srgb, var(--label-color, #607D8B) 12%, transparent); } .key-hint { - font-size: 0.7rem; - font-weight: 700; - opacity: 0.6; - font-family: var(--font-mono); + font-size: 0.65rem; + font-family: var(--font-mono, monospace); + opacity: 0.55; + line-height: 1; } -.emoji { font-size: 1.2rem; line-height: 1; } +.emoji { + font-size: 1.25rem; + line-height: 1; +} .label-name { - font-size: 0.65rem; + font-size: 0.7rem; text-align: center; line-height: 1.2; word-break: break-word; - color: var(--color-text-muted); + hyphens: auto; } -.label-grid.bucket-mode .label-name { - font-size: 0.75rem; - color: var(--color-text); +/* Reduced-motion fallback */ +@media (prefers-reduced-motion: reduce) { + .label-grid, + .label-btn { + transition: none; + } } From 8e3d26384741abf65c6f8f85bd05d4da8dbb17b8 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:21:07 -0800 Subject: [PATCH 21/99] =?UTF-8?q?feat(avocet):=20LabelView=20=E2=80=94=20w?= =?UTF-8?q?ires=20store,=20API,=20card=20stack,=20keyboard,=20easter=20egg?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Task 13: LabelView.vue wires together the label store, API fetch, card stack, bucket grid, keyboard shortcuts, haptics, motion preference, and three easter egg badges (on-a-roll, speed round, fifty deep). App.vue updated to mount LabelView and restore hacker-mode theme on load. 3 new LabelView tests; all 48 tests pass, build clean. --- web/src/App.vue | 53 +++--- web/src/views/LabelView.test.ts | 46 +++++ web/src/views/LabelView.vue | 318 ++++++++++++++++++++++++++++++++ 3 files changed, 395 insertions(+), 22 deletions(-) create mode 100644 web/src/views/LabelView.test.ts create mode 100644 web/src/views/LabelView.vue diff --git a/web/src/App.vue b/web/src/App.vue index 58b0f21..8f15aa8 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,30 +1,39 @@ - - - diff --git a/web/src/views/LabelView.test.ts b/web/src/views/LabelView.test.ts new file mode 100644 index 0000000..35a4c35 --- /dev/null +++ b/web/src/views/LabelView.test.ts @@ -0,0 +1,46 @@ +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import LabelView from './LabelView.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') + }) +}) diff --git a/web/src/views/LabelView.vue b/web/src/views/LabelView.vue new file mode 100644 index 0000000..de15f30 --- /dev/null +++ b/web/src/views/LabelView.vue @@ -0,0 +1,318 @@ + + + + + From 5cf5fe9c4339e084cd8dbaf16b4cf69a4109d879 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:23:56 -0800 Subject: [PATCH 22/99] feat(avocet): manage.sh start-api / stop-api / open-api commands --- manage.sh | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/manage.sh b/manage.sh index 39ea114..0979636 100755 --- a/manage.sh +++ b/manage.sh @@ -93,6 +93,11 @@ usage() { echo -e " ${GREEN}score [args]${NC} Shortcut: --score [args]" echo -e " ${GREEN}compare [args]${NC} Shortcut: --compare [args]" echo "" + echo " Vue API:" + echo -e " ${GREEN}start-api${NC} Build Vue SPA + start FastAPI on port 8503" + echo -e " ${GREEN}stop-api${NC} Stop FastAPI server" + echo -e " ${GREEN}open-api${NC} Open Vue UI in browser (http://localhost:8503)" + echo "" echo " Dev:" echo -e " ${GREEN}test${NC} Run pytest suite" echo "" @@ -251,6 +256,58 @@ case "$CMD" in exec "$0" benchmark --compare "$@" ;; + start-api) + API_PID_FILE=".avocet-api.pid" + API_PORT=8503 + if [[ -f "$API_PID_FILE" ]] && kill -0 "$(<"$API_PID_FILE")" 2>/dev/null; then + warn "API already running (PID $(<"$API_PID_FILE")) → http://localhost:${API_PORT}" + exit 0 + fi + mkdir -p "$LOG_DIR" + API_LOG="${LOG_DIR}/api.log" + info "Building Vue SPA…" + (cd web && npm run build) >> "$API_LOG" 2>&1 + info "Starting FastAPI on port ${API_PORT}…" + nohup "$PYTHON_UI" -m uvicorn app.api:app \ + --host 127.0.0.1 --port "$API_PORT" \ + >> "$API_LOG" 2>&1 & + echo $! > "$API_PID_FILE" + sleep 1 + if kill -0 "$(<"$API_PID_FILE")" 2>/dev/null; then + success "Avocet API started → http://localhost:${API_PORT} (PID $(<"$API_PID_FILE"))" + else + error "API died. Check ${API_LOG}" + fi + ;; + + stop-api) + API_PID_FILE=".avocet-api.pid" + if [[ ! -f "$API_PID_FILE" ]]; then + warn "API not running." + exit 0 + fi + PID="$(<"$API_PID_FILE")" + if kill -0 "$PID" 2>/dev/null; then + kill "$PID" && rm -f "$API_PID_FILE" + success "API stopped (PID ${PID})." + else + warn "Stale PID file (process ${PID} not running). Cleaning up." + rm -f "$API_PID_FILE" + fi + ;; + + open-api) + URL="http://localhost:8503" + info "Opening ${URL}" + if command -v xdg-open &>/dev/null; then + xdg-open "$URL" + elif command -v open &>/dev/null; then + open "$URL" + else + echo "$URL" + fi + ;; + help|--help|-h) usage ;; From 65d9f6089efe796597a1a3211a24f379554ad389 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:24:47 -0800 Subject: [PATCH 23/99] =?UTF-8?q?feat(avocet):=20easter=20eggs=20=E2=80=94?= =?UTF-8?q?=20hired=20confetti,=20century=20mark,=20clean=20sweep,=20midni?= =?UTF-8?q?ght=20labeler,=20cursor=20trail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/composables/useEasterEgg.ts | 111 ++++++++++++++++++++++++++++ web/src/views/LabelView.vue | 80 ++++++++++++++++++-- 2 files changed, 184 insertions(+), 7 deletions(-) diff --git a/web/src/composables/useEasterEgg.ts b/web/src/composables/useEasterEgg.ts index 62c48e7..3e3e0ad 100644 --- a/web/src/composables/useEasterEgg.ts +++ b/web/src/composables/useEasterEgg.ts @@ -41,3 +41,114 @@ export function useHackerMode() { 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/views/LabelView.vue b/web/src/views/LabelView.vue index de15f30..246bcf7 100644 --- a/web/src/views/LabelView.vue +++ b/web/src/views/LabelView.vue @@ -6,9 +6,12 @@ - 🔥 On a roll! - ⚡ Speed round! - 🎯 Fifty deep! + 🔥 On a roll! + ⚡ Speed round! + 🎯 Fifty deep! + 💯 Century! + 🧹 Clean sweep! + 🦉 Midnight labeler!
@@ -61,12 +64,13 @@ @@ -253,9 +316,12 @@ onMounted(async () => { to { transform: scale(1); opacity: 1; } } -.badge-roll { background: #ff6b35; color: #fff; } -.badge-speed { background: #7c3aed; color: #fff; } -.badge-fifty { background: var(--app-accent, #B8622A); color: var(--app-accent-text, #1a2338); } +.badge-roll { background: #ff6b35; color: #fff; } +.badge-speed { background: #7c3aed; color: #fff; } +.badge-fifty { background: var(--app-accent, #B8622A); color: var(--app-accent-text, #1a2338); } +.badge-century { background: #ffd700; color: #1a2338; } +.badge-sweep { background: var(--app-primary, #2A6080); color: #fff; } +.badge-midnight { background: #1a1a2e; color: #7c9dcf; border: 1px solid #7c9dcf; } .header-actions { display: flex; From 2fdafc1d10aa5b6e8c8bd521150fa17dfebe4a6a Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:28:18 -0800 Subject: [PATCH 24/99] =?UTF-8?q?fix(avocet):=20strip=20HTML=20from=20emai?= =?UTF-8?q?l=20bodies=20=E2=80=94=20stdlib=20HTMLParser,=20no=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/label_tool.py | 241 ++++++++++++++++++++++++++++++++++++--- tests/test_label_tool.py | 87 ++++++++++++++ 2 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 tests/test_label_tool.py 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/tests/test_label_tool.py b/tests/test_label_tool.py new file mode 100644 index 0000000..7e5d257 --- /dev/null +++ b/tests/test_label_tool.py @@ -0,0 +1,87 @@ +"""Tests for label_tool HTML extraction utilities. + +These functions are stdlib-only and safe to test without an IMAP connection. +""" +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from app.label_tool import _extract_body, _strip_html + + +# ── _strip_html ────────────────────────────────────────────────────────────── + +def test_strip_html_removes_tags(): + assert _strip_html("

Hello world

") == "Hello world" + + +def test_strip_html_skips_script_content(): + result = _strip_html("

real

") + assert "doEvil" not in result + assert "real" in result + + +def test_strip_html_skips_style_content(): + result = _strip_html("

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) == "" From 5ac64b3429f4d9d6a8ae855b12d622ea789c1299 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 18:11:53 -0800 Subject: [PATCH 25/99] =?UTF-8?q?fix(avocet):=20start-api=20polls=20port?= =?UTF-8?q?=20instead=20of=20sleeping=201s=20=E2=80=94=20avoids=20false-su?= =?UTF-8?q?ccess=20on=20slow=20start?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manage.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/manage.sh b/manage.sh index 0979636..5bbfcce 100755 --- a/manage.sh +++ b/manage.sh @@ -272,11 +272,20 @@ case "$CMD" in --host 127.0.0.1 --port "$API_PORT" \ >> "$API_LOG" 2>&1 & echo $! > "$API_PID_FILE" - sleep 1 - if kill -0 "$(<"$API_PID_FILE")" 2>/dev/null; then - success "Avocet API started → http://localhost:${API_PORT} (PID $(<"$API_PID_FILE"))" - else - error "API died. Check ${API_LOG}" + # Poll until port is actually bound (up to 10 s), not just process alive + for _i in $(seq 1 20); do + sleep 0.5 + if (echo "" >/dev/tcp/127.0.0.1/"$API_PORT") 2>/dev/null; then + success "Avocet API started → http://localhost:${API_PORT} (PID $(<"$API_PID_FILE"))" + break + fi + if ! kill -0 "$(<"$API_PID_FILE")" 2>/dev/null; then + rm -f "$API_PID_FILE" + error "API died during startup. Check ${API_LOG}" + fi + done + if ! (echo "" >/dev/tcp/127.0.0.1/"$API_PORT") 2>/dev/null; then + error "API did not bind to port ${API_PORT} within 10 s. Check ${API_LOG}" fi ;; From cb9ebb805ccd9e5a413af1cc74a956dcf6ec8a83 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 18:43:00 -0800 Subject: [PATCH 26/99] fix(avocet): normalize queue schema + bind to 0.0.0.0 for LAN access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _item_id() (content hash) + _normalize() to map legacy JSONL fields (from_addr/account/no-id) to Vue schema (from/source/id) - All mutating endpoints now look up by _normalize(x)[id] — handles both stored-id (test fixtures) and content-hash (real data) transparently - Change uvicorn bind from 127.0.0.1 to 0.0.0.0 so LAN clients can connect --- app/api.py | 47 +++++++++++++++++++++++++++++++++++------------ manage.sh | 2 +- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/app/api.py b/app/api.py index ccbefd1..ef30eda 100644 --- a/app/api.py +++ b/app/api.py @@ -5,6 +5,7 @@ Endpoints and static file serving are added in subsequent tasks. """ from __future__ import annotations +import hashlib import json from pathlib import Path @@ -60,6 +61,29 @@ def _append_jsonl(path: Path, record: dict) -> None: 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) @@ -69,7 +93,7 @@ _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": items[:limit], "total": len(items)} + return {"items": [_normalize(x) for x in items[:limit]], "total": len(items)} class LabelRequest(BaseModel): @@ -81,13 +105,13 @@ class LabelRequest(BaseModel): def post_label(req: LabelRequest): global _last_action items = _read_jsonl(_queue_file()) - match = next((x for x in items if x["id"] == req.id), None) + 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 x["id"] != req.id]) + _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} @@ -100,10 +124,10 @@ class SkipRequest(BaseModel): def post_skip(req: SkipRequest): global _last_action items = _read_jsonl(_queue_file()) - match = next((x for x in items if x["id"] == req.id), None) + 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 x["id"] != req.id] + [match] + 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} @@ -117,14 +141,14 @@ class DiscardRequest(BaseModel): def post_discard(req: DiscardRequest): global _last_action items = _read_jsonl(_queue_file()) - match = next((x for x in items if x["id"] == req.id), None) + 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 x["id"] != req.id]) - _last_action = {"type": "discard", "item": match} # store ORIGINAL match, not enriched 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} @@ -153,14 +177,13 @@ def delete_undo(): _write_jsonl(_queue_file(), [item] + items) elif action["type"] == "skip": items = _read_jsonl(_queue_file()) - # Remove the item wherever it sits (guards against duplicate insertion), - # then prepend it to the front — restoring it to position 0. - items = [item] + [x for x in items if x["id"] != item["id"]] + 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": item}} + return {"undone": {"type": action["type"], "item": _normalize(item)}} # Label metadata — 10 labels matching label_tool.py diff --git a/manage.sh b/manage.sh index 5bbfcce..7ea900d 100755 --- a/manage.sh +++ b/manage.sh @@ -269,7 +269,7 @@ case "$CMD" in (cd web && npm run build) >> "$API_LOG" 2>&1 info "Starting FastAPI on port ${API_PORT}…" nohup "$PYTHON_UI" -m uvicorn app.api:app \ - --host 127.0.0.1 --port "$API_PORT" \ + --host 0.0.0.0 --port "$API_PORT" \ >> "$API_LOG" 2>&1 & echo $! > "$API_PID_FILE" # Poll until port is actually bound (up to 10 s), not just process alive From 3788254abd3bd75bd2099739a35192665580a0a6 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 19:26:34 -0800 Subject: [PATCH 27/99] fix: prevent blank page on rebuild and queue drain on skip/discard Two bugs fixed: 1. Blank white page after vue SPA rebuild: browsers cached old index.html referencing old asset hashes. Assets are deleted on rebuild, causing 404s for JS/CSS -> blank page. Fix: serve index.html with Cache-Control: no-cache so browsers always fetch fresh HTML. Hashed assets (/assets/chunk-abc123.js) remain cacheable forever. 2. Queue draining to empty on skip/discard: handleSkip and handleDiscard never refilled the local queue buffer. After enough skips, store.current went null and the empty state showed (blank-looking). Fix: both handlers now call fetchBatch() when queue drops below 3, matching handleLabel. Also: sync classifier_adapters LABELS to match current 10-label schema (new_lead + hired, remove unrelated). 48 Python tests pass, 48 frontend tests pass. --- app/api.py | 11 +++++++++++ scripts/classifier_adapters.py | 6 ++++-- tests/test_classifier_adapters.py | 8 +++++--- web/src/views/LabelView.vue | 2 ++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/api.py b/app/api.py index ef30eda..30ddc56 100644 --- a/app/api.py +++ b/app/api.py @@ -209,5 +209,16 @@ def get_labels(): # 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/scripts/classifier_adapters.py b/scripts/classifier_adapters.py index 2817078..5704de1 100644 --- a/scripts/classifier_adapters.py +++ b/scripts/classifier_adapters.py @@ -27,8 +27,9 @@ LABELS: list[str] = [ "survey_received", "neutral", "event_rescheduled", - "unrelated", "digest", + "new_lead", + "hired", ] # Natural-language descriptions used by the RerankerAdapter. @@ -40,8 +41,9 @@ LABEL_DESCRIPTIONS: dict[str, str] = { "survey_received": "invitation to complete a culture-fit survey or assessment", "neutral": "automated ATS confirmation such as application received", "event_rescheduled": "an interview or scheduled event moved to a new time", - "unrelated": "non-job-search email unrelated to any application or recruiter", "digest": "job digest or multi-listing email with multiple job postings", + "new_lead": "unsolicited recruiter outreach or cold contact about a new opportunity", + "hired": "job offer accepted, onboarding logistics, welcome email, or start date confirmation", } # Lazy import shims — allow tests to patch without requiring the libs installed. diff --git a/tests/test_classifier_adapters.py b/tests/test_classifier_adapters.py index f50ef3b..85f9dc1 100644 --- a/tests/test_classifier_adapters.py +++ b/tests/test_classifier_adapters.py @@ -2,14 +2,16 @@ import pytest -def test_labels_constant_has_nine_items(): +def test_labels_constant_has_ten_items(): from scripts.classifier_adapters import LABELS - assert len(LABELS) == 9 + assert len(LABELS) == 10 assert "interview_scheduled" in LABELS assert "neutral" in LABELS assert "event_rescheduled" in LABELS - assert "unrelated" in LABELS assert "digest" in LABELS + assert "new_lead" in LABELS + assert "hired" in LABELS + assert "unrelated" not in LABELS def test_compute_metrics_perfect_predictions(): diff --git a/web/src/views/LabelView.vue b/web/src/views/LabelView.vue index 246bcf7..1925bfc 100644 --- a/web/src/views/LabelView.vue +++ b/web/src/views/LabelView.vue @@ -211,6 +211,7 @@ async function handleSkip() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }), }) + if (store.queue.length < 3) await fetchBatch() } async function handleDiscard() { @@ -228,6 +229,7 @@ async function handleDiscard() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }), }) + if (store.queue.length < 3) await fetchBatch() } async function handleUndo() { From 8d2fdf6299c9be9bdedbd6ab5849f85d316248fb Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 4 Mar 2026 11:29:03 -0800 Subject: [PATCH 28/99] fix: UndoToast now emits expire after 5s so toast self-dismisses --- web/src/components/UndoToast.test.ts | 15 ++++++++++ web/src/components/UndoToast.vue | 7 +++-- web/src/views/LabelView.vue | 43 ++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/web/src/components/UndoToast.test.ts b/web/src/components/UndoToast.test.ts index 3378504..b5a2b30 100644 --- a/web/src/components/UndoToast.test.ts +++ b/web/src/components/UndoToast.test.ts @@ -71,4 +71,19 @@ describe('UndoToast', () => { 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 index 18e7f57..aa02a04 100644 --- a/web/src/components/UndoToast.vue +++ b/web/src/components/UndoToast.vue @@ -13,7 +13,7 @@ import { ref, onMounted, onUnmounted, computed } from 'vue' import type { LastAction } from '../stores/label' const props = defineProps<{ action: LastAction }>() -defineEmits<{ undo: [] }>() +const emit = defineEmits<{ undo: []; expire: [] }>() const DURATION = 5000 const elapsed = ref(0) @@ -30,14 +30,15 @@ const label = computed(() => { }) function tick(ts: number) { - if (!start) start = ts elapsed.value = ts - start if (elapsed.value < DURATION) { raf = requestAnimationFrame(tick) + } else { + emit('expire') } } -onMounted(() => { raf = requestAnimationFrame(tick) }) +onMounted(() => { start = performance.now(); raf = requestAnimationFrame(tick) }) onUnmounted(() => cancelAnimationFrame(raf)) diff --git a/web/src/views/LabelView.vue b/web/src/views/LabelView.vue index 1925bfc..cc7df66 100644 --- a/web/src/views/LabelView.vue +++ b/web/src/views/LabelView.vue @@ -1,11 +1,19 @@