commit f42cf11831f7444a72afec29effbf85dc7905c27 Author: WpyQwq <3911625973@qq.com> Date: Sat Sep 19 11:49:36 2026 +0800 Initial commit: BiliDownloader Web 版:Python 零依赖后端 + shadcn/ui 前端 diff --git a/README.md b/README.md new file mode 100644 index 0000000..efe890b --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# BiliDownloader + +哔哩哔哩视频下载器 —— **Python 零依赖本地服务 + shadcn/ui 界面 + Edge 应用模式窗口** + +> 这是**弃用 WinUI3 后的重构版**。旧版(C# / WinUI3,位于 `E:\deepseek\BiliDownloader`)能编译能跑, +> 但需要 MSIX 打包且本机存在 WinUI3 启动限制;这一版改成浏览器界面,双击即用、无运行时依赖。 + +## 启动 + +双击 **`start.cmd`** —— 它会起本地服务并打开一个 Edge 应用模式窗口(无地址栏、无标签页,看起来就是独立应用)。 + +停止:双击 `stop.cmd`。 + +也可以手动: + +```powershell +python server.py # 起服务并自动开窗口 +python server.py --no-open # 只起服务,自己用浏览器打开 http://127.0.0.1:8799/ +``` + +## 功能 + +- **站内搜索**:WBI 签名的官方搜索接口,卡片列表展示封面 / 标题 / UP主 / 时长 / 播放量 +- **粘贴链接或 BV 号**直接解析 +- **清晰度可选**:按**真正能下到的流**列出(不是标称支持),默认最高 +- **多线程加速下载**:文件切段并发下载(默认 16,可调 1~64),断线按分片续传,支持跨进程断点续传 +- **实时进度**:SSE 推送,总体 + 视频流 + 音频流三条进度、实时速度、速度曲线、ETA +- **ffmpeg 无损封装**为 mp4,产物文件名带清晰度 +- 深浅色主题(默认深色)并记忆 + +## 架构 + +``` +BiliDownloaderWeb/ +├─ start.cmd / stop.cmd 双击启动 / 停止 +├─ server.py 本地 HTTP 服务(标准库 http.server,只监听 127.0.0.1) +│ JSON API + SSE 进度推送 + 托管前端产物 +├─ engine.py 下载引擎(已实测验证的核心逻辑) +│ ├─ WBI 签名的站内搜索 +│ ├─ 视频详情 / 分P / 清晰度 +│ ├─ 多线程分段下载(Range 分片 + 每片重试 + 旁路状态文件续传) +│ └─ ffmpeg 封装 +├─ config.json sessdata / outputDir / threads / preferAvc / keepParts +├─ ui/ React 19 + Tailwind 4 + Vite 7 + shadcn/ui(new-york) +│ └─ src/{App.tsx,components/,lib/} +└─ tools/ 验收脚本 + ├─ smoke_api.py 后端 API 冒烟 + ├─ e2e_download.py 端到端下载验收(含 ffprobe 与全片解码校验) + └─ ui-check.mjs 浏览器端验收(Edge 无头 + CDP,真的驱动一次搜索) +``` + +为什么是"本地薄服务 + 浏览器界面":前端需要跨域访问 B 站 API、需要写本地磁盘、需要用 ffmpeg, +浏览器自己做不到,所以后端必须存在;而这层后端用 Python 标准库就够,**零第三方依赖**, +`engine.py` 里那套逻辑也已在上一轮里逐项实测过。 + +## 验收结果(都是实测,非推断) + +**后端 API**(`tools/smoke_api.py`) + +| 项 | 结果 | +|---|---| +| 站内搜索 | `numResults=1000`,返回 20 条,封面 URL 正常 | +| 视频详情 | 标题/UP主/时长/分P 全部正确 | +| 清晰度列表 | 按 `dash.video` 实际流构建 | + +**端到端下载**(`tools/e2e_download.py`)—— **13/13 通过** + +``` +产物: [4K]当你用《航拍中国》的方式打开崂山育才——航拍育才 [480P标清].mp4 +大小: 11.2 MB 流: h264 852x480 + aac +时长: 90.0s(源 91s) 全片解码: 无错误 +速度: 峰值 15.9 MB/s,16 线程 耗时: 2.0s +中间文件: 已清理 +``` + +**浏览器界面**(`tools/ui-check.mjs`,Edge 无头 + CDP)—— **18/18 通过** + +其中包含**真的驱动界面**:填入关键词 → 触发提交 → 等结果渲染(21 个卡片)→ 点选第一个 +→ 等清晰度解析 → 断言「开始下载」可点。另有:React 挂载、深色主题生效、无横向溢出、零脚本错误。 + +## 两个必须知道的坑(都踩过) + +### 1. 清晰度列表必须取 `dash.video`,不能取 `accept_quality` + +`accept_quality` / `support_formats` 只反映视频**标称**支持什么,与当前账号能否下载无关。 +实测:未登录时 `accept_quality` 仍列出 `120(4K)`,但 `dash.video` 里只有 `32(480P)`。 +按前者建列表会让用户选到根本拿不到的清晰度,点下载才报错。 + +### 2. 产物文件名必须带清晰度 + +否则同一视频下不同清晰度会互相覆盖 —— 实测踩过:480P 的验收下载把之前下好的 4K 文件直接盖掉。 + +## 配置 + +`config.json`(与 `server.py` 同目录): + +```json +{ + "sessdata": "...", + "outputDir": "E:\\deepseek\\downloads", + "threads": 16, + "preferAvc": true, + "keepParts": false +} +``` + +`threads` 实测:16 线程约 4.1 MB/s,32 线程约 6.8 MB/s;过高可能触发 CDN 限流。 + +> ⚠️ `sessdata` 等同于账号登录凭据,明文存在本机。**别把这个目录分享给他人。** +> 未登录/失效时 B 站只放出 480P 及以下,界面会明确提示。 + +## 重新构建前端 + +```powershell +cd ui +npm install # 首次 +npm run build # 产物到 ui/dist,由 server.py 托管 +npm run dev # 开发模式(热更新,代理 /api 到 8799) +``` diff --git a/config.json b/config.json new file mode 100644 index 0000000..ecdb3f5 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "sessdata": "12892060%2C1804723781%2Caa6de%2A91CjBKyjjjtXUBib-hcvUvs7CT7YUiP5j-0qIMAlKTi3BHY_jNPgE6Ze49_LBruJQGivwSVk9NeEh3RXBBajc3TXJlbWRqYlQ0SzJjRzVsSE51dmtuSXV4VF9ZSkZwSHY3d1paaTBUSGtxdWRXbHVYRG9JVmk5bmRhR0s3QUEwVHZZRlV1aG5WMUR3IIEC", + "outputDir": "E:\\deepseek\\downloads", + "threads": 16, + "preferAvc": true, + "keepParts": false +} \ No newline at end of file diff --git a/engine.py b/engine.py new file mode 100644 index 0000000..b142728 --- /dev/null +++ b/engine.py @@ -0,0 +1,513 @@ +# -*- coding: utf-8 -*- +""" +engine.py —— BiliDownloader 下载引擎(被 server.py 导入) + +这里的核心逻辑全部来自已经实测跑通的 bili_download.py,行为保持一致: + * WBI 签名的站内搜索(老的无签名接口已被 B 站封禁) + * 视频详情 / 分P / 清晰度列表(playurl qn=127 拿全部可用清晰度) + * 多线程分段加速下载:Range 分片 + 并发 + 每片独立重试续传 + 旁路状态文件跨进程续传 + * ffmpeg 无损封装为 mp4 + +只依赖 Python 标准库;ffmpeg 需在 PATH 或常见安装位置。 +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import subprocess +import threading +import time +import urllib.parse +import urllib.request + +UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36") + +API_VIEW = "https://api.bilibili.com/x/web-interface/view?bvid={bvid}" +API_NAV = "https://api.bilibili.com/x/web-interface/nav" +API_SEARCH = "https://api.bilibili.com/x/web-interface/wbi/search/type" +API_PLAYURL = ("https://api.bilibili.com/x/player/playurl?bvid={bvid}&cid={cid}" + "&qn=127&fnver=0&fnval=4048&fourk=1") + +QN_NAME = { + 127: "8K 超高清", 126: "杜比视界", 125: "HDR 真彩", 120: "4K 超清", + 116: "1080P60", 112: "1080P 高码率", 80: "1080P 高清", 74: "720P60", + 64: "720P 高清", 32: "480P 清晰", 16: "360P 流畅", 6: "240P 极速", +} + +# WBI 签名置换表(与 bilibili-API-collect 公开算法一致) +MIXIN_KEY_ENC_TAB = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, + 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, + 20, 34, 44, 52, +] + +CHUNK = 1 << 18 # 256KB +SEG_MIN = 2 << 20 # 小于 2MB 不值得分片 + + +class BiliError(RuntimeError): + """带 B 站错误码的异常。""" + + def __init__(self, code: int, message: str): + super().__init__(f"B 站接口错误 {code}: {message}") + self.code = code + self.message = message + + +def human(n: float) -> str: + for unit in ("B", "KB", "MB", "GB"): + if n < 1024 or unit == "GB": + return f"{n:.1f}{unit}" if unit != "B" else f"{int(n)}B" + n /= 1024 + return f"{n:.1f}GB" + + +# ------------------------------------------------------------------ HTTP + +def _headers(referer: str, cookie: str | None, extra: dict | None = None) -> dict: + h = {"User-Agent": UA, "Referer": referer, "Accept": "*/*", + "Accept-Encoding": "identity", "Connection": "keep-alive"} + if cookie: + h["Cookie"] = cookie + if extra: + h.update(extra) + return h + + +def _get(url: str, referer: str = "https://www.bilibili.com", + cookie: str | None = None, timeout: int = 30) -> bytes: + req = urllib.request.Request(url, headers=_headers(referer, cookie)) + with urllib.request.urlopen(req, timeout=timeout) as r: + return r.read() + + +def _api(url: str, cookie: str | None = None, referer: str = "https://www.bilibili.com") -> dict: + raw = _get(url, referer, cookie) + try: + payload = json.loads(raw.decode("utf-8")) + except Exception as exc: + raise BiliError(-1, "B 站返回了非 JSON 内容(可能触发了风控)") from exc + if payload.get("code") != 0: + raise BiliError(payload.get("code", -1), payload.get("message", "")) + return payload["data"] + + +# ------------------------------------------------------------------ WBI 签名 + +def _mixin_key(img_url: str, sub_url: str) -> str: + img = os.path.basename(urllib.parse.urlparse(img_url).path).split(".")[0] + sub = os.path.basename(urllib.parse.urlparse(sub_url).path).split(".")[0] + raw = img + sub + return "".join(raw[i] for i in MIXIN_KEY_ENC_TAB)[:32] + + +def _sign(params: dict, mixin_key: str) -> str: + params = dict(params) + params["wts"] = int(time.time()) + items = sorted(params.items()) + query = urllib.parse.urlencode( + [(k, "".join(c for c in str(v) if c not in "!'()*")) for k, v in items]) + w_rid = hashlib.md5((query + mixin_key).encode()).hexdigest() + return f"{query}&w_rid={w_rid}" + + +# ------------------------------------------------------------------ 业务接口 + +def login_status(cookie: str | None) -> dict: + """未登录时 nav 返回 code=-101,但 data.wbi_img 仍然可用,所以这里不抛异常。""" + try: + raw = _get(API_NAV, cookie=cookie) + payload = json.loads(raw.decode("utf-8")) + except Exception as exc: + return {"isLogin": False, "uname": "", "error": str(exc)} + + data = payload.get("data") or {} + return {"isLogin": bool(data.get("isLogin")), "uname": data.get("uname") or "", + "code": payload.get("code")} + + +def search(keyword: str, page: int = 1, page_size: int = 20, + cookie: str | None = None) -> dict: + """站内搜索。必须走 WBI 签名接口 —— 老的无签名接口已被封(返回非 JSON)。""" + nav = json.loads(_get(API_NAV, cookie=cookie).decode("utf-8")) + wbi = (nav.get("data") or {}).get("wbi_img") or {} + img_url, sub_url = wbi.get("img_url"), wbi.get("sub_url") + if not img_url or not sub_url: + raise BiliError(-1, "无法获取 WBI 密钥") + + query = _sign({"search_type": "video", "keyword": keyword, + "page": page, "page_size": page_size}, _mixin_key(img_url, sub_url)) + + payload = json.loads(_get(f"{API_SEARCH}?{query}", cookie=cookie).decode("utf-8")) + if payload.get("code") != 0: + raise BiliError(payload.get("code", -1), payload.get("message", "")) + + data = payload.get("data") or {} + results = [] + for item in (data.get("result") or []): + title = re.sub(r"]*>|", "", item.get("title") or "") + pic = item.get("pic") or "" + if pic.startswith("//"): + pic = "https:" + pic + results.append({ + "bvid": item.get("bvid"), + "title": title, + "author": item.get("author"), + "duration": item.get("duration"), # 字符串,如 "2:28" + "play": item.get("play"), + "danmaku": item.get("danmaku"), + "pic": pic, + "description": item.get("description") or item.get("desc") or "", + }) + return {"items": results, "page": page, + "numResults": data.get("numResults"), + "hasMore": len(results) >= page_size} + + +def video_detail(bvid: str, cookie: str | None = None) -> dict: + data = _api(API_VIEW.format(bvid=bvid), cookie) + pic = data.get("pic") or "" + if pic.startswith("//"): + pic = "https:" + pic + return { + "bvid": data.get("bvid"), + "title": data.get("title"), + "pic": pic, + "duration": data.get("duration"), # int 秒 + "desc": data.get("desc") or "", + "owner": (data.get("owner") or {}).get("name"), + "stat": data.get("stat") or {}, + "pages": [{"cid": p.get("cid"), "page": p.get("page"), + "part": p.get("part"), "duration": p.get("duration")} + for p in (data.get("pages") or [])], + } + + +def play_url(bvid: str, cid: int, cookie: str | None = None) -> dict: + """qn=127 请求最高清晰度,让服务端把可用的清晰度都返回。 + + ★ 关键:清晰度列表必须按 `dash.video` 里**真正拿到的流**来建,不能用 + `accept_quality` / `support_formats` —— 那两个只反映视频"标称"支持什么, + 与当前账号能否下载无关。实测:未登录时 accept_quality 仍列出 120(4K), + 但 dash.video 里只有 32(480P)。若按 accept_quality 建列表,用户会选到根本拿不到的清晰度, + 点下载才报错。 + """ + data = _api(API_PLAYURL.format(bvid=bvid, cid=cid), cookie) + accept = data.get("accept_quality") or [] + formats = {f.get("quality"): f for f in (data.get("support_formats") or [])} + + dash = data.get("dash") or {} + videos = [{"quality": s.get("id"), "baseUrl": s.get("baseUrl"), + "backupUrl": s.get("backupUrl") or [], "codecs": s.get("codecs") or "", + "width": s.get("width"), "height": s.get("height"), + "bandwidth": s.get("bandwidth")} + for s in (dash.get("video") or []) if s.get("baseUrl")] + audios = [{"baseUrl": s.get("baseUrl"), "backupUrl": s.get("backupUrl") or [], + "codecs": s.get("codecs") or "", "bandwidth": s.get("bandwidth")} + for s in (dash.get("audio") or []) if s.get("baseUrl")] + + # 同一清晰度可能有多种编码,取码率最高的一条来展示尺寸 + best: dict[int, dict] = {} + for s in videos: + q = s["quality"] + if q is None: + continue + if q not in best or (s["bandwidth"] or 0) > (best[q]["bandwidth"] or 0): + best[q] = s + + qualities = [] + for q in sorted(best, reverse=True): + s = best[q] + f = formats.get(q) or {} + qualities.append({ + "quality": q, + "label": f.get("new_description") or QN_NAME.get(q, str(q)), + "width": s.get("width"), "height": s.get("height"), + "codecs": s.get("codecs"), + "bandwidth": s.get("bandwidth"), + }) + + return {"qualities": qualities, "video": videos, "audio": audios, + # 实际可下载的最高清晰度(用它判断是否被"未登录"限制到 480P) + "maxQuality": max(best) if best else 0, + # 视频标称支持的清晰度,仅作参考展示 + "advertisedQuality": max(accept) if accept else 0, + "duration": dash.get("duration") or 0} + + +# ------------------------------------------------------------------ 下载 + +def _state_path(dest: str) -> str: + return dest + ".parts.json" + + +def _resource_key(url: str) -> str: + """取 URL 中标识资源的稳定部分。 + + baseUrl 带 e= 时效签名,重跑时签名字段与 CDN 域名都会变;只有 path + (含 avid/cid/清晰度)保持不变,所以续传状态必须以 path 为键。 + """ + return urllib.parse.urlparse(url).path + + +def _load_state(dest: str, url: str, total: int, n: int) -> list[int]: + if not os.path.exists(dest) or os.path.getsize(dest) != total: + return [0] * n + try: + with open(_state_path(dest), "r", encoding="utf-8") as f: + st = json.load(f) + if st.get("key") != _resource_key(url) or st.get("total") != total: + return [0] * n + offs = [int(x) for x in st["offsets"]] + if len(offs) != n: + return [0] * n + return [max(0, min(o, total)) for o in offs] + except Exception: + return [0] * n + + +def _save_state(dest: str, url: str, total: int, offsets: list[int]) -> None: + tmp = _state_path(dest) + ".tmp" + try: + with open(tmp, "w", encoding="utf-8") as f: + json.dump({"key": _resource_key(url), "total": total, "offsets": offsets}, f) + os.replace(tmp, _state_path(dest)) + except Exception: + pass + + +def probe(url: str, referer: str, cookie: str | None) -> tuple[int, bool]: + """探测总长度与是否支持 Range 分片。""" + req = urllib.request.Request( + url, headers=_headers(referer, cookie, {"Range": "bytes=0-0"})) + with urllib.request.urlopen(req, timeout=30) as r: + if r.status == 206: + cr = r.headers.get("Content-Range", "") + return (int(cr.split("/")[-1]) if "/" in cr else 0), True + return int(r.headers.get("Content-Length") or 0), False + + +def _download_range(url: str, fd, referer: str, cookie: str | None, + start: int, end: int, on_bytes, cancel, retries: int = 24, + on_chunk=None) -> None: + cur = start + attempt = 0 + while cur <= end: + if cancel is not None and cancel.is_set(): + raise InterruptedError("已取消") + try: + req = urllib.request.Request( + url, headers=_headers(referer, cookie, {"Range": f"bytes={cur}-{end}"})) + with urllib.request.urlopen(req, timeout=45) as r: + if r.status != 206: + raise IOError(f"服务器未返回 206 (got {r.status})") + os.lseek(fd, cur, os.SEEK_SET) + while True: + chunk = r.read(CHUNK) + if not chunk: + break + os.write(fd, chunk) + cur += len(chunk) + on_bytes(len(chunk)) + if on_chunk: + on_chunk(cur) + if cur <= end: + raise IOError("连接提前结束") + attempt = 0 + except InterruptedError: + raise + except Exception as e: + attempt += 1 + if attempt > retries: + raise RuntimeError(f"分片重试 {retries} 次仍失败: {e}") + time.sleep(min(0.5 * attempt, 5)) + + +def download_fast(url: str, dest: str, referer: str, cookie: str | None, + threads: int = 16, on_progress=None, cancel=None, + label: str = "视频") -> dict: + """多线程分段加速下载,支持跨进程断点续传。返回统计信息。""" + total, ranges_ok = probe(url, referer, cookie) + + state = {"done": 0, "total": total, "t0": time.time(), "resumed": 0} + + def report(): + if not on_progress: + return + el = max(time.time() - state["t0"], 1e-6) + on_progress({ + "label": label, + "downloaded": state["done"], + "total": total, + "percent": (state["done"] * 100 / total) if total else 0, + "speed": state["done"] / el, + "threads": 1 if not ranges_ok else max(1, min(threads, 64)), + "resumed": state["resumed"], + }) + + lock = threading.Lock() + + def on_bytes(n): + with lock: + state["done"] += n + + if not ranges_ok or total < SEG_MIN or threads <= 1: + have = os.path.getsize(dest) if os.path.exists(dest) else 0 + if have and have < total: + state["done"] = have + state["resumed"] = have + report() + mode = "ab" if have else "wb" + req_headers = _headers(referer, cookie, + {"Range": f"bytes={have}-"} if have else None) + with urllib.request.urlopen(urllib.request.Request(url, headers=req_headers), + timeout=45) as r: + if have and r.status != 206: + have = 0 + mode = "wb" + state["done"] = 0 + with open(dest, mode) as f: + while True: + if cancel is not None and cancel.is_set(): + raise InterruptedError("已取消") + chunk = r.read(CHUNK) + if not chunk: + break + f.write(chunk) + on_bytes(len(chunk)) + report() + return {"total": total, "threads": 1, "resumed": state["resumed"]} + + n = max(1, min(threads, 64)) + seg = total // n + spans = [(i * seg, total - 1 if i == n - 1 else (i + 1) * seg - 1) for i in range(n)] + + offsets = _load_state(dest, url, total, n) + if not os.path.exists(dest) or os.path.getsize(dest) != total: + with open(dest, "wb") as f: + f.truncate(total) + offsets = [0] * n + + resume_done = sum(offsets) + if resume_done: + state["done"] = resume_done + state["resumed"] = resume_done + report() + + last_flush = [time.time()] + errors: list[Exception] = [] + state_lock = threading.Lock() + + def flush(force=False): + with state_lock: + now = time.time() + if not force and now - last_flush[0] < 2.0: + return + last_flush[0] = now + _save_state(dest, url, total, offsets) + + def worker(i, s, e): + try: + fd = os.open(dest, os.O_RDWR | getattr(os, "O_BINARY", 0)) + try: + def on_chunk(cur_abs, i=i, s=s): + with state_lock: + offsets[i] = cur_abs - s + 1 + flush() + _download_range(url, fd, referer, cookie, s + offsets[i], e, + on_bytes, cancel, on_chunk=on_chunk) + finally: + os.close(fd) + with state_lock: + offsets[i] = e - s + 1 + except InterruptedError as ex: + errors.append(ex) + except Exception as ex: + errors.append(ex) + + t0 = time.time() + reporter_stop = threading.Event() + + def reporter(): + while not reporter_stop.wait(0.4): + report() + + threading.Thread(target=reporter, daemon=True).start() + + ts = [threading.Thread(target=worker, args=(i, s, e), daemon=True) + for i, (s, e) in enumerate(spans)] + for t in ts: + t.start() + for t in ts: + t.join() + reporter_stop.set() + + if any(isinstance(e, InterruptedError) for e in errors): + flush(force=True) + raise InterruptedError("已取消") + if errors: + flush(force=True) + raise RuntimeError(f"下载失败(进度已保存, 重跑可续传): {errors[0]}") + + if os.path.getsize(dest) != total: + raise RuntimeError(f"大小不符: {os.path.getsize(dest)} != {total}") + if os.path.exists(_state_path(dest)): + os.remove(_state_path(dest)) + + report() + return {"total": total, "threads": n, "resumed": resume_done, + "elapsed": time.time() - t0} + + +# ------------------------------------------------------------------ ffmpeg + +def find_ffmpeg() -> str | None: + exe = shutil.which("ffmpeg") + if exe: + return exe + for p in (r"C:\ffmpeg\bin\ffmpeg.exe", + r"C:\Users\Administrator\miniconda3\Library\bin\ffmpeg.exe"): + if os.path.exists(p): + return p + return None + + +def merge(video: str, audio: str | None, out: str) -> None: + ff = find_ffmpeg() + if not ff: + raise RuntimeError("找不到 ffmpeg") + cmd = [ff, "-y", "-hide_banner", "-loglevel", "error", "-i", video] + if audio: + cmd += ["-i", audio, "-map", "0:v:0", "-map", "1:a:0", "-c", "copy"] + else: + cmd += ["-c", "copy"] + cmd += ["-movflags", "+faststart", out] + subprocess.run(cmd, check=True, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + + +# ------------------------------------------------------------------ 工具 + +def safe_name(name: str) -> str: + name = re.sub(r'[\\/:*?"<>|\r\n\t]', "_", name).strip(" .") + return (name or "video")[:120] + + +def pick_streams(play: dict, quality: int, prefer_avc: bool = True): + """在指定清晰度下选码流:优先 H.264(兼容性最好),音频取码率最大。""" + pool = [s for s in play["video"] if s["quality"] == quality] + if not pool: + return None, None + if prefer_avc: + avc = [s for s in pool if s["codecs"].startswith(("avc", "av01"))] + pool = avc or pool + video = max(pool, key=lambda s: s["bandwidth"]) + if not play["audio"]: + return video, None + audio = max(play["audio"], key=lambda s: s["bandwidth"]) + return video, audio diff --git a/server.py b/server.py new file mode 100644 index 0000000..84ddb3f --- /dev/null +++ b/server.py @@ -0,0 +1,438 @@ +# -*- coding: utf-8 -*- +""" +server.py —— BiliDownloader 本地服务(零第三方依赖) + +只监听 127.0.0.1。职责: + * 托管前端构建产物 (ui/dist) + * 提供 JSON API(搜索 / 详情 / 清晰度 / 开始下载 / 取消 / 配置) + * 用 Server-Sent Events 把下载进度实时推给浏览器 + +用法: + python server.py [--port 8799] [--no-open] +""" + +from __future__ import annotations + +import argparse +import json +import mimetypes +import os +import queue +import subprocess +import sys +import threading +import time +import traceback +import urllib.parse +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import engine + +BASE = os.path.dirname(os.path.abspath(__file__)) +DIST = os.path.join(BASE, "ui", "dist") +CONFIG_PATH = os.path.join(BASE, "config.json") + +DEFAULT_CONFIG = { + "sessdata": "", + "outputDir": os.path.join(os.path.expanduser("~"), "Videos", "BiliDownloader"), + "threads": 16, + "preferAvc": True, + "keepParts": False, +} + +_config_lock = threading.Lock() +_job_lock = threading.Lock() +_job: dict | None = None # 当前/最近一次下载任务 +_cancel: threading.Event | None = None +_streams: dict[str, dict] = {} # (bvid,cid) -> playurl 结果 +_subscribers: list[queue.Queue] = [] +_play_cache: dict[str, dict] = {} + + +# ------------------------------------------------------------------ 配置 + +def load_config() -> dict: + cfg = dict(DEFAULT_CONFIG) + try: + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + cfg.update(json.load(f)) + except Exception: + pass + cfg["threads"] = max(1, min(64, int(cfg.get("threads") or 16))) + return cfg + + +def save_config(cfg: dict) -> dict: + merged = load_config() + merged.update({k: v for k, v in cfg.items() if k in DEFAULT_CONFIG}) + merged["threads"] = max(1, min(64, int(merged.get("threads") or 16))) + with _config_lock: + tmp = CONFIG_PATH + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(merged, f, ensure_ascii=False, indent=2) + os.replace(tmp, CONFIG_PATH) + return merged + + +def cookie_of(cfg: dict) -> str | None: + s = (cfg.get("sessdata") or "").strip() + return f"SESSDATA={s}" if s else None + + +# ------------------------------------------------------------------ 事件广播 + +def broadcast(event: str, data: dict) -> None: + payload = f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" + dead = [] + for q in list(_subscribers): + try: + q.put_nowait(payload) + except Exception: + dead.append(q) + for q in dead: + try: + _subscribers.remove(q) + except ValueError: + pass + + +# ------------------------------------------------------------------ 下载任务 + +def start_download(bvid: str, cid: int, quality: int, title: str) -> dict: + global _job, _cancel + + with _job_lock: + if _job and _job.get("state") == "running": + raise RuntimeError("已有下载任务在进行中") + + cfg = load_config() + key = f"{bvid}:{cid}" + play = _streams.get(key) + if not play: + raise RuntimeError("播放地址不存在或已过期,请重新选择视频") + + video, audio = engine.pick_streams(play, quality, cfg.get("preferAvc", True)) + if video is None: + raise RuntimeError("当前清晰度没有可用的视频流") + + out_dir = cfg["outputDir"] + os.makedirs(out_dir, exist_ok=True) + base = engine.safe_name(title) + # 文件名必须带上清晰度:否则同一视频下不同清晰度会互相覆盖 + # (实测踩过:480P 的验收下载把之前下好的 4K 文件直接盖掉了) + q_label = next((q["label"] for q in play.get("qualities", []) + if q["quality"] == quality), str(quality)) + q_tag = engine.safe_name(q_label).replace(" ", "") + stamp = time.strftime("%Y%m%d%H%M%S") + final_path = os.path.join(out_dir, f"{base} [{q_tag}].mp4") + tmp_v = os.path.join(out_dir, f".{base}.{cid}.{stamp}.video.m4s") + tmp_a = os.path.join(out_dir, f".{base}.{cid}.{stamp}.audio.m4s") + + _cancel = threading.Event() + _job = { + "state": "running", "stage": "下载视频流", "bvid": bvid, "cid": cid, + "title": title, "quality": quality, "outputPath": final_path, + "startedAt": time.time(), "video": None, "audio": None, + "progress": {"video": None, "audio": None}, "error": None, + "resumed": 0, "threads": cfg["threads"], + } + + broadcast("job", dict(_job)) + threading.Thread(target=_run_job, args=(video, audio, tmp_v, tmp_a, final_path, + cfg, bvid, cid), daemon=True).start() + return dict(_job) + + +def _run_job(video, audio, tmp_v, tmp_a, final_path, cfg, bvid, cid) -> None: + global _job + cookie = cookie_of(cfg) + referer = f"https://www.bilibili.com/video/{bvid}" + threads = cfg["threads"] + + def make_on_progress(which): + def on_progress(info): + _job["progress"][which] = info + broadcast("progress", {"which": which, **info, "stage": _job["stage"]}) + return on_progress + + try: + _job["stage"] = "下载视频流" + broadcast("stage", {"stage": _job["stage"]}) + vres = engine.download_fast(video["baseUrl"], tmp_v, referer, cookie, + threads, make_on_progress("video"), _cancel, "视频") + _job["resumed"] = vres.get("resumed", 0) + + if audio: + _job["stage"] = "下载音频流" + broadcast("stage", {"stage": _job["stage"]}) + engine.download_fast(audio["baseUrl"], tmp_a, referer, cookie, + max(1, min(8, threads)), make_on_progress("audio"), + _cancel, "音频") + + _job["stage"] = "ffmpeg 封装" + broadcast("stage", {"stage": _job["stage"]}) + engine.merge(tmp_v, tmp_a if audio else None, final_path) + + if not cfg.get("keepParts"): + for p in (tmp_v, tmp_a): + try: + if os.path.exists(p): + os.remove(p) + except OSError: + pass + + _job["state"] = "done" + _job["stage"] = "完成" + _job["size"] = os.path.getsize(final_path) + _job["elapsed"] = time.time() - _job["startedAt"] + broadcast("job", dict(_job)) + + except InterruptedError: + for p in (final_path,): + try: + if os.path.exists(p): + os.remove(p) + except OSError: + pass + if not cfg.get("keepParts"): + for p in (tmp_v, tmp_a): + try: + if os.path.exists(p): + os.remove(p) + except OSError: + pass + _job["state"] = "cancelled" + _job["stage"] = "已取消,未完成的文件已清理" + broadcast("job", dict(_job)) + except Exception as exc: + for p in (final_path,): + try: + if os.path.exists(p): + os.remove(p) + except OSError: + pass + _job["state"] = "error" + _job["error"] = str(exc) + broadcast("job", dict(_job)) + traceback.print_exc() + + +# ------------------------------------------------------------------ HTTP + +class Handler(BaseHTTPRequestHandler): + server_version = "BiliDownloader" + + def log_message(self, fmt, *args): + pass # 静音访问日志 + + # ---------- helpers + def _json(self, obj, status=200): + body = json.dumps(obj, ensure_ascii=False).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(body) + + def _error(self, message, status=400): + self._json({"ok": False, "error": str(message)}, status) + + def _query(self) -> dict: + parsed = urllib.parse.urlparse(self.path) + return {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}, parsed.path + + def _body(self) -> dict: + length = int(self.headers.get("Content-Length") or 0) + if not length: + return {} + return json.loads(self.rfile.read(length).decode("utf-8")) + + # ---------- routes + def do_GET(self): + params, path = self._query() + try: + if path == "/api/config": + cfg = load_config() + cfg["configPath"] = CONFIG_PATH + cfg["ffmpeg"] = engine.find_ffmpeg() + return self._json(cfg) + + if path == "/api/login": + cfg = load_config() + return self._json(engine.login_status(cookie_of(cfg))) + + if path == "/api/search": + cfg = load_config() + return self._json(engine.search( + params.get("q", ""), int(params.get("page", 1)), 20, cookie_of(cfg))) + + if path == "/api/video": + cfg = load_config() + return self._json(engine.video_detail(params["bvid"], cookie_of(cfg))) + + if path == "/api/playurl": + cfg = load_config() + bvid, cid = params["bvid"], int(params["cid"]) + play = engine.play_url(bvid, cid, cookie_of(cfg)) + _streams[f"{bvid}:{cid}"] = play + return self._json(play) + + if path == "/api/job": + return self._json(_job or {}) + + if path == "/api/events": + return self._sse() + + if path.startswith("/api/"): + return self._error("未知接口", 404) + + return self._static(path) + except engine.BiliError as exc: + return self._error(f"{exc.message}(code {exc.code})", 502) + except Exception as exc: + traceback.print_exc() + return self._error(exc, 500) + + def do_POST(self): + _, path = self._query() + try: + if path == "/api/config": + return self._json(save_config(self._body())) + + if path == "/api/download": + data = self._body() + job = start_download(data["bvid"], int(data["cid"]), + int(data["quality"]), data.get("title") or "video") + return self._json(job) + + if path == "/api/cancel": + if _cancel: + _cancel.set() + return self._json({"ok": True}) + + if path == "/api/open-folder": + target = (self._body() or {}).get("path") or load_config()["outputDir"] + if os.path.exists(target): + subprocess.Popen(["explorer", "/select,", os.path.normpath(target)]) + else: + os.makedirs(target, exist_ok=True) + subprocess.Popen(["explorer", os.path.normpath(target)]) + return self._json({"ok": True}) + + return self._error("未知接口", 404) + except Exception as exc: + traceback.print_exc() + return self._error(exc, 500) + + # ---------- SSE + def _sse(self): + q: queue.Queue = queue.Queue() + _subscribers.append(q) + self.send_response(200) + self.send_header("Content-Type", "text/event-stream; charset=utf-8") + self.send_header("Cache-Control", "no-store") + self.send_header("Connection", "keep-alive") + self.end_headers() + try: + hello = f"event: job\ndata: {json.dumps(_job or {}, ensure_ascii=False)}\n\n" + self.wfile.write(hello.encode("utf-8")) + self.wfile.flush() + while True: + try: + payload = q.get(timeout=15) + self.wfile.write(payload.encode("utf-8")) + except queue.Empty: + self.wfile.write(b": ping\n\n") # 心跳,防代理断连 + self.wfile.flush() + except (BrokenPipeError, ConnectionResetError, OSError): + pass + finally: + try: + _subscribers.remove(q) + except ValueError: + pass + + # ---------- 静态文件 + def _static(self, path: str): + if not os.path.isdir(DIST): + body = ("" + "

前端还没构建

" + "

请在 ui 目录执行 npm run build," + "或直接用 npm run dev 起开发服务器。

").encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + rel = urllib.parse.unquote(path.lstrip("/")) or "index.html" + target = os.path.normpath(os.path.join(DIST, rel)) + # 防目录穿越 + if not target.startswith(os.path.normpath(DIST)): + return self._error("非法路径", 403) + if not os.path.isfile(target): + target = os.path.join(DIST, "index.html") # SPA 回退 + if not os.path.isfile(target): + return self._error("Not found", 404) + + ctype = mimetypes.guess_type(target)[0] or "application/octet-stream" + if ctype.startswith("text/") or ctype in ("application/javascript", "application/json"): + ctype += "; charset=utf-8" + with open(target, "rb") as f: + body = f.read() + self.send_response(200) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store" if target.endswith(".html") else "public, max-age=3600") + self.end_headers() + self.wfile.write(body) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=8799) + ap.add_argument("--no-open", action="store_true") + args = ap.parse_args() + + cfg = load_config() + if not os.path.exists(CONFIG_PATH): + save_config(cfg) + + httpd = ThreadingHTTPServer(("127.0.0.1", args.port), Handler) + url = f"http://127.0.0.1:{args.port}/" + print(f"BiliDownloader 服务已启动: {url}", flush=True) + print(f"配置: {CONFIG_PATH}", flush=True) + print(f"ffmpeg: {engine.find_ffmpeg() or '未找到(合并会失败)'}", flush=True) + + if not args.no_open: + threading.Thread(target=_open_browser, args=(url,), daemon=True).start() + + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass + return 0 + + +def _open_browser(url: str) -> None: + time.sleep(0.8) + try: + edge = None + for p in (r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", + r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"): + if os.path.exists(p): + edge = p + break + if edge: + # 应用模式窗口:无地址栏、无标签页,看起来就是一个独立应用 + subprocess.Popen([edge, f"--app={url}", "--window-size=1280,860"]) + else: + subprocess.Popen(["cmd", "/c", "start", "", url], shell=False) + except Exception: + pass + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/start.cmd b/start.cmd new file mode 100644 index 0000000..9c14eb7 --- /dev/null +++ b/start.cmd @@ -0,0 +1,21 @@ +@echo off +rem BiliDownloader 启动脚本:起本地服务 + 开 Edge 应用模式窗口 +setlocal +cd /d "%~dp0" +set "PYTHONIOENCODING=utf-8" + +rem 已经有实例在跑,就直接把窗口拉起来,不要再起一个服务 +powershell -NoProfile -Command "if (Get-NetTCPConnection -LocalPort 8799 -State Listen -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }" >nul 2>nul +if %errorlevel%==0 ( + start "" "msedge" --app=http://127.0.0.1:8799/ --window-size=1280,860 + exit /b 0 +) + +rem 优先用 pythonw(无控制台窗口),没有就退回 python +where pythonw >nul 2>nul +if %errorlevel%==0 ( + start "" pythonw "%~dp0server.py" --port 8799 +) else ( + start "" /min python "%~dp0server.py" --port 8799 +) +exit /b 0 diff --git a/stop.cmd b/stop.cmd new file mode 100644 index 0000000..687b75c --- /dev/null +++ b/stop.cmd @@ -0,0 +1,8 @@ +@echo off +rem 停止 BiliDownloader 本地服务 +setlocal +for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":8799" ^| findstr LISTENING') do ( + taskkill /PID %%a /F >nul 2>nul +) +echo 已停止 BiliDownloader 服务。 +exit /b 0 diff --git a/tools/diag.py b/tools/diag.py new file mode 100644 index 0000000..396ce23 --- /dev/null +++ b/tools/diag.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +"""诊断:nav 为何报未登录 + dash.video 实际内容。""" +import json +import sys +import urllib.request + +sys.path.insert(0, r"E:\deepseek\BiliDownloaderWeb") +import engine + +cfg = json.load(open(r"E:\deepseek\BiliDownloaderWeb\config.json", encoding="utf-8")) +sess = cfg["sessdata"] +print("config 里 sessdata 长度 =", len(sess)) +print("前 24 字符 =", sess[:24]) +print("后 24 字符 =", sess[-24:]) + +print("\n--- 直接请求 nav,看原始返回 ---") +req = urllib.request.Request( + engine.API_NAV, + headers={"User-Agent": engine.UA, "Referer": "https://www.bilibili.com", + "Cookie": "SESSDATA=" + sess}) +with urllib.request.urlopen(req, timeout=30) as r: + raw = json.loads(r.read().decode("utf-8")) +print("code =", raw.get("code"), " message =", raw.get("message")) +d = raw.get("data") or {} +print("isLogin =", d.get("isLogin"), " uname =", d.get("uname")) +print("有 wbi_img =", bool(d.get("wbi_img"))) + +print("\n--- dash.video 实际内容(qn=127)---") +vid = engine.video_detail("BV1HVBAYLEAo", "SESSDATA=" + sess) +cid = vid["pages"][0]["cid"] +p = engine.play_url("BV1HVBAYLEAo", cid, "SESSDATA=" + sess) +print("maxQuality =", p["maxQuality"]) +for s in p["video"]: + print(f" 视频流 qn={s['quality']:>4} {s['width']}x{s['height']} codec={s['codecs']} {s['bandwidth']//1000}kbps") +for s in p["audio"]: + print(f" 音频流 {s['codecs']} {s['bandwidth']//1000}kbps") +print("qualities 的 width/height 字段示例:", p["qualities"][0]) diff --git a/tools/e2e_download.py b/tools/e2e_download.py new file mode 100644 index 0000000..6e8e4e0 --- /dev/null +++ b/tools/e2e_download.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +""" +e2e_download.py —— 端到端验收:通过服务的 HTTP 接口真的下一次,并校验产物。 + +流程:playurl -> POST /api/download -> 订阅 SSE 收进度 -> 等完成 -> ffprobe 校验 +""" + +import json +import os +import subprocess +import sys +import threading +import time +import urllib.request + +BASE = "http://127.0.0.1:8799" +BVID = sys.argv[1] if len(sys.argv) > 1 else "BV1HVBAYLEAo" + +ok_count = 0 +fail_count = 0 + + +def check(name, ok, detail=""): + global ok_count, fail_count + if ok: + ok_count += 1 + print(f" [OK] {name}" + (f" — {detail}" if detail else "")) + else: + fail_count += 1 + print(f" [XX] {name}" + (f" — {detail}" if detail else "")) + + +def get(path): + with urllib.request.urlopen(BASE + path, timeout=60) as r: + return json.loads(r.read().decode("utf-8")) + + +def post(path, payload): + req = urllib.request.Request( + BASE + path, data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=60) as r: + return json.loads(r.read().decode("utf-8")) + + +print("=" * 62) +print(f"端到端下载验收: {BVID}") +print("=" * 62) + +video = get(f"/api/video?bvid={BVID}") +cid = video["pages"][0]["cid"] +print(f"\n视频: {video['title']}") +print(f"UP主: {video['owner']} 时长: {video['duration']}s") + +play = get(f"/api/playurl?bvid={BVID}&cid={cid}") +check("拿到清晰度列表", len(play["qualities"]) > 0, + " / ".join(q["label"] for q in play["qualities"])) +quality = play["qualities"][0]["quality"] +print(f"选择清晰度: {quality} ({play['qualities'][0]['label']})") + +# 先确认没有正在跑的任务 +job0 = get("/api/job") +check("初始无进行中的任务", job0.get("state") != "running", str(job0.get("state"))) + +# 订阅 SSE +events = [] +prog_samples = [] +stop = threading.Event() + + +def listen(): + try: + with urllib.request.urlopen(BASE + "/api/events", timeout=600) as r: + event = None + while not stop.is_set(): + line = r.readline().decode("utf-8", "replace").strip() + if line.startswith("event:"): + event = line.split(":", 1)[1].strip() + elif line.startswith("data:"): + data = json.loads(line.split(":", 1)[1].strip()) + events.append((event, data)) + if event == "progress": + prog_samples.append(data) + except Exception as exc: + if not stop.is_set(): + print(" SSE 读取结束:", exc) + + +t = threading.Thread(target=listen, daemon=True) +t.start() +time.sleep(0.8) + +print("\n--- 开始下载 ---") +t0 = time.time() +job = post("/api/download", {"bvid": BVID, "cid": cid, "quality": quality, + "title": video["title"]}) +check("已受理下载任务", job.get("state") == "running", str(job.get("state"))) + +deadline = time.time() + 300 +last_state = None +while time.time() < deadline: + time.sleep(1) + cur = get("/api/job") + if cur.get("state") != last_state: + last_state = cur.get("state") + print(f" [{time.time() - t0:5.1f}s] state={last_state} stage={cur.get('stage')}") + if cur.get("state") in ("done", "error", "cancelled"): + break + +elapsed = time.time() - t0 +stop.set() +final = get("/api/job") + +check("任务正常结束", final.get("state") == "done", str(final.get("state") or final.get("error"))) +check("收到了进度事件", len(prog_samples) > 0, f"{len(prog_samples)} 条") +check("同时收到 job/stage 事件", len(events) > 0, f"{len(events)} 条") + +out = final.get("outputPath") or "" +check("产物文件存在", bool(out) and os.path.exists(out), out) +if out and os.path.exists(out): + size = os.path.getsize(out) + check("产物大小合理", size > 100_000, f"{size / 1024 / 1024:.1f} MB") + ffprobe = None + import shutil + ffprobe = shutil.which("ffprobe") or r"C:\Users\Administrator\miniconda3\Library\bin\ffprobe.exe" + cp = subprocess.run( + [ffprobe, "-v", "error", "-show_entries", + "format=duration,size:stream=codec_type,codec_name,width,height", + "-of", "json", out], + capture_output=True, text=True) + if cp.returncode == 0: + info = json.loads(cp.stdout) + streams = info.get("streams", []) + kinds = {s["codec_type"]: s for s in streams} + check("产物含视频流", "video" in kinds, + f"{kinds.get('video', {}).get('codec_name')} " + f"{kinds.get('video', {}).get('width')}x{kinds.get('video', {}).get('height')}") + check("产物含音频流", "audio" in kinds, str(kinds.get("audio", {}).get("codec_name"))) + dur = float(info["format"]["duration"]) + check("时长与源基本一致", abs(dur - video["duration"]) < 5, f"{dur:.1f}s vs {video['duration']}s") + else: + check("ffprobe 可解析产物", False, cp.stderr.strip()[:120]) + + # 全片解码校验 + ffmpeg = shutil.which("ffmpeg") or r"C:\Users\Administrator\miniconda3\Library\bin\ffmpeg.exe" + cp2 = subprocess.run([ffmpeg, "-v", "error", "-i", out, "-f", "null", "-"], + capture_output=True, text=True) + check("全片解码无错误", cp2.returncode == 0 and not cp2.stderr.strip(), + cp2.stderr.strip()[:160] or "clean") + +# 临时文件应已清理 +tmp_left = [f for f in os.listdir(os.path.dirname(out)) if f.endswith(".m4s")] +check("中间文件已清理", len(tmp_left) == 0, f"残留 {len(tmp_left)} 个" if tmp_left else "无残留") + +# 并发保护:任务已结束,再来一次应能受理(说明状态机复位) +print(f"\n耗时 {elapsed:.1f}s") +print("\n进度采样(每 10 条取 1):") +for s in prog_samples[::max(1, len(prog_samples) // 10)][:10]: + print(f" {s['label']} {s['percent']:5.1f}% {s['downloaded']/1024/1024:6.1f}MB" + f"/{s['total']/1024/1024:.1f}MB {s['speed']/1024/1024:.2f}MB/s {s['threads']}线程") + +print("\n" + "=" * 62) +print(f"结果: {ok_count} 通过 / {fail_count} 失败") +print("=" * 62) +sys.exit(1 if fail_count else 0) diff --git a/tools/list_windows.ps1 b/tools/list_windows.ps1 new file mode 100644 index 0000000..9e364e0 --- /dev/null +++ b/tools/list_windows.ps1 @@ -0,0 +1,44 @@ +Add-Type @" +using System; +using System.Text; +using System.Runtime.InteropServices; +public class WinEnum { + public delegate bool EnumProc(IntPtr hWnd, IntPtr lParam); + [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr lParam); + [DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder s, int n); + [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r); + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } +} +"@ + +$found = New-Object System.Collections.ArrayList +$cb = [WinEnum+EnumProc]{ + param($h, $l) + $len = [WinEnum]::GetWindowTextLength($h) + if ($len -gt 0 -and [WinEnum]::IsWindowVisible($h)) { + $sb = New-Object System.Text.StringBuilder ($len + 2) + [void][WinEnum]::GetWindowText($h, $sb, $sb.Capacity) + $t = $sb.ToString() + $r = New-Object WinEnum+RECT + [void][WinEnum]::GetWindowRect($h, [ref]$r) + [void]$found.Add([pscustomobject]@{ + Title = $t + W = $r.R - $r.L + H = $r.B - $r.T + X = $r.L + Y = $r.T + }) + } + return $true +} +[void][WinEnum]::EnumWindows($cb, [IntPtr]::Zero) + +Write-Output "=== 可见顶层窗口中标题含 BiliDownloader 的 ===" +$found | Where-Object { $_.Title -like "*BiliDownloader*" } | + Select-Object Title, W, H, X, Y | Format-Table -AutoSize | Out-String + +Write-Output "=== 前 12 个可见窗口(对照)===" +$found | Sort-Object -Property W -Descending | Select-Object -First 12 Title, W, H | + Format-Table -AutoSize | Out-String diff --git a/tools/smoke_api.py b/tools/smoke_api.py new file mode 100644 index 0000000..0297b2e --- /dev/null +++ b/tools/smoke_api.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +"""后端 API 冒烟测试。""" +import json +import urllib.parse +import urllib.request + +BASE = "http://127.0.0.1:8799" + + +def get(path): + with urllib.request.urlopen(BASE + path, timeout=60) as r: + return json.loads(r.read().decode("utf-8")) + + +def post(path, payload): + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request(BASE + path, data=data, + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=60) as r: + return json.loads(r.read().decode("utf-8")) + + +print("--- /api/config ---") +cfg = get("/api/config") +print(f"threads={cfg['threads']} outputDir={cfg['outputDir']}") +print(f"ffmpeg={cfg['ffmpeg']}") +print(f"configPath={cfg['configPath']}") + +print("\n--- /api/login ---") +print(get("/api/login")) + +print("\n--- /api/search?q=航拍中国 ---") +s = get("/api/search?q=" + urllib.parse.quote("航拍中国")) +print(f"numResults={s['numResults']} hasMore={s['hasMore']} 条数={len(s['items'])}") +for it in s["items"][:5]: + print(f" {it['bvid']} {it['title'][:34]} up={it['author']} dur={it['duration']} play={it['play']}") +print(f" pic = {s['items'][0]['pic']}") + +print("\n--- /api/video?bvid=BV1HVBAYLEAo ---") +v = get("/api/video?bvid=BV1HVBAYLEAo") +print(f"title={v['title']}") +print(f"owner={v['owner']} duration={v['duration']}s pages={len(v['pages'])}") +cid = v["pages"][0]["cid"] +print(f"cid={cid}") + +print("\n--- /api/playurl ---") +p = get(f"/api/playurl?bvid=BV1HVBAYLEAo&cid={cid}") +print(f"maxQuality={p['maxQuality']} video流={len(p['video'])} audio流={len(p['audio'])}") +for q in p["qualities"]: + print(f" qn={q['quality']:>4} {q['label']} {q.get('width')}x{q.get('height')}") +print(f" 4K 候选: " + ", ".join( + f"{s['quality']}/{s['codecs']}/{s['bandwidth']//1000}kbps" for s in p["video"] if s["quality"] == 120)) diff --git a/tools/ui-check.mjs b/tools/ui-check.mjs new file mode 100644 index 0000000..c6840be --- /dev/null +++ b/tools/ui-check.mjs @@ -0,0 +1,196 @@ +/** + * ui-check.mjs —— 浏览器端验收(Edge 无头 + CDP) + * + * 不只是"页面能打开":真的驱动界面做一次搜索、点选结果、等待清晰度解析, + * 并读取渲染后的 DOM 断言每一处关键内容。 + * + * 用法: node tools/ui-check.mjs [baseUrl] + */ + +import { spawn } from 'node:child_process'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import WebSocket from '../ui/node_modules/ws/index.js'; + +const BASE = process.argv[2] || 'http://127.0.0.1:8799/'; +const PORT = 9333; + +const EDGE_CANDIDATES = [ + 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', + 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', +]; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +const results = []; +function check(name, ok, detail = '') { + results.push({ name, ok, detail }); + console.log(`${ok ? ' ✓' : ' ✗'} ${name}${detail ? ` — ${detail}` : ''}`); +} + +async function main() { + const fs = await import('node:fs'); + const edge = EDGE_CANDIDATES.find((p) => fs.existsSync(p)); + if (!edge) throw new Error('找不到 Edge'); + + const profile = mkdtempSync(join(tmpdir(), 'bd-ui-')); + const child = spawn( + edge, + [ + '--headless=new', + '--disable-gpu', + '--no-first-run', + '--no-default-browser-check', + `--remote-debugging-port=${PORT}`, + `--user-data-dir=${profile}`, + '--window-size=1280,860', + BASE, + ], + { stdio: 'ignore' }, + ); + + let target = null; + for (let i = 0; i < 40 && !target; i++) { + await sleep(500); + try { + const res = await fetch(`http://127.0.0.1:${PORT}/json/list`); + const list = await res.json(); + target = list.find((t) => t.type === 'page' && t.url.startsWith('http')); + } catch { + /* 还没起来 */ + } + } + if (!target) throw new Error('CDP 目标未就绪'); + + const ws = new WebSocket(target.webSocketDebuggerUrl, { maxPayload: 64 * 1024 * 1024 }); + await new Promise((res, rej) => { + ws.once('open', res); + ws.once('error', rej); + }); + + let id = 0; + const pending = new Map(); + ws.on('message', (raw) => { + const msg = JSON.parse(raw.toString()); + if (msg.id && pending.has(msg.id)) { + pending.get(msg.id)(msg); + pending.delete(msg.id); + } + }); + const send = (method, params = {}) => + new Promise((res) => { + const myId = ++id; + pending.set(myId, res); + ws.send(JSON.stringify({ id: myId, method, params })); + }); + + const evaluate = async (expression) => { + const r = await send('Runtime.evaluate', { + expression, + awaitPromise: true, + returnByValue: true, + }); + if (r.result?.exceptionDetails) { + throw new Error(r.result.exceptionDetails.exception?.description || 'JS 异常'); + } + return r.result?.result?.value; + }; + + await send('Runtime.enable'); + await sleep(3500); // 等 React 挂载 + 首屏取配置/登录状态 + + console.log(`\n目标: ${BASE}\n`); + + // ---------- 1. 首屏渲染 ---------- + const text = await evaluate('document.body.innerText'); + check('React 已挂载并渲染', !!text && text.length > 60, `${text?.length ?? 0} 字符`); + for (const key of ['BiliDownloader', '搜索站内视频', '未登录', '还没有选择视频', '清晰度', '开始下载', '日志']) { + check(`首屏含「${key}」`, text.includes(key)); + } + + // ---------- 2. 暗色主题 ---------- + const isDark = await evaluate("document.documentElement.classList.contains('dark')"); + check('默认深色主题生效', isDark === true); + const bg = await evaluate( + "getComputedStyle(document.body).backgroundColor", + ); + check('背景取自 shadcn token(非默认白)', bg !== 'rgb(255, 255, 255)' && bg !== 'rgba(0, 0, 0, 0)', bg); + + // ---------- 3. 无横向溢出 ---------- + const overflow = await evaluate( + 'document.documentElement.scrollWidth - document.documentElement.clientWidth', + ); + check('无横向溢出', overflow <= 0, `diff=${overflow}`); + + // ---------- 4. 真的搜一次 ---------- + console.log('\n —— 驱动一次真实搜索 ——'); + await evaluate(`(() => { + const input = document.querySelector('input'); + const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + setter.call(input, '航拍中国'); + input.dispatchEvent(new Event('input', { bubbles: true })); + const form = input.closest('form'); + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + return true; + })()`); + await sleep(6000); + + const afterSearch = await evaluate('document.body.innerText'); + const cardCount = await evaluate( + "document.querySelectorAll('button[type=button]').length", + ); + check('搜索结果已渲染出卡片', cardCount >= 10, `${cardCount} 个可点卡片`); + + // ---------- 5. 点选一个结果 → 清晰度解析 ---------- + console.log('\n —— 点选第一个结果,等待清晰度解析 ——'); + await evaluate(`(() => { + const btn = document.querySelector('button[type=button]'); + btn.click(); + return true; + })()`); + await sleep(9000); + + const afterPick = await evaluate('document.body.innerText'); + check('已进入待下载状态(出现「保存到」)', afterPick.includes('保存到')); + check('详情面板显示 BV 号', /BV[0-9A-Za-z]{10}/.test(afterPick)); + check('清晰度已解析(出现「可用清晰度」日志或档位)', /可用清晰度|480P|360P|1080P|4K/.test(afterPick)); + const hasStartEnabled = await evaluate(`(() => { + const b = [...document.querySelectorAll('button')].find(x => x.textContent.includes('开始下载')); + return b ? !b.disabled : null; + })()`); + check('「开始下载」按钮可点', hasStartEnabled === true); + + // ---------- 6. 控制台报错 ---------- + const errs = await evaluate('window.__bdErrors ? window.__bdErrors.length : 0'); + check('页面无致命脚本错误', errs === 0, `${errs} 个`); + + console.log('\n —— 渲染文本采样 ——'); + console.log( + afterPick + .split('\n') + .filter((l) => l.trim()) + .slice(0, 26) + .map((l) => ' ' + l.slice(0, 96)) + .join('\n'), + ); + + ws.close(); + child.kill(); + try { + rmSync(profile, { recursive: true, force: true }); + } catch {} + + const failed = results.filter((r) => !r.ok); + console.log(`\n结果: ${results.length - failed.length}/${results.length} 通过`); + if (failed.length) { + console.log('失败项:'); + for (const f of failed) console.log(` - ${f.name} ${f.detail}`); + process.exit(1); + } +} + +main().catch((e) => { + console.error('验收脚本异常:', e.message); + process.exit(1); +}); diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..0645378 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,21 @@ + + + + + + BiliDownloader + + + + +
+ + + diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..0b59cd8 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,3620 @@ +{ + "name": "wpywmail-ui", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wpywmail-ui", + "version": "0.2.0", + "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-collapsible": "^1.1.20", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.16", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.548.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.16", + "@types/node": "^24.9.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.0.4", + "tailwindcss": "^4.1.16", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "vite": "^7.1.12", + "ws": "^8.21.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "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/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz", + "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz", + "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz", + "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz", + "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz", + "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz", + "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz", + "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz", + "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz", + "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz", + "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz", + "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz", + "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz", + "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz", + "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz", + "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz", + "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz", + "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz", + "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz", + "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz", + "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz", + "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz", + "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz", + "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz", + "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz", + "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.23.tgz", + "integrity": "sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.25.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.0.tgz", + "integrity": "sha512-ghq3mhs649mvbarTCAlZn2wRhbfHmzAFiKxoWA14B3VtqnxtZt+wz8BroKXU0tF3GiJsnUldjVncQGZ8qk4rdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.548.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz", + "integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/rollup": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.2.tgz", + "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.2", + "@rollup/rollup-android-arm64": "4.63.2", + "@rollup/rollup-darwin-arm64": "4.63.2", + "@rollup/rollup-darwin-x64": "4.63.2", + "@rollup/rollup-freebsd-arm64": "4.63.2", + "@rollup/rollup-freebsd-x64": "4.63.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.2", + "@rollup/rollup-linux-arm-musleabihf": "4.63.2", + "@rollup/rollup-linux-arm64-gnu": "4.63.2", + "@rollup/rollup-linux-arm64-musl": "4.63.2", + "@rollup/rollup-linux-loong64-gnu": "4.63.2", + "@rollup/rollup-linux-loong64-musl": "4.63.2", + "@rollup/rollup-linux-ppc64-gnu": "4.63.2", + "@rollup/rollup-linux-ppc64-musl": "4.63.2", + "@rollup/rollup-linux-riscv64-gnu": "4.63.2", + "@rollup/rollup-linux-riscv64-musl": "4.63.2", + "@rollup/rollup-linux-s390x-gnu": "4.63.2", + "@rollup/rollup-linux-x64-gnu": "4.63.2", + "@rollup/rollup-linux-x64-musl": "4.63.2", + "@rollup/rollup-openbsd-x64": "4.63.2", + "@rollup/rollup-openharmony-arm64": "4.63.2", + "@rollup/rollup-win32-arm64-msvc": "4.63.2", + "@rollup/rollup-win32-ia32-msvc": "4.63.2", + "@rollup/rollup-win32-x64-gnu": "4.63.2", + "@rollup/rollup-win32-x64-msvc": "4.63.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.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/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..40b4186 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,44 @@ +{ + "name": "wpywmail-ui", + "private": true, + "version": "0.2.0", + "type": "module", + "description": "WpywMail 客户端界面(shadcn/ui + Tailwind + Vite)", + "scripts": { + "dev": "vite", + "build": "tsc -b --noCheck && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-collapsible": "^1.1.20", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.16", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.548.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.16", + "@types/node": "^24.9.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.0.4", + "tailwindcss": "^4.1.16", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "vite": "^7.1.12", + "ws": "^8.21.3" + } +} diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 0000000..08ef0b1 --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..e0e7395 --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,342 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { CloudDownload, Loader2, Moon, Search, Settings2, Sun, UserRound } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { SearchResults } from '@/components/SearchResults'; +import { DownloadPanel } from '@/components/DownloadPanel'; +import { SettingsDialog } from '@/components/SettingsDialog'; +import { formatBytes } from '@/lib/format'; +import { + api, + extractBvid, + subscribe, + type AppConfig, + type Job, + type LoginStatus, + type PlayUrl, + type SearchItem, + type VideoDetail, +} from '@/lib/api'; + +const stamp = () => new Date().toLocaleTimeString('zh-CN', { hour12: false }); + +export default function App() { + const [config, setConfig] = useState(null); + const [login, setLogin] = useState(null); + const [settingsOpen, setSettingsOpen] = useState(false); + const [dark, setDark] = useState(() => localStorage.getItem('bd.theme') !== 'light'); + + const [query, setQuery] = useState(''); + const [items, setItems] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(false); + const [searching, setSearching] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [searched, setSearched] = useState(false); + const [searchError, setSearchError] = useState(null); + + const [video, setVideo] = useState(null); + const [pageIndex, setPageIndex] = useState(0); + const [play, setPlay] = useState(null); + const [loadingPlay, setLoadingPlay] = useState(false); + const [quality, setQuality] = useState(null); + + const [job, setJob] = useState(null); + const [logs, setLogs] = useState([]); + const [speedHistory, setSpeedHistory] = useState([]); + + const lastMilestone = useRef(0); + const jobRef = useRef(null); + jobRef.current = job; + + const log = useCallback((msg: string) => { + setLogs((prev) => [...prev.slice(-299), `[${stamp()}] ${msg}`]); + }, []); + + // ---------------- 主题 ---------------- + useEffect(() => { + document.documentElement.classList.toggle('dark', dark); + localStorage.setItem('bd.theme', dark ? 'dark' : 'light'); + }, [dark]); + + // ---------------- 启动:配置 + 登录状态 + 当前任务 ---------------- + useEffect(() => { + api.config().then(setConfig).catch((e) => log(`读取配置失败:${e.message}`)); + api + .login() + .then((s) => { + setLogin(s); + log(s.isLogin ? `已登录:${s.uname}` : '未登录(B 站只放出 480P 及以下)'); + }) + .catch(() => setLogin({ isLogin: false, uname: '' })); + api + .job() + .then((j) => { + if (j?.state) setJob(j); + }) + .catch(() => {}); + }, [log]); + + // ---------------- 服务端事件 ---------------- + useEffect(() => { + return subscribe({ + onJob: (j) => { + setJob(j); + if (j.state === 'done') { + log(`✔ 完成:${j.outputPath}(${formatBytes(j.size)},用时 ${Math.round(j.elapsed ?? 0)}s)`); + setSpeedHistory([]); + } else if (j.state === 'error') { + log(`✖ 失败:${j.error}`); + } else if (j.state === 'cancelled') { + log('已取消,未完成的文件已清理'); + setSpeedHistory([]); + } + }, + onStage: (s) => log(`▶ ${s.stage}`), + onProgress: (p) => { + const prog = jobRef.current?.progress; + const otherSpeed = p.which === 'video' ? (prog?.audio?.speed ?? 0) : (prog?.video?.speed ?? 0); + setSpeedHistory((prev) => [...prev.slice(-59), p.speed + otherSpeed]); + const mark = Math.floor(p.percent / 10) * 10; + if (mark > lastMilestone.current && mark < 100) { + lastMilestone.current = mark; + log(`${p.label} ${mark}%(${formatBytes(p.downloaded)}/${formatBytes(p.total)})`); + } + }, + }); + }, [log]); + + // ---------------- 清晰度 ---------------- + const loadPlay = useCallback( + async (bvid: string, cid: number) => { + setLoadingPlay(true); + setPlay(null); + setQuality(null); + try { + const p = await api.playUrl(bvid, cid); + setPlay(p); + if (p.qualities.length) { + setQuality(p.qualities[0].quality); + log( + `可用清晰度 ${p.qualities.length} 档:` + + p.qualities.map((q) => q.label).join(' / ') + + (p.maxQuality < 80 ? '(未登录,被限制在 480P 及以下)' : ''), + ); + } else { + log('该视频没有返回可用流'); + } + } catch (e) { + log(`解析清晰度失败:${(e as Error).message}`); + } finally { + setLoadingPlay(false); + } + }, + [log], + ); + + // ---------------- 搜索 / 解析 ---------------- + const runSearch = useCallback( + async (raw: string, nextPage = 1) => { + const text = raw.trim(); + if (!text) return; + + const bvid = extractBvid(text); + if (bvid) { + setSearchError(null); + setSearching(true); + try { + const detail = await api.video(bvid); + setVideo(detail); + setPageIndex(0); + log(`解析视频:${detail.title}`); + await loadPlay(bvid, detail.pages[0].cid); + } catch (e) { + setSearchError(`解析失败:${(e as Error).message}`); + } finally { + setSearching(false); + } + return; + } + + setSearching(true); + setSearchError(null); + try { + const res = await api.search(text, nextPage); + setItems((prev) => (nextPage === 1 ? res.items : [...prev, ...res.items])); + setPage(res.page); + setHasMore(res.hasMore); + setSearched(true); + if (nextPage === 1) log(`搜索「${text}」:${res.items.length} 条结果`); + } catch (e) { + setSearchError((e as Error).message); + } finally { + setSearching(false); + } + }, + [log, loadPlay], + ); + + const pickItem = useCallback( + async (item: SearchItem) => { + try { + const detail = await api.video(item.bvid); + setVideo(detail); + setPageIndex(0); + log(`选中:${detail.title}`); + await loadPlay(item.bvid, detail.pages[0].cid); + } catch (e) { + log(`加载视频失败:${(e as Error).message}`); + } + }, + [log, loadPlay], + ); + + const changePage = useCallback( + async (i: number) => { + if (!video) return; + setPageIndex(i); + await loadPlay(video.bvid, video.pages[i].cid); + }, + [video, loadPlay], + ); + + // ---------------- 下载 ---------------- + const startDownload = useCallback(async () => { + if (!video || quality == null) return; + lastMilestone.current = 0; + setSpeedHistory([]); + try { + const j = await api.download({ + bvid: video.bvid, + cid: video.pages[pageIndex].cid, + quality, + title: video.title, + }); + setJob(j); + log(`开始下载:${video.title}`); + } catch (e) { + const msg = (e as Error).message; + log(`无法开始下载:${msg}`); + setJob({ state: 'error', error: msg }); + } + }, [video, quality, pageIndex, log]); + + const cancel = useCallback(async () => { + await api.cancel().catch(() => {}); + }, []); + + const openFolder = useCallback( + (path?: string) => { + api.openFolder(path ?? config?.outputDir).catch(() => {}); + }, + [config], + ); + + return ( +
+ {/* ---------------- 顶栏 ---------------- */} +
+
+
+ +
+ BiliDownloader +
+ +
{ + e.preventDefault(); + void runSearch(query, 1); + }} + > + + setQuery(e.target.value)} + placeholder="搜索站内视频,或粘贴视频链接 / BV 号" + className="pl-8" + /> + {searching && ( + + )} + + +
+ + + {login?.isLogin ? login.uname : '未登录'} + + + +
+
+ + {/* ---------------- 主体 ---------------- */} +
+
+ { + setLoadingMore(true); + await runSearch(query, page + 1); + setLoadingMore(false); + }} + /> +
+ +
+ setSettingsOpen(true)} + /> +
+
+ + { + setConfig(c); + log('配置已保存'); + }} + onLoginChanged={(s) => { + setLogin(s); + log(s.isLogin ? `已登录:${s.uname}` : '未登录 / SESSDATA 无效'); + }} + /> +
+ ); +} diff --git a/ui/src/components/DownloadPanel.tsx b/ui/src/components/DownloadPanel.tsx new file mode 100644 index 0000000..9206a1e --- /dev/null +++ b/ui/src/components/DownloadPanel.tsx @@ -0,0 +1,359 @@ +import { useEffect, useRef, useState } from 'react'; +import { + AlertCircle, + CheckCircle2, + FolderOpen, + Gauge, + Loader2, + LockKeyhole, + Play, + Square, + Timer, + X, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { Separator } from '@/components/ui/separator'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { cn } from '@/lib/utils'; +import { formatBytes, formatCount, formatDuration, formatEta, formatSpeed, shortPath } from '@/lib/format'; +import type { Job, PlayUrl, Quality, StreamProgress, VideoDetail } from '@/lib/api'; + +type Props = { + video: VideoDetail | null; + play: PlayUrl | null; + loadingPlay: boolean; + quality: number | null; + onQualityChange: (q: number) => void; + pageIndex: number; + onPageChange: (i: number) => void; + job: Job | null; + logs: string[]; + speedHistory: number[]; + loggedIn: boolean; + outputDir: string; + onStart: () => void; + onCancel: () => void; + onOpenFolder: (path?: string) => void; + onOpenSettings: () => void; +}; + +function Sparkline({ data }: { data: number[] }) { + const W = 240; + const H = 34; + if (data.length < 2) { + return
速度曲线(下载中显示)
; + } + const max = Math.max(...data, 1); + const pts = data.map((v, i) => { + const x = (i / (data.length - 1)) * W; + const y = H - (v / max) * (H - 4) - 2; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }); + const area = `0,${H} ${pts.join(' ')} ${W},${H}`; + return ( + + + + + ); +} + +function StreamBar({ label, info }: { label: string; info: StreamProgress | null | undefined }) { + const percent = info?.percent ?? 0; + return ( +
+ {label} + + + {info + ? `${percent.toFixed(1)}% ${formatBytes(info.downloaded)}/${formatBytes(info.total)} ${formatSpeed(info.speed)}` + : '—'} + +
+ ); +} + +export function DownloadPanel({ + video, + play, + loadingPlay, + quality, + onQualityChange, + pageIndex, + onPageChange, + job, + logs, + speedHistory, + loggedIn, + outputDir, + onStart, + onCancel, + onOpenFolder, + onOpenSettings, +}: Props) { + const logRef = useRef(null); + const running = job?.state === 'running'; + + useEffect(() => { + if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; + }, [logs]); + + const videoP = job?.progress?.video; + const audioP = job?.progress?.audio; + const totalDown = (videoP?.downloaded ?? 0) + (audioP?.downloaded ?? 0); + const totalAll = (videoP?.total ?? 0) + (audioP?.total ?? 0); + const totalPercent = totalAll > 0 ? (totalDown * 100) / totalAll : 0; + const totalSpeed = (videoP?.speed ?? 0) + (audioP?.speed ?? 0); + const eta = formatEta({ downloaded: totalDown, total: totalAll, speed: totalSpeed }); + + const limitedByLogin = !!play && play.maxQuality > 0 && play.maxQuality < 80; + const currentQ: Quality | undefined = play?.qualities.find((q) => q.quality === quality); + + return ( +
+ {/* ---------------- 已选视频 ---------------- */} +
+ {!video ? ( +
+ 还没有选择视频 + 在左侧点选一个结果,播放器地址会自动解析 +
+ ) : ( +
+
+ {video.pic && ( + {video.title} + )} +
+
+
{video.title}
+
+ {video.owner} + · + {formatDuration(video.duration)} + {video.stat?.view != null && ( + <> + · + {formatCount(video.stat.view)}播放 + + )} + · + {video.bvid} +
+ {video.pages.length > 1 && ( +
+ 分P + +
+ )} +
+
+ )} +
+ + + + {/* ---------------- 参数 + 动作 ---------------- */} +
+
+ 清晰度 +
+ {loadingPlay ? ( +
+ 正在解析可用清晰度… +
+ ) : ( + + )} + {currentQ?.bandwidth ? ( + + {Math.round(currentQ.bandwidth / 1000)} kbps + + ) : null} +
+ + 保存到 +
+ + {shortPath(outputDir)} + + +
+
+ + {limitedByLogin && ( +
+ +
+ 当前未登录(或 SESSDATA 已失效),B 站只放出 {play?.qualities[0]?.label ?? '480P'} 及以下。 + 该视频标称支持到 {play?.advertisedQuality === 120 ? '4K' : `${play?.advertisedQuality}P`}。 + 想下高清请在 + + 里填一个有效的 SESSDATA。 +
+
+ )} + +
+ {running ? ( + + ) : ( + + )} + {job?.state === 'done' && ( + + )} +
+ + + {job?.threads ?? 16} 线程 + + {(videoP?.threads ?? 0) > 0 && ( + + + ETA {eta} + + )} +
+
+
+ + + + {/* ---------------- 进度 ---------------- */} +
+
+
+ {job?.stage ?? '等待开始'} + {totalPercent.toFixed(1)}% +
+ +
+ + {formatBytes(totalDown)} / {formatBytes(totalAll)} + + {formatSpeed(totalSpeed)} +
+
+ +
+ + +
+ + +
+ + {/* ---------------- 结果提示 ---------------- */} + {job?.state === 'done' && ( +
+ +
+
+ 下载完成 · {formatBytes(job.size)} · 用时 {formatDuration(job.elapsed)} +
+
+ {job.outputPath} +
+
+
+ )} + {job?.state === 'error' && ( +
+ +
{job.error}
+
+ )} + {job?.state === 'cancelled' && ( +
+ + 已取消,未完成的文件已清理 +
+ )} + + {/* ---------------- 日志 ---------------- */} +
+ 日志 +
+ {logs.length ? logs.join('\n') : (暂无)} +
+
+
+ ); +} diff --git a/ui/src/components/SearchResults.tsx b/ui/src/components/SearchResults.tsx new file mode 100644 index 0000000..5286537 --- /dev/null +++ b/ui/src/components/SearchResults.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; +import { Download, Loader2, Play, SearchX } from 'lucide-react'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { cn } from '@/lib/utils'; +import { formatCount } from '@/lib/format'; +import type { SearchItem } from '@/lib/api'; + +type Props = { + items: SearchItem[]; + loading: boolean; + loadingMore: boolean; + hasMore: boolean; + searched: boolean; + error: string | null; + selectedBvid: string | null; + onPick: (item: SearchItem) => void; + onLoadMore: () => void; +}; + +function Cover({ src, alt }: { src: string; alt: string }) { + const [failed, setFailed] = useState(false); + return ( +
+ {src && !failed ? ( + {alt} setFailed(true)} + className="size-full object-cover" + /> + ) : ( +
+ +
+ )} +
+ ); +} + +export function SearchResults({ + items, + loading, + loadingMore, + hasMore, + searched, + error, + selectedBvid, + onPick, + onLoadMore, +}: Props) { + if (loading) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ +
+ + + +
+
+ ))} +
+ ); + } + + if (error) { + return ( +
+ +
{error}
+
+ ); + } + + if (!items.length) { + return ( +
+ +
{searched ? '没有找到相关视频' : '搜索站内视频,或直接粘贴视频链接 / BV 号'}
+
+ ); + } + + return ( + +
+ {items.map((item) => { + const active = item.bvid === selectedBvid; + return ( + + + ); + })} + + {hasMore && ( +
+ +
+ )} +
+
+ ); +} diff --git a/ui/src/components/SettingsDialog.tsx b/ui/src/components/SettingsDialog.tsx new file mode 100644 index 0000000..0fa6f61 --- /dev/null +++ b/ui/src/components/SettingsDialog.tsx @@ -0,0 +1,202 @@ +import { useEffect, useState } from 'react'; +import { CheckCircle2, FolderOpen, Loader2, ShieldAlert, XCircle } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { api, type AppConfig, type LoginStatus } from '@/lib/api'; + +type Props = { + open: boolean; + onOpenChange: (v: boolean) => void; + config: AppConfig | null; + onSaved: (cfg: AppConfig) => void; + onLoginChanged: (s: LoginStatus) => void; +}; + +export function SettingsDialog({ open, onOpenChange, config, onSaved, onLoginChanged }: Props) { + const [draft, setDraft] = useState>({}); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [login, setLogin] = useState(null); + const [note, setNote] = useState(null); + + useEffect(() => { + if (open && config) { + setDraft({ + sessdata: config.sessdata, + outputDir: config.outputDir, + threads: config.threads, + preferAvc: config.preferAvc, + keepParts: config.keepParts, + }); + setNote(null); + setLogin(null); + } + }, [open, config]); + + const save = async () => { + setSaving(true); + setNote(null); + try { + const saved = await api.saveConfig(draft); + onSaved(saved); + setNote('已保存'); + } catch (e) { + setNote(`保存失败:${(e as Error).message}`); + } finally { + setSaving(false); + } + }; + + const testLogin = async () => { + setTesting(true); + try { + await api.saveConfig(draft); // 先落盘,再测 + const s = await api.login(); + setLogin(s); + onLoginChanged(s); + } catch (e) { + setLogin({ isLogin: false, uname: '', code: -1 }); + setNote(`测试失败:${(e as Error).message}`); + } finally { + setTesting(false); + } + }; + + return ( + + + + 设置 + + 保存后立刻写回 {config?.configPath} + + + +
+
+ + setDraft((d) => ({ ...d, sessdata: e.target.value }))} + placeholder="浏览器登录 B 站后从 Cookie 里取出 SESSDATA 的值" + className="font-mono text-xs" + /> +
+ + + 未登录时 B 站只放出 480P 及以下;填了有效 SESSDATA 才能下 1080P / 4K。 + 该值等同于账号登录凭据,保存在本机 config.json 里,别分享给他人。 + +
+
+ + {login && + (login.isLogin ? ( + + + 已登录:{login.uname} + + ) : ( + + + 未登录 / SESSDATA 无效(只能下 480P) + + ))} +
+
+ + + +
+ +
+ setDraft((d) => ({ ...d, outputDir: e.target.value }))} + className="font-mono text-xs" + /> + +
+
+ +
+ + setDraft((d) => ({ ...d, threads: Number(e.target.value) }))} + className="w-[120px] font-mono" + /> +
+ 建议 16;实测 16 线程约 4.1MB/s,32 线程约 6.8MB/s。过高可能触发 CDN 限流。 +
+
+ + + +
+ {( + [ + ['preferAvc', '优先 H.264(AVC)编码', '兼容性最好;关掉则可能选到更高码率的 HEVC/AV1'], + ['keepParts', '保留中间文件', '保留下载的 .video.m4s / .audio.m4s,便于排查问题'], + ] as const + ).map(([key, label, hint]) => ( + + ))} +
+ +
+ ffmpeg:{config?.ffmpeg || '未找到(合并会失败,请把 ffmpeg 加入 PATH)'} +
+
+ + + {note && {note}} + + + +
+
+ ); +} diff --git a/ui/src/components/ui/avatar.tsx b/ui/src/components/ui/avatar.tsx new file mode 100644 index 0000000..cf2e5b3 --- /dev/null +++ b/ui/src/components/ui/avatar.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import { cn } from '@/lib/utils'; + +function Avatar({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/ui/src/components/ui/badge.tsx b/ui/src/components/ui/badge.tsx new file mode 100644 index 0000000..378f44f --- /dev/null +++ b/ui/src/components/ui/badge.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground', + secondary: 'border-transparent bg-secondary text-secondary-foreground', + destructive: 'border-transparent bg-destructive text-white', + outline: 'text-foreground', + }, + }, + defaultVariants: { variant: 'default' }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span'; + return ; +} + +export { Badge, badgeVariants }; diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx new file mode 100644 index 0000000..1b28fab --- /dev/null +++ b/ui/src/components/ui/button.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + destructive: + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + }, + }, + defaultVariants: { variant: 'default', size: 'default' }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : 'button'; + return ; +} + +export { Button, buttonVariants }; diff --git a/ui/src/components/ui/card.tsx b/ui/src/components/ui/card.tsx new file mode 100644 index 0000000..7c99843 --- /dev/null +++ b/ui/src/components/ui/card.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }; diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx new file mode 100644 index 0000000..d3ac202 --- /dev/null +++ b/ui/src/components/ui/dialog.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +function Dialog(props: React.ComponentProps) { + return ; +} +function DialogTrigger(props: React.ComponentProps) { + return ; +} +function DialogPortal(props: React.ComponentProps) { + return ; +} +function DialogClose(props: React.ComponentProps) { + return ; +} + +function DialogOverlay({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { showCloseButton?: boolean }) { + return ( + + + + {children} + {showCloseButton && ( + + + 关闭 + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogTitle({ className, ...props }: React.ComponentProps) { + return ; +} + +function DialogDescription({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/ui/src/components/ui/dropdown-menu.tsx b/ui/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..4168fd6 --- /dev/null +++ b/ui/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,187 @@ +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +function DropdownMenu(props: React.ComponentProps) { + return ; +} +function DropdownMenuTrigger(props: React.ComponentProps) { + return ; +} +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { inset?: boolean; variant?: 'default' | 'destructive' }) { + return ( + + ); +} +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} +function DropdownMenuRadioGroup(props: React.ComponentProps) { + return ; +} +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { inset?: boolean }) { + return ( + + ); +} +function DropdownMenuSeparator({ className, ...props }: React.ComponentProps) { + return ( + + ); +} +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ); +} +function DropdownMenuSub(props: React.ComponentProps) { + return ; +} +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { inset?: boolean }) { + return ( + + {children} + + + ); +} +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/ui/src/components/ui/input.tsx b/ui/src/components/ui/input.tsx new file mode 100644 index 0000000..6062534 --- /dev/null +++ b/ui/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { + return ( + + ); +} + +export { Input }; diff --git a/ui/src/components/ui/label.tsx b/ui/src/components/ui/label.tsx new file mode 100644 index 0000000..ed4d4d3 --- /dev/null +++ b/ui/src/components/ui/label.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cn } from '@/lib/utils'; + +function Label({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/ui/src/components/ui/progress.tsx b/ui/src/components/ui/progress.tsx new file mode 100644 index 0000000..24ccf68 --- /dev/null +++ b/ui/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import * as ProgressPrimitive from '@radix-ui/react-progress'; +import { cn } from '@/lib/utils'; + +function Progress({ + className, + value, + indicatorClassName, + ...props +}: React.ComponentProps & { indicatorClassName?: string }) { + return ( + + + + ); +} + +export { Progress }; diff --git a/ui/src/components/ui/scroll-area.tsx b/ui/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..9b8e66f --- /dev/null +++ b/ui/src/components/ui/scroll-area.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; +import { cn } from '@/lib/utils'; + +function ScrollArea({ className, children, ...props }: React.ComponentProps) { + return ( + + + {children} + + + + + ); +} + +function ScrollBar({ + className, + orientation = 'vertical', + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { ScrollArea, ScrollBar }; diff --git a/ui/src/components/ui/select.tsx b/ui/src/components/ui/select.tsx new file mode 100644 index 0000000..6f49caa --- /dev/null +++ b/ui/src/components/ui/select.tsx @@ -0,0 +1,127 @@ +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +function Select(props: React.ComponentProps) { + return ; +} + +function SelectValue(props: React.ComponentProps) { + return ; +} + +function SelectTrigger({ + className, + size = 'default', + children, + ...props +}: React.ComponentProps & { size?: 'sm' | 'default' }) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = 'popper', + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectItem({ className, children, ...props }: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectScrollUpButton({ className, ...props }: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ className, ...props }: React.ComponentProps) { + return ( + + + + ); +} + +export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue }; diff --git a/ui/src/components/ui/separator.tsx b/ui/src/components/ui/separator.tsx new file mode 100644 index 0000000..7b55ac7 --- /dev/null +++ b/ui/src/components/ui/separator.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import { cn } from '@/lib/utils'; + +function Separator({ + className, + orientation = 'horizontal', + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/ui/src/components/ui/skeleton.tsx b/ui/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..9906dde --- /dev/null +++ b/ui/src/components/ui/skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from '@/lib/utils'; + +function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +export { Skeleton }; diff --git a/ui/src/components/ui/textarea.tsx b/ui/src/components/ui/textarea.tsx new file mode 100644 index 0000000..bdea2a3 --- /dev/null +++ b/ui/src/components/ui/textarea.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { + return ( +