From ca6b010f5912c5cbfbf1f69ace2a0abfdc81e978 Mon Sep 17 00:00:00 2001 From: "hangyu.tao" Date: Wed, 6 May 2026 19:47:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=A4=9A=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E4=BA=BA=E7=B3=BB=E7=BB=9F=E5=8F=8A=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 24 + DESIGN.md | 80 + backend/__pycache__/database.cpython-313.pyc | Bin 0 -> 2383 bytes .../__pycache__/feishu_client.cpython-313.pyc | Bin 0 -> 4775 bytes backend/__pycache__/main.cpython-313.pyc | Bin 0 -> 23256 bytes backend/__pycache__/models.cpython-313.pyc | Bin 0 -> 3291 bytes backend/add_column_migration.py | 40 + backend/add_column_migration_tasks.py | 40 + backend/database.py | 54 + backend/dump_mysql.sql | 87 + backend/dump_mysql_full.sql | 97 + backend/feishu_client.py | 112 + backend/head.md | 22 + backend/main.py | 664 ++++ backend/migrate_sqlite_to_mysql.py | 126 + backend/models.py | 67 + backend/quantum_test.db | Bin 0 -> 77824 bytes backend/schema_mysql.sql | 80 + backend/seed.py | 632 +++ eslint.config.js | 22 + index.html | 13 + package-lock.json | 3528 +++++++++++++++++ package.json | 35 + public/favicon.svg | 1 + public/icons.svg | 24 + src/App.css | 184 + src/App.tsx | 434 ++ src/assets/hero.png | Bin 0 -> 13057 bytes src/assets/react.svg | 1 + src/assets/vite.svg | 1 + src/components/auth/LoginPage.tsx | 313 ++ src/components/editor/ImportModal.tsx | 224 ++ src/components/editor/MindMapView.tsx | 618 +++ src/components/editor/PropertyPanel.tsx | 397 ++ src/components/editor/ReviewersInput.tsx | 211 + src/components/editor/TableView.tsx | 842 ++++ src/components/editor/UserMentionInput.tsx | 221 ++ src/components/layout/Sidebar.tsx | 565 +++ src/components/layout/Toast.tsx | 108 + src/components/plans/PlanListView.tsx | 840 ++++ src/components/plans/TaskExecutionView.tsx | 411 ++ src/components/shared/BugView.tsx | 245 ++ src/components/shared/DashboardView.tsx | 456 +++ src/index.css | 134 + src/main.tsx | 10 + src/store/useStore.ts | 670 ++++ src/user.json | 27 + tsconfig.app.json | 25 + tsconfig.json | 7 + tsconfig.node.json | 24 + vite.config.ts | 7 + 51 files changed, 12723 insertions(+) create mode 100644 .gitignore create mode 100644 DESIGN.md create mode 100644 backend/__pycache__/database.cpython-313.pyc create mode 100644 backend/__pycache__/feishu_client.cpython-313.pyc create mode 100644 backend/__pycache__/main.cpython-313.pyc create mode 100644 backend/__pycache__/models.cpython-313.pyc create mode 100644 backend/add_column_migration.py create mode 100644 backend/add_column_migration_tasks.py create mode 100644 backend/database.py create mode 100644 backend/dump_mysql.sql create mode 100644 backend/dump_mysql_full.sql create mode 100644 backend/feishu_client.py create mode 100644 backend/head.md create mode 100644 backend/main.py create mode 100644 backend/migrate_sqlite_to_mysql.py create mode 100644 backend/models.py create mode 100644 backend/quantum_test.db create mode 100644 backend/schema_mysql.sql create mode 100644 backend/seed.py create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 public/icons.svg create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/assets/hero.png create mode 100644 src/assets/react.svg create mode 100644 src/assets/vite.svg create mode 100644 src/components/auth/LoginPage.tsx create mode 100644 src/components/editor/ImportModal.tsx create mode 100644 src/components/editor/MindMapView.tsx create mode 100644 src/components/editor/PropertyPanel.tsx create mode 100644 src/components/editor/ReviewersInput.tsx create mode 100644 src/components/editor/TableView.tsx create mode 100644 src/components/editor/UserMentionInput.tsx create mode 100644 src/components/layout/Sidebar.tsx create mode 100644 src/components/layout/Toast.tsx create mode 100644 src/components/plans/PlanListView.tsx create mode 100644 src/components/plans/TaskExecutionView.tsx create mode 100644 src/components/shared/BugView.tsx create mode 100644 src/components/shared/DashboardView.tsx create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/store/useStore.ts create mode 100644 src/user.json create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..f363412 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,80 @@ +## 1. 项目背景与战略目标 +在大型项目(如字节跳动内部项目)中,测试用例的管理需要兼顾**灵活性**与**规范性**。本项目致力于实现: +- **测试管理与创建**:支持多场景(Web、移动端、API等)测试用例的创建与维护,确保全链路覆盖。 +- **场景规范化**:通过模板化和标准化流程,统一不同研发、测试人员对业务场景的定义,减少沟通误差。 +- **报告数据化**:全量采集执行结果,自动生成多维报表,驱动效率优化与质量分析。 + +## 2. 核心功能设计 + +### 2.1 脑图与表格双向联动 +- **思维导图视图**:用于梳理测试逻辑、功能模块和边界情况。支持快捷键(Enter 新增兄弟节点,Tab 新增子节点)。 +- **表格视图**:用于录入详细的“操作步骤”、“预期结果”和“优先级”。 +- **实时同步**:在脑图中修改节点名称,表格自动更新;在表格中增删行,脑图结构随之变化。 + +### 2.2 测试计划管理 (Test Plan) +- **多类型支持**:支持创建不同类型的测试计划,包括**需求测试**、**回归测试**、**开发自测**、**冒烟测试**等。 +- **独立上下文**:每个测试计划可以关联不同的用例集,记录独立的执行状态和评审进度。 +- **一键拉群关联**:飞书拉群功能将直接关联到特定的测试计划,群内实时推送该计划的执行进度。 + +### 2.3 用例详情与属性 (Property Panel) +- **步骤管理**:右侧面板支持录入详细的“操作步骤”与“预期结果”,支持动态增删。 +- **优先级标记**:支持 P0-P3 快速切换。 +- **评审记录**:支持记录节点的状态变化。 + +### 2.4 用例分级 (Priority Management) +- **等级定义**: + - **P0**:冒烟测试核心用例,阻塞性故障。 + - **P1**:主要功能路径。 + - **P2**:次要功能及边界。 + - **P3**:视觉、文案等细微问题。 +- **视觉呈现**:脑图节点支持标记 P0-P3 图标,表格支持按优先级过滤。 + +### 2.3 飞书(Feishu/Lark)一键拉群 +- **场景**:针对某个“测试计划”或“高优用例集”,点击按钮直接调起飞书 API 创建群聊。 +- **功能点**: + - 自动邀请相关测试人员与开发人员。 + - 群名自动生成(例如:`【测试专项】XX项目_20240428`)。 + - 群内自动推送测试报告概览卡片。 + +### 2.4 用例评审与执行 +- **评审流**:支持节点标记“已通过”、“待修改”。 +- **执行模式**:进入执行态后,点击节点可快速标记 Pass/Fail,并自动记录执行人。 + +## 3. 技术架构方案 + +### 3.1 前端技术栈 +- **框架**:React + TypeScript (提供强类型支持,适配复杂逻辑)。 +- **样式**:Vanilla CSS + CSS Variables (打造极致的响应式与动态主题,如深色模式)。 +- **脑图引擎**:基于 `React Flow` 或自研 SVG 引擎,实现平滑的缩放与拖拽。 +- **状态管理**:`Zustand` (轻量级,适配频繁同步的脑图数据)。 + +### 3.2 数据结构设计 +用例以**树状结构**存储,每个节点包含: +```json +{ + "id": "uuid", + "text": "登录功能", + "priority": "P0", + "children": [...], + "steps": [ + {"action": "输入账号", "expected": "账号回显正确"} + ], + "status": "pass" | "fail" | "blocked" +} +``` + +## 4. 界面交互设计 (Premium Aesthetics) +- **配色**:采用字节跳动风格的“极客蓝”配合深色/亮色毛玻璃效果。 +- **动画**:使用 `Framer Motion` 实现视图切换时的平滑过渡。 +- **布局**: + - 左侧:项目/目录树。 + - 顶部:工具栏(视图切换、飞书拉群、全局搜索)。 + - 中央:核心编辑器(脑图/表格)。 + - 右侧:属性面板(选中节点的详细信息、评审记录)。 + +## 5. 开发路线图 +1. **Phase 1**: 基础框架搭建,实现脑图与表格的渲染。 +2. **Phase 2**: 实现数据的本地/持久化存储及双向同步逻辑。 +3. **Phase 3**: 集成用例标级功能与右侧属性面板。 +4. **Phase 4**: 模拟飞书 API 集成与一键拉群交互。 +5. **Phase 5**: 整体视觉打磨与性能优化。 diff --git a/backend/__pycache__/database.cpython-313.pyc b/backend/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e67e17ef0b5247746f26406f56fff9d3772a9f1 GIT binary patch literal 2383 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 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/models.cpython-313.pyc b/backend/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a36176f9d3207b5669f7d488873ef58405e868b8 GIT binary patch literal 3291 zcmcImO>7%Q6yEjE+Fr*_oTS~fv?#O@mD4tXsA(y*fYP{WTo5UQZ6d_#&kAtc`0^=@q9R&AwP@|$_@ zo4?)feQ$OGQuj0G-+ zvSAt?7KVgFf*RZ}sG)1&$)G!zN=`HkQK>B&aiS53M%$uMCmMsO+!l>F(KtkV+M;qP zkxn*nlXQ?v=PG7xvCJjaqDHyILs|85E-muVl+H-OJ&9}mM|unV+rl-0imE^Zsz`%s zfWpBK3D+cij7ia*poY}&=>U!Rh~y&?H3~>njnP<1PRDszt?C8Rz%97Mu+JJ3;N7qE45!1+v`u34-S#7%kUKQ=c=*T_Km2c~sNN`XbRZE^FFirC2ku-=k?uHQj6> zy_z;}P-dA%nUpI4Cp68%C0v~1Yo{SRLBJ^T5JvePO=FgB83j$Z;Qh?iEW$L6reM@I z{DtO8FymJlp=`W(oL1&4FqVx!_+sPFA3k~T%~uaTyK(=!Z|{G)IXNq z8>=o;NEyK~_|)tOm>a?`g9qxDevPMYy|~uDI=V4p$H(hau0ODPbR%rX$5y84es5+6 zKN3Xow|xiR|5NeP&Rj^9P6sKZSc}z&&mFJ^+{JuE_7Jb7kb=8%A+GkothkRPywPgX zK_EeQ&FOWJL>HvbL6SbQ`wYF0!1^A6r5vmuau=U+pu-#N@b>ttw7)l;wRbn-JuOw# z6dfw{rw4d6PndNMRMvpa zW=NS(-2_QQiez4|nO0*Efxwjs9-gN9yv3tebNP&#&rCacn@jJb{V;+Kpuw?p51PGb zd<{?qTpq#WXs8B@T(`L7Dhs^jD&bMkWrZ5nGEY$Qp+P=!q*7zNk6b5(nq^eVUPNBh zjj{!QLV1#srJ6y>A}L$Av@};MK}#L&hnYpH3^ve9jK|z%L0ddl)oE*d)F}@*V;`Ov zrRWOcIqV(u48B6v1cRa)u3x&R9Q<YRUD){b9S7WN&Q6GGD z_!@BQLmV`Qw?G%=`y&%-oF+>>>4dA6yv`OHhv4unwOHV{tEEjIJaQ$Io1V?h(8Dmk zmHEAooB4DIH<7URLqbQYre5}QG^R6Vl*)uKE*B`#EmDNrp=&mt#;wudnVAO0%f^)Q zrL;!Z<;{?tnuKh0e6}9nAa?2mWS56;%-X4wkbR!Nl{yTWno`z6cPu+~tUi0M4Oz*$ z#N&?LiT@suXGOeEntL_KEqdLO?|F|Tdv`MVYbN1Sq1X9TaLVQtAfzz{57SBnB$#F-?!KqrQaRyGqLs0r3!Ws`<2K}~cx94#Qai;KU@^a!CUXFO|8BQ6*xHo(scZDtM6@0+R2xpBK!DWJNXhw z)AP5I!z(kc0CC+MeAdcr$<)ol`($ys?P_&yx@b{{k59N>DSo%PAA`>2h2nKm$T{8r z?l&Gi" for uid in plan.assignees]) + + card = { + "config": {"wide_screen_mode": True}, + "header": { + "title": {"tag": "plain_text", "content": "🚀 新测试计划已创建"}, + "template": "blue" + }, + "elements": [ + { + "tag": "div", + "text": {"tag": "lark_md", "content": f"**计划名称**: {plan.name}\n**计划类型**: {plan_type_cn}\n**包含用例**: {len(plan.caseIds or [])} 条"} + }, + { + "tag": "div", + "text": {"tag": "lark_md", "content": f"**执行人**: {mentions}"} + }, + { + "tag": "hr" + }, + { + "tag": "note", + "elements": [{"tag": "plain_text", "content": "请相关同学及时关注并开始执行。"}] + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": "查看计划详情" + }, + "type": "primary", + "url": f"http://localhost:5173/?view=execution&plan_id={new_id}" + } + ] + } + ] + + } + # Send to each assignee as a private message or we could create a group. + # User said "机器人提醒", usually private message is safer if no group context. + for open_id in plan.assignees: + print(f"Sending Feishu card to {open_id}...") + feishu.send_card(open_id, card) + print(f"Successfully sent card to {open_id}") + except Exception as e: + print(f"❌ Failed to send Feishu notification: {e}") + import traceback + traceback.print_exc() + + return {"message": "success", "id": new_id} + + + + + +@app.put("/api/plans/{plan_id}") +def update_plan(plan_id: str, plan: TestPlanSchema, db: Session = Depends(get_db)): + db_plan = db.query(models.TestPlan).filter(models.TestPlan.id == plan_id).first() + if not db_plan: + raise HTTPException(status_code=404, detail="Plan not found") + if plan.name: db_plan.name = plan.name + if plan.type: db_plan.type = plan.type + if plan.caseIds is not None: db_plan.case_ids = plan.caseIds + if plan.assignees is not None: db_plan.assignees = plan.assignees + db.commit() + + return {"message": "success"} + + +@app.post("/api/plans/{plan_id}/copy") +def copy_plan(plan_id: str, db: Session = Depends(get_db)): + source_plan = db.query(models.TestPlan).filter(models.TestPlan.id == plan_id).first() + if not source_plan: + raise HTTPException(status_code=404, detail="Plan not found") + + new_id = f"p-copy-{str(uuid.uuid4())[:8]}" + db_plan = models.TestPlan( + id=new_id, + name=f"{source_plan.name} (副本)", + type=source_plan.type, + case_ids=source_plan.case_ids, + assignees=source_plan.assignees, + created_at=datetime.datetime.now().isoformat(), + space_id=source_plan.space_id + ) + db.add(db_plan) + db.commit() + + # Create associated task for the copy + new_task_id = str(uuid.uuid4()) + db_task = models.TestTask( + id=new_task_id, + name=f"【执行任务】{db_plan.name}", + status="PENDING", + plan_id=new_id, + assignees=db_plan.assignees, + created_at=datetime.datetime.now().isoformat() + ) + db.add(db_task) + db.commit() + + return {"message": "success", "id": new_id} + + + +@app.delete("/api/plans/{plan_id}") +def delete_plan(plan_id: str, db: Session = Depends(get_db)): + db_plan = db.query(models.TestPlan).filter(models.TestPlan.id == plan_id).first() + if db_plan: + db.delete(db_plan) + db.commit() + return {"message": "success"} + + +# ───────────────────────────────────────────── +# Test Tasks +# ───────────────────────────────────────────── + +@app.get("/api/tasks") +def get_tasks(db: Session = Depends(get_db)): + tasks = db.query(models.TestTask).all() + return {"data": [ + { + "id": t.id, + "name": t.name, + "status": t.status, + "planId": t.plan_id, + "assignees": t.assignees or [], + "createdAt": t.created_at, + } for t in tasks + ]} + + + +@app.post("/api/tasks") +def create_task(task: TaskSchema, db: Session = Depends(get_db)): + new_id = task.id or str(uuid.uuid4()) + db_task = models.TestTask( + id=new_id, + name=task.name, + status=task.status, + plan_id=task.planId, + assignees=task.assignees or [], + created_at=datetime.datetime.now().isoformat(), + + ) + db.add(db_task) + db.commit() + return {"id": new_id} + + +@app.put("/api/tasks/{task_id}/status") +def update_task_status(task_id: str, body: TaskStatusUpdate, db: Session = Depends(get_db)): + db_task = db.query(models.TestTask).filter(models.TestTask.id == task_id).first() + if not db_task: + raise HTTPException(status_code=404, detail="Task not found") + db_task.status = body.status + db.commit() + return {"message": "success"} + + +# ───────────────────────────────────────────── +# Bugs +# ───────────────────────────────────────────── + +@app.get("/api/bugs") +def get_bugs(db: Session = Depends(get_db)): + bugs = db.query(models.Bug).all() + return {"data": [ + {"id": b.id, "title": b.title, "status": b.status, "caseId": b.case_id} + for b in bugs + ]} + + +@app.post("/api/bugs") +def create_bug(bug: BugSchema, db: Session = Depends(get_db)): + new_id = bug.id or f"BUG-{str(uuid.uuid4())[:8].upper()}" + db_bug = models.Bug(id=new_id, title=bug.title, status=bug.status or "OPEN", case_id=bug.caseId) + db.add(db_bug) + db.commit() + return {"id": new_id} + + +@app.put("/api/bugs/{bug_id}") +def update_bug(bug_id: str, bug: BugSchema, db: Session = Depends(get_db)): + db_bug = db.query(models.Bug).filter(models.Bug.id == bug_id).first() + if not db_bug: + raise HTTPException(status_code=404, detail="Bug not found") + if bug.status: db_bug.status = bug.status + if bug.status: db_bug.status = bug.status + db.commit() + return {"message": "success"} + +@app.delete("/api/bugs/{bug_id}") +def delete_bug(bug_id: str, db: Session = Depends(get_db)): + db_bug = db.query(models.Bug).filter(models.Bug.id == bug_id).first() + if db_bug: + db.delete(db_bug) + db.commit() + return {"message": "success"} + + +# ───────────────────────────────────────────── +# Spaces +# ───────────────────────────────────────────── + +@app.get("/api/spaces") +def get_spaces(db: Session = Depends(get_db)): + spaces = db.query(models.Space).all() + return {"data": [{"id": s.id, "name": s.name} for s in spaces]} + + +@app.post("/api/spaces") +def create_space(space: SpaceSchema, db: Session = Depends(get_db)): + new_id = space.id or f"space-{str(uuid.uuid4())[:8]}" + db_space = models.Space(id=new_id, name=space.name) + db.add(db_space) + db.commit() + return {"id": new_id, "message": "success"} + + +@app.delete("/api/spaces/{space_id}") +def delete_space(space_id: str, db: Session = Depends(get_db)): + db_space = db.query(models.Space).filter(models.Space.id == space_id).first() + if db_space: + # 1. 删除该空间下的所有用例 + db.query(models.TestCase).filter(models.TestCase.space_id == space_id).delete(synchronize_session=False) + + # 2. 删除该空间下的所有测试计划 + # 注意:如果有测试任务关联到这些计划,可能也需要处理。 + # 这里先简单删除计划,因为任务没有 space_id 只有 plan_id + plans = db.query(models.TestPlan).filter(models.TestPlan.space_id == space_id).all() + plan_ids = [p.id for p in plans] + if plan_ids: + db.query(models.TestTask).filter(models.TestTask.plan_id.in_(plan_ids)).delete(synchronize_session=False) + db.query(models.TestPlan).filter(models.TestPlan.id.in_(plan_ids)).delete(synchronize_session=False) + + # 3. 删除空间本身 + db.delete(db_space) + db.commit() + return {"message": "success"} + + +# ───────────────────────────────────────────── +# Feishu Integration & Batch Review +# ───────────────────────────────────────────── + +@app.post("/api/reviews/batch") +async def batch_review(data: BatchReviewSchema, db: Session = Depends(get_db)): + # 1. Fetch case details + cases = db.query(models.TestCase).filter(models.TestCase.id.in_(data.caseIds)).all() + if not cases: + raise HTTPException(status_code=404, detail="No cases found") + + # 2. Collect participants: maintainer + per-case reviewers + initiator + participants = set() + participants.add(data.reviewerOpenId) # The person who triggered the review + for c in cases: + if c.maintainer and c.maintainer.startswith("ou_"): + participants.add(c.maintainer) + if c.reviewers: + for r in c.reviewers: + if r and r.startswith("ou_"): + participants.add(r) + participants_list = list(participants) + print(f"DEBUG: Batch review participants: {participants_list}") + + + + # 3. Create Feishu Group + module_part = f"-{data.moduleName}" if data.moduleName else "" + group_name = f"用例评审{module_part}-{datetime.datetime.now().strftime('%m%d-%H%M')}" + + group_resp = feishu.create_group( + name=group_name, + description=f"针对 {len(cases)} 条用例的批量评审群", + user_ids=participants_list + ) + print(f"DEBUG: Feishu create_group response: {group_resp}") + + + + chat_id = group_resp.get("data", {}).get("chat_id") + if not chat_id: + # Fallback: if group creation fails (e.g. user not in app contact scope), + # try sending to the reviewer directly + chat_id = data.reviewerOpenId + + # 4. Prepare & Send Card + card_content = { + "config": {"wide_screen_mode": True}, + "header": { + "template": "blue", + "title": {"content": "📋 测试用例评审邀请", "tag": "plain_text"} + }, + "elements": [ + { + "tag": "div", + "text": {"content": f"**发起人:** \n**评审数量:** {len(cases)} 条\n**说明:** {data.message}", "tag": "lark_md"} + }, + {"tag": "hr"}, + { + "tag": "div", + "text": {"content": "**待评审列表:**", "tag": "lark_md"} + } + ] + } + + # Add first 5 cases to the card + for c in cases[:5]: + card_content["elements"].append({ + "tag": "div", + "text": {"content": f"• [{c.priority}] {c.text}", "tag": "plain_text"} + }) + + if len(cases) > 5: + card_content["elements"].append({ + "tag": "div", + "text": {"content": f"... 以及其他 {len(cases)-5} 条用例", "tag": "plain_text"} + }) + + card_content["elements"].append({ + "tag": "action", + "actions": [ + { + "tag": "button", + "text": {"content": "立即去评审", "tag": "plain_text"}, + "type": "primary", + "url": "http://120.48.157.2:55335/static/login.html" # Should be the real URL in prod + } + ] + }) + + send_resp = feishu.send_card(chat_id, card_content) + + return { + "message": "success", + "chat_id": chat_id, + "feishu_response": send_resp + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) + diff --git a/backend/migrate_sqlite_to_mysql.py b/backend/migrate_sqlite_to_mysql.py new file mode 100644 index 0000000..d473e3c --- /dev/null +++ b/backend/migrate_sqlite_to_mysql.py @@ -0,0 +1,126 @@ +""" +migrate_sqlite_to_mysql.py +───────────────────────────────────────────────────────────────────────────── +将本地 SQLite (quantum_test.db) 的全量数据迁移至百度云 MySQL (case_platform)。 + +用法: + cd backend + source venv/bin/activate + python migrate_sqlite_to_mysql.py + +迁移顺序(严格按依赖关系,避免外键冲突): + test_cases → test_plans → test_tasks → bugs +""" + +import json +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +# ── 源:SQLite ───────────────────────────────────────────────────────────── +SQLITE_URL = "sqlite:///./quantum_test.db" + +# ── 目标:MySQL ──────────────────────────────────────────────────────────── +MYSQL_URL = ( + "mysql+pymysql://root_dev:8B1EBC1509cc602b" + "@mysql1.rdsmbk3ednsgnnt.rds.bj.baidubce.com:3306" + "/case_platform?charset=utf8mb4" +) + + +def make_session(url, **engine_kwargs): + engine = create_engine(url, **engine_kwargs) + Session = sessionmaker(bind=engine) + return engine, Session() + + +def migrate(): + print("=" * 60) + print("📦 SQLite → MySQL 数据迁移") + print("=" * 60) + + # ── 连接两端 ──────────────────────────────────────────────────────────── + sqlite_engine, src = make_session( + SQLITE_URL, connect_args={"check_same_thread": False} + ) + mysql_engine, dst = make_session( + MYSQL_URL, + pool_pre_ping=True, + pool_recycle=1800, + ) + + # ── 在 MySQL 中建表(如果不存在) ──────────────────────────────────────── + print("\n[1/5] 在 MySQL 中初始化表结构...") + import models + models.Base.metadata.create_all(bind=mysql_engine) + print(" ✅ 表结构就绪") + + # ── 通用:清空目标表后写入 ─────────────────────────────────────────────── + def migrate_table(table_name: str, rows): + if not rows: + print(f" ⚠️ {table_name}: 源表为空,跳过") + return + # 清空(先删子表,再删父表,顺序由调用方保证) + dst.execute(text(f"DELETE FROM {table_name}")) + dst.commit() + + inserted = 0 + for row in rows: + data = dict(row._mapping) + # JSON 字段在 SQLite 里是字符串,需要反序列化 + for col in ("steps", "tags", "case_ids"): + if col in data and isinstance(data[col], str): + try: + data[col] = json.loads(data[col]) + except (json.JSONDecodeError, TypeError): + data[col] = None + cols = ", ".join(data.keys()) + placeholders = ", ".join(f":{k}" for k in data.keys()) + dst.execute( + text(f"INSERT INTO {table_name} ({cols}) VALUES ({placeholders})"), + data, + ) + inserted += 1 + dst.commit() + print(f" ✅ {table_name}: 迁移 {inserted} 条") + + # ── 读取 SQLite 数据 ───────────────────────────────────────────────────── + print("\n[2/5] 读取 SQLite 数据...") + cases = src.execute(text("SELECT * FROM test_cases")).fetchall() + plans = src.execute(text("SELECT * FROM test_plans")).fetchall() if _table_exists(src, "test_plans") else [] + tasks = src.execute(text("SELECT * FROM test_tasks")).fetchall() if _table_exists(src, "test_tasks") else [] + bugs = src.execute(text("SELECT * FROM bugs")).fetchall() if _table_exists(src, "bugs") else [] + + print(f" test_cases : {len(cases)} 条") + print(f" test_plans : {len(plans)} 条") + print(f" test_tasks : {len(tasks)} 条") + print(f" bugs : {len(bugs)} 条") + + # ── 迁移(父表先于子表) ───────────────────────────────────────────────── + print("\n[3/5] 迁移 test_cases ...") + migrate_table("test_cases", cases) + + print("\n[4/5] 迁移 test_plans / test_tasks / bugs ...") + migrate_table("test_plans", plans) + migrate_table("test_tasks", tasks) + migrate_table("bugs", bugs) + + print("\n[5/5] 验证 MySQL 行数...") + for tbl in ("test_cases", "test_plans", "test_tasks", "bugs"): + count = dst.execute(text(f"SELECT COUNT(*) FROM {tbl}")).scalar() + print(f" {tbl:<14}: {count} 条") + + src.close() + dst.close() + print("\n🎉 迁移完成!") + + +def _table_exists(session, table_name: str) -> bool: + """检查 SQLite 中该表是否存在""" + result = session.execute( + text(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'") + ).fetchone() + return result is not None + + +if __name__ == "__main__": + migrate() diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..9e412a8 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,67 @@ +from sqlalchemy import Column, String, JSON +from database import Base + + +class Space(Base): + __tablename__ = "spaces" + id = Column(String(50), primary_key=True) + name = Column(String(200), nullable=False) + + +class TestCase(Base): + __tablename__ = "test_cases" + + id = Column(String(50), primary_key=True) + case_id = Column(String(50), unique=True, nullable=True) + text = Column(String(200), nullable=False) + module = Column(String(100)) + type = Column(String(50), default="General") + priority = Column(String(20), default="P2") + review_status = Column(String(50), default="Draft") + execution_status = Column(String(50), default="UNTESTED") + maintainer = Column(String(100)) + requirement_id = Column(String(100)) + bug_id = Column(String(100)) + steps = Column(JSON, nullable=True) # [{action, expected}] + tags = Column(JSON, nullable=True) # string[] + reviewers = Column(JSON, nullable=True) # string[] (Feishu open_ids) + parent_id = Column(String(50), nullable=True) + space_id = Column(String(50)) + + + + +class TestTask(Base): + __tablename__ = "test_tasks" + + id = Column(String(50), primary_key=True) + name = Column(String(200), nullable=False) + status = Column(String(50), default="PENDING") # PENDING / RUNNING / COMPLETED + plan_id = Column(String(50)) + assignees = Column(JSON, nullable=True) # string[] + + created_at = Column(String(50)) + + +class TestPlan(Base): + __tablename__ = "test_plans" + + id = Column(String(50), primary_key=True) + name = Column(String(200), nullable=False) + type = Column(String(50)) # Self-test / Regression / Requirement / Smoke + case_ids = Column(JSON, nullable=True) # string[] + assignees = Column(JSON, nullable=True) # string[] (Feishu open_ids) + created_at = Column(String(50)) + + space_id = Column(String(50)) + + + +class Bug(Base): + __tablename__ = "bugs" + + id = Column(String(50), primary_key=True) + title = Column(String(200), nullable=False) + status = Column(String(50), default="OPEN") # OPEN / RESOLVED / CLOSED + case_id = Column(String(50)) + diff --git a/backend/quantum_test.db b/backend/quantum_test.db new file mode 100644 index 0000000000000000000000000000000000000000..ebe94ab9caba9f48c8e8d6fe4b06bba992a01df7 GIT binary patch literal 77824 zcmeI)&2Jk;7{KA(Cb1nSj`N{tl>^dRs3H~=H4UYTA_1lt2pFevlZYOWwT*{pA$DBv znx^6uDskhE#0iND66ziG#+4I)Ld5}k;oKuLvtF-1Hm-mZtKTY)cW3r%p7))JCw6js zuj2Z`{`9odbbNc$STs!2c+0j8!&s1y75NC4oLrd>H{`Q9uzu0%g7NA%&vN-^#&X&< z^40vW`BZLY;kWt6^Od<%6>MxoxPsw%N zdEmAh;>dyI+FD&Tzbg99 z-4mh8)nlo5?0)I(O1ZapcGhj!ho#!h_e-_)>sO0*b$8#c9#kr?*x6a%^-n~<_S)5v zYR`B4uGg=ZN2`O598bub9WB09+xe(eyJLS?zN7CkTYPCLWo@sThHj;I??k?~s5@Q% zRBs1u-cYx_?B>(fR>3@<2|aw_`StdR)AH(G+c{E~i;5OUg`%qd-byPfw%5b66b~0t z)|GT9v>IQm0y)hpfhH$@r7<56X*shETcj_f6r-bhbq(m=n~`|5;xxIL{C z>g!UJP`Wm{wVCmR>K#8Zq0~Mx$>EE$s56d@$j#G6cVNU0`*GYT(C)aW9oK)*@4?rC zKHm{vxZ-|2K4Zn1IO_WDY3qXersKAJ`4FA{X~TQyhl{TmIHlUvFD3+ zG*J1D`~eq^T5YE@a<1O^_7BFUndf#A9je~gq}NmLPNl5NYvw0m;_Az(aVc1t9!;Ts zy~@pCL#NJ{X4+a?Gtc*Q<^&bN_e(<=6CN@#TPFV$7#|*9zLc`wT)Svg4lY^W2%e|p zORJ1}d8H=xX8wsGzgQ4J009ILKmY**5I_I{1Q0;r|0?i;IaR!@C;eBpZoDR57h?12 z<8S|b^uyEdfBxgUM_Jh}Gyj(%zgQ4J009ILKmY**5I_I{1Q0+V5duq7#o%LJ{oe&J z|4&3;00Iag TfB*srAb + + + + + + - + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9935fc1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3528 @@ +{ + "name": "-", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "-", + "version": "0.0.0", + "dependencies": { + "framer-motion": "^12.38.0", + "html-to-image": "^1.11.13", + "lucide-react": "^1.11.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "reactflow": "^11.11.4", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/background/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", + "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/type-utils": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", + "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", + "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.1", + "@typescript-eslint/types": "^8.59.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", + "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", + "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", + "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", + "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", + "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.1", + "@typescript-eslint/tsconfig-utils": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", + "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", + "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "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/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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, + "libc": [ + "glibc" + ], + "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, + "libc": [ + "musl" + ], + "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, + "libc": [ + "glibc" + ], + "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, + "libc": [ + "musl" + ], + "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/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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": "1.11.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", + "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz", + "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.1", + "@typescript-eslint/parser": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bc75709 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "-", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "framer-motion": "^12.38.0", + "html-to-image": "^1.11.13", + "lucide-react": "^1.11.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "reactflow": "^11.11.4", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..2e4207e --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,434 @@ +import React, { useEffect } from 'react'; +import { useStore } from './store/useStore'; + +// Auth +import LoginPage from './components/auth/LoginPage'; + +// Layout +import Sidebar from './components/layout/Sidebar'; +import { ToastContainer, useToastStore } from './components/layout/Toast'; + +// Editor +import TableView from './components/editor/TableView'; +import PropertyPanel from './components/editor/PropertyPanel'; +import { ImportModal } from './components/editor/ImportModal'; + +// Plans +import PlanListView from './components/plans/PlanListView'; +import TaskExecutionView from './components/plans/TaskExecutionView'; + +// Shared +import DashboardView from './components/shared/DashboardView'; +import BugView from './components/shared/BugView'; + +import { Search, UploadCloud, LogOut } from 'lucide-react'; + + const App: React.FC = () => { + const { + spaces, currentSpaceId, setCurrentSpaceId, viewMode, setViewMode, + fetchSpaces, fetchData, fetchTasks, fetchBugs, testTasks, setSelectedTaskId, + showImportModal, setShowImportModal, currentUser, setCurrentUser, logout + } = useStore(); + + const { addToast } = useToastStore(); + + useEffect(() => { + // fetchSpaces will internally call fetchData after resolving currentSpaceId + fetchSpaces(); + fetchTasks(); + fetchBugs(); + }, []); + + // Re-fetch data when user manually switches spaces + useEffect(() => { + if (currentSpaceId) { + fetchData(); + } + }, [currentSpaceId]); + + // Ref to track handled deep links + const handledRef = React.useRef(null); + + // Handle deep linking via URL parameters + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const planId = params.get('plan_id'); + const view = params.get('view'); + + if (view === 'execution' && planId) { + if (handledRef.current === planId) return; + + const { testTasks, setSelectedTaskId } = useStore.getState(); + const task = testTasks.find(t => t.planId === planId); + if (task) { + setSelectedTaskId(task.id); + setViewMode('execution'); + handledRef.current = planId; + + // Clear URL parameters after successful jump + const newUrl = window.location.pathname; + window.history.replaceState({}, '', newUrl); + } else { + // If tasks are still loading, don't clear yet, wait for next testTasks update + setViewMode('execution'); + } + } else if (view && !handledRef.current) { + setViewMode(view as any); + handledRef.current = 'mode-only'; + const newUrl = window.location.pathname; + window.history.replaceState({}, '', newUrl); + } + }, [testTasks]); + + + + + + + if (!currentUser) { + return setCurrentUser(user)} />; + } + + return ( +
+ + {showImportModal && setShowImportModal(false)} />} +
+ {/* Main Navigation Sidebar */} + + +
+ {/* Header is now inside main content wrapper so sidebar can be full height */} +
+
+
+
D
+ D-Case +
+
+ + +
+
+
{currentUser.name.slice(-1)}
+ {currentUser.name} + +
+ +
+
+ +
+ {/* Dynamic Content Area based on Sidebar selection */} +
+ {viewMode === 'table' && } + {viewMode === 'dashboard' && } + {viewMode === 'bugs' && } + {viewMode === 'execution' && } + {viewMode === 'plans' && } +
+ + + {(viewMode === 'table' || viewMode === 'execution') && } + + +
+
+
+ + + + + + + + + +
+ ); +}; + +export default App; diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx new file mode 100644 index 0000000..1af4782 --- /dev/null +++ b/src/components/auth/LoginPage.tsx @@ -0,0 +1,313 @@ +import React, { useState } from 'react'; +import usersData from '../../user.json'; +import { Eye, EyeOff, ShieldCheck, LogIn } from 'lucide-react'; + +const users = usersData as Record; +const DEFAULT_PASSWORD = 'admin123'; + +export interface LoginUser { + name: string; + openId: string; +} + +interface LoginPageProps { + onLogin: (user: LoginUser) => void; +} + +const LoginPage: React.FC = ({ onLogin }) => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + if (!username.trim()) { + setError('请输入用户名'); + return; + } + if (!users[username]) { + setError('用户名不存在'); + return; + } + if (password !== DEFAULT_PASSWORD) { + setError('密码错误'); + return; + } + setLoading(true); + setTimeout(() => { + onLogin({ name: username, openId: users[username] }); + setLoading(false); + }, 600); + }; + + return ( +
+
+
+
+
+
+ +
+
+
Q
+

QuantumTest

+

测试用例管理平台

+
+ +
+
+ + { setUsername(e.target.value); setError(''); }} + autoFocus + /> +
+ +
+ +
+ { setPassword(e.target.value); setError(''); }} + /> + +
+
+ + {error && ( +
+ + {error} +
+ )} + + +
+ +

默认密码: admin123

+
+ + +
+ ); +}; + +export default LoginPage; diff --git a/src/components/editor/ImportModal.tsx b/src/components/editor/ImportModal.tsx new file mode 100644 index 0000000..4a5ebec --- /dev/null +++ b/src/components/editor/ImportModal.tsx @@ -0,0 +1,224 @@ +import React, { useState, useRef } from 'react'; +import { UploadCloud, X, Image as ImageIcon, Loader2, Sparkles, FileText } from 'lucide-react'; +import { useStore } from '../../store/useStore'; +import { useToastStore } from '../layout/Toast'; + +const parsedData = { + id: `node-ai-${Date.now()}`, + text: '推理服务', + children: [ + { + id: `node-ai-${Date.now()}-1`, + text: '创建推理服务', + children: [ + { + id: `node-ai-${Date.now()}-1-1`, + text: '点击创建推理服务按钮', + steps: [{ action: '点击创建推理服务按钮', expected: '进入创建推理服务页面' }] + }, + { + id: `node-ai-${Date.now()}-1-2`, + text: '查看页面显示', + children: [ + { id: `node-ai-${Date.now()}-1-2-1`, text: '预期: 左上角显示返回按钮,显示创建推理服务标题' }, + { id: `node-ai-${Date.now()}-1-2-2`, text: '预期: 下方显示基本信息、推理类型、规格信息等' }, + { id: `node-ai-${Date.now()}-1-2-3`, text: '预期: 右下角显示取消、确认按钮' } + ] + }, + { + id: `node-ai-${Date.now()}-1-3`, + text: '基本信息', + children: [ + { + id: `node-ai-${Date.now()}-1-3-1`, + text: '名称', + children: [ + { id: `node-ai-${Date.now()}-1-3-1-1`, text: '备注: 为必填项,不允许重复,不允许为空,支持1-64字符' }, + { id: `node-ai-${Date.now()}-1-3-1-2`, text: '不输入名称', steps: [{ action: '不输入名称,其他配置合法点击确认', expected: '提示名称不能为空' }] }, + { id: `node-ai-${Date.now()}-1-3-1-3`, text: '名称重复', steps: [{ action: '输入重复名称', expected: '提示名称已存在' }] }, + { id: `node-ai-${Date.now()}-1-3-1-4`, text: '名称合法', steps: [{ action: '输入合法名称', expected: '推理服务创建成功' }] }, + { id: `node-ai-${Date.now()}-1-3-1-5`, text: '输入65位字符', steps: [{ action: '输入65位字符', expected: '第65位字符不允许输入' }] } + ] + }, + { + id: `node-ai-${Date.now()}-1-3-2`, + text: '描述', + children: [ + { id: `node-ai-${Date.now()}-1-3-2-1`, text: '备注: 非必填项,200字符以内' }, + { id: `node-ai-${Date.now()}-1-3-2-2`, text: '输入为空', steps: [{ action: '输入为空', expected: '创建成功,描述为空' }] }, + { id: `node-ai-${Date.now()}-1-3-2-3`, text: '输入201字符', steps: [{ action: '输入201字符', expected: '第201位字符不允许输入' }] } + ] + }, + { + id: `node-ai-${Date.now()}-1-3-3`, + text: '标签', + children: [ + { id: `node-ai-${Date.now()}-1-3-3-1`, text: '备注: 最多支持创建20个标签' }, + { id: `node-ai-${Date.now()}-1-3-3-2`, text: '不输入标签键,输入标签值', steps: [{ action: '点击确认', expected: '给出提示,标签键不能为空' }] } + ] + } + ] + }, + { + id: `node-ai-${Date.now()}-1-4`, + text: '推理类型', + children: [ + { id: `node-ai-${Date.now()}-1-4-1`, text: '查看类型显示', steps: [{ action: '查看显示', expected: '显示推理服务、分布式推理服务' }] }, + { id: `node-ai-${Date.now()}-1-4-2`, text: '多次切换选项', steps: [{ action: '多次切换', expected: '仅支持单选,下方规格随之切换' }] } + ] + }, + { id: `node-ai-${Date.now()}-1-5`, text: '规格信息' }, + { id: `node-ai-${Date.now()}-1-6`, text: '基础配置' }, + { id: `node-ai-${Date.now()}-1-7`, text: '存储配置' } + ] + }, + { id: `node-ai-${Date.now()}-2`, text: '搜索推理服务' }, + { id: `node-ai-${Date.now()}-3`, text: '推理服务列表' } + ] +}; + +interface ImportModalProps { + onClose: () => void; +} + +export const ImportModal: React.FC = ({ onClose }) => { + const [isDragging, setIsDragging] = useState(false); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [progress, setProgress] = useState(0); + const fileInputRef = useRef(null); + + const { currentSpaceId, spaceData, importNodes } = useStore(); + const { addToast } = useToastStore(); + + const handleSimulateAI = () => { + setIsAnalyzing(true); + let current = 0; + const interval = setInterval(() => { + current += 15; + if (current >= 100) { + clearInterval(interval); + setProgress(100); + setTimeout(async () => { + // Add parsed data to the tree and persist to backend + await importNodes([parsedData]); + addToast('AI 识别完毕!已成功导入推理服务用例脑图', 'success'); + onClose(); + }, 500); + } else { + setProgress(current); + } + }, 400); + }; + + + const onFileDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleSimulateAI(); + } + }; + + return ( +
+
e.stopPropagation()}> +
+
+ +

AI 智能导入

+
+ +
+ +
+ {!isAnalyzing ? ( +
{ e.preventDefault(); setIsDragging(true); }} + onDragLeave={() => setIsDragging(false)} + onDrop={onFileDrop} + onClick={() => fileInputRef.current?.click()} + > +
+ +
+

点击或将图片拖拽到这里

+

+ 支持解析 XMind 截图、手绘脑图、Excel 截图 +

+ { + if (e.target.files && e.target.files.length > 0) { + handleSimulateAI(); + } + }} + /> +
+ ) : ( +
+
+ +
+

AI 正在解析图片逻辑树...

+

+ 已识别到「推理服务」等 28 个节点 +

+
+
+
+
+ )} +
+
+ +
+ ); +}; diff --git a/src/components/editor/MindMapView.tsx b/src/components/editor/MindMapView.tsx new file mode 100644 index 0000000..f829abc --- /dev/null +++ b/src/components/editor/MindMapView.tsx @@ -0,0 +1,618 @@ +import React, { useMemo, useEffect, useRef } from 'react'; +import ReactFlow, { + ReactFlowProvider, + useReactFlow, + Background, + Controls, + Panel, + Handle, + Position, +} from 'reactflow'; +import type { Node, Edge, NodeProps } from 'reactflow'; +import { toPng } from 'html-to-image'; + +import 'reactflow/dist/style.css'; +import { useStore } from '../../store/useStore'; +import type { TestCaseNode, Priority } from '../../store/useStore'; + +import { Plus, CornerDownRight, ChevronRight, ChevronDown, Bug, Link, Trash2 } from 'lucide-react'; +import { useToastStore } from '../layout/Toast'; + +// Custom Node Component +const MindMapNode = ({ data }: NodeProps) => { + const node = data.node as TestCaseNode; + const { updateNode, addNode, addSiblingNode, deleteNode, selectedNodeId, setSelectedNodeId, editingNodeId, setEditingNodeId } = useStore(); + const isSelected = selectedNodeId === node.id; + const isEditing = editingNodeId === node.id; + const isSet = !node.caseId; + + // Local state for editing — avoids store re-renders breaking the input + const [editingText, setEditingText] = React.useState(node.text); + React.useEffect(() => { + if (isEditing) setEditingText(node.text); + }, [isEditing, node.id]); + + const commitEdit = () => { + updateNode(node.id, { text: editingText }); + setEditingNodeId(null); + }; + + const priorityColors: Record = { + P0: '#F53F3F', + P1: '#FF7D00', + P2: '#F7BA1E', + P3: '#165DFF', + }; + + const statusStyles: Record = { + PASS: { border: '#00B42A', bg: '#E8FFFB', color: '#00B42A' }, + FAIL: { border: '#F53F3F', bg: '#FFECE8', color: '#F53F3F' }, + BLOCK: { border: '#FF7D00', bg: '#FFF7E8', color: '#FF7D00' }, + UNTESTED: { border: '#E5E6EB', bg: 'white', color: '#86909C' } + }; + + const currentStatus = node.executionStatus || 'UNTESTED'; + const statusStyle = statusStyles[currentStatus]; + + return ( +
{ + e.stopPropagation(); + setSelectedNodeId(node.id); + }} + onDoubleClick={(e) => { + e.stopPropagation(); + setEditingNodeId(node.id); + }} + > + + +
+
+ {node.priority && ( + + {node.priority} + + )} + {isEditing ? ( + setEditingText(e.target.value)} + onBlur={commitEdit} + onKeyDown={(e) => { + if (e.key === 'Enter') { e.preventDefault(); commitEdit(); } + if (e.key === 'Escape') { setEditingText(node.text); setEditingNodeId(null); } + }} + placeholder="" + /> + ) : ( + {node.text || '未命名'} + + )} +
+ + {(node.requirementId || node.bugId || (node.tags && node.tags.length > 0)) && ( +
+ {node.requirementId && ( + + {node.requirementId} + + )} + {node.bugId && ( + + {node.bugId} + + )} + {node.tags && node.tags.map((tag, idx) => ( + + {tag} + + ))} +
+ )} + +
+ + + +
+
+ + + + {node.children && node.children.length > 0 && ( + + )} + + + +
+ ); +}; + +const nodeTypes = { + mindMap: MindMapNode, +}; + + interface MindMapProps { + selectedModuleId?: string | null; + onClearModuleSelection?: () => void; + executionMode?: boolean; + } + + const MindMapInner: React.FC = ({ selectedModuleId, onClearModuleSelection, executionMode }) => { + + + const { spaceData, currentSpaceId, selectedNodeId, editingNodeId, addNode, addSiblingNode, deleteNode, setEditingNodeId, selectedPlanId, testPlans } = useStore(); + const testCases = spaceData[currentSpaceId] || []; + const { addToast } = useToastStore(); + const { fitView } = useReactFlow(); + + const handleExportImage = () => { + const element = document.querySelector('.react-flow__viewport') as HTMLElement; + if (!element) return; + + addToast('正在生成图片...', 'info'); + toPng(document.querySelector('.react-flow') as HTMLElement, { + backgroundColor: '#f8fafc', + filter: (node) => { + // Exclude controls and panels from the export + if (node?.classList?.contains('react-flow__controls') || node?.classList?.contains('react-flow__panel')) { + return false; + } + return true; + } + }).then((dataUrl) => { + const link = document.createElement('a'); + link.download = `mindmap-${currentSpaceId}-${Date.now()}.png`; + link.href = dataUrl; + link.click(); + addToast('导出成功', 'success'); + }).catch((err) => { + console.error('Export failed', err); + addToast('导出失败', 'error'); + }); + }; + + const handleAutoLayout = () => { + fitView({ duration: 800, padding: 0.2 }); + addToast('布局已整理', 'success'); + }; + + const activePlanCaseIds = useMemo(() => { + if (!selectedPlanId) return null; + const plan = testPlans.find(p => p.id === selectedPlanId); + return plan ? new Set(plan.caseIds) : null; + }, [selectedPlanId, testPlans]); + + // Keyboard shortcuts handler + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const state = useStore.getState(); + const { selectedNodeId, editingNodeId, addNode, addSiblingNode, deleteNode, setEditingNodeId } = state; + if (editingNodeId) return; + if (!selectedNodeId) return; + const target = e.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { + return; + } + + switch (e.key) { + case 'Tab': + e.preventDefault(); + e.stopPropagation(); + addNode(selectedNodeId); + break; + case 'Enter': + e.preventDefault(); + e.stopPropagation(); + addSiblingNode(selectedNodeId); + break; + case 'Backspace': + case 'Delete': + e.preventDefault(); + e.stopPropagation(); + deleteNode(selectedNodeId); + break; + case ' ': + case 'F2': + e.preventDefault(); + e.stopPropagation(); + setEditingNodeId(selectedNodeId); + break; + case 'Escape': + e.preventDefault(); + e.stopPropagation(); + setEditingNodeId(null); + break; + default: + break; + } + }; + window.addEventListener('keydown', handleKeyDown, true); + return () => window.removeEventListener('keydown', handleKeyDown, true); + }, []); + + const { nodes, edges } = useMemo(() => { + const initialNodes: Node[] = []; + const initialEdges: Edge[] = []; + const verticalSpacing = 90; + const horizontalSpacing = 280; + + const getSubtreeHeight = (node: TestCaseNode): number => { + if (!node.children || node.children.length === 0 || node.isExpanded === false) { + return verticalSpacing; + } + return node.children.reduce((acc, child) => acc + getSubtreeHeight(child), 0); + }; + + const filterTree = (nodes: TestCaseNode[]): TestCaseNode[] => { + if (!activePlanCaseIds) return nodes; + return nodes + .map(node => { + const children = node.children ? filterTree(node.children) : []; + const isInPlan = activePlanCaseIds.has(node.id); + if (isInPlan || children.length > 0) { + return { ...node, children }; + } + return null; + }) + .filter((n): n is TestCaseNode => n !== null); + }; + + const tree = filterTree(testCases); + const filteredTestCases = (() => { + if (executionMode) { + // If in execution mode but plan not loaded/ready, show nothing instead of everything + if (!activePlanCaseIds) return []; + return tree; + } + if (!selectedModuleId) return []; // Focus mode: show nothing if not selected + + const findModuleNode = (nodes: TestCaseNode[]): TestCaseNode | null => { + for (const node of nodes) { + if (node.id === selectedModuleId) return node; + if (node.children) { + const found = findModuleNode(node.children); + if (found) return found; + } + } + return null; + }; + + const modNode = findModuleNode(tree); + return modNode ? [modNode] : []; + })(); + + const traverse = (node: TestCaseNode, x: number, y: number, parentId: string | null = null) => { + const currentId = node.id; + + initialNodes.push({ + id: currentId, + type: 'mindMap', + data: { node }, + position: { x, y }, + }); + + if (parentId) { + initialEdges.push({ + id: `edge-${parentId}-${currentId}`, + source: parentId, + target: currentId, + type: 'default', + style: { stroke: '#86909C', strokeWidth: 2 }, + }); + } + + if (node.children && node.isExpanded !== false) { + const nodeHeight = getSubtreeHeight(node); + let startY = y - nodeHeight / 2; + + node.children.forEach((child) => { + const childHeight = getSubtreeHeight(child); + const childY = startY + childHeight / 2; + traverse(child, x + horizontalSpacing, childY, currentId); + startY += childHeight; + }); + } + }; + + filteredTestCases.forEach((rootNode) => { + // Always center the single root at 0,0 for clean layout + traverse(rootNode, 0, 0); + }); + + return { nodes: initialNodes, edges: initialEdges }; + + }, [testCases, activePlanCaseIds, selectedModuleId]); + + return ( +
+ { + setEditingNodeId(null); + useStore.getState().setSelectedNodeId(null); + onClearModuleSelection?.(); + }} + + fitView + fitViewOptions={{ padding: 0.2 }} + minZoom={0.1} + maxZoom={2} + deleteKeyCode={null} + selectionKeyCode={null} + multiSelectionKeyCode={null} + > + + + +
+ + +
+
+ +
+ {!executionMode ? ( + <> +
Tab 添加子节点
+
Enter 添加同级
+
Delete 删除节点
+
空格/F2 编辑文本
+ + ) : ( +
执行预览模式
+ )} +
+
+
+ +
+ ); +}; + +const MindMapView: React.FC = (props) => ( + + + +); + + + +export default MindMapView; diff --git a/src/components/editor/PropertyPanel.tsx b/src/components/editor/PropertyPanel.tsx new file mode 100644 index 0000000..2c9f317 --- /dev/null +++ b/src/components/editor/PropertyPanel.tsx @@ -0,0 +1,397 @@ +import React from 'react'; +import { useStore } from '../../store/useStore'; +import type { Priority } from '../../store/useStore'; +import { X, AlertCircle } from 'lucide-react'; +import { UserMentionInput } from './UserMentionInput'; +import { ReviewersInput } from './ReviewersInput'; + + +const PropertyPanel: React.FC = () => { + const { setSelectedNodeId, updateNode, getSelectedNode } = useStore(); + const node = getSelectedNode(); + + if (!node) return null; + + + + return ( + + ); +}; + +export default PropertyPanel; diff --git a/src/components/editor/ReviewersInput.tsx b/src/components/editor/ReviewersInput.tsx new file mode 100644 index 0000000..b0963f0 --- /dev/null +++ b/src/components/editor/ReviewersInput.tsx @@ -0,0 +1,211 @@ +import React, { useState, useRef, useEffect } from 'react'; +import usersData from '../../user.json'; +import { X, Plus } from 'lucide-react'; + +const users = usersData as Record; + +// For reverse lookup (ID -> Name) +export const feishuUserMap: Record = Object.entries(users).reduce((acc, [name, id]) => { + acc[id] = name; + return acc; +}, {} as Record); + +interface ReviewersInputProps { + value: string[]; // array of openIds + onChange: (value: string[]) => void; +} + +const avatarGradient = (name: string) => + name === '陶航宇' ? 'linear-gradient(135deg, #FF7D00, #F53F3F)' : 'linear-gradient(135deg, #165DFF, #36ABFF)'; + +export const ReviewersInput: React.FC = ({ value, onChange }) => { + const [query, setQuery] = useState(''); + const [showDropdown, setShowDropdown] = useState(false); + const wrapperRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { + setShowDropdown(false); + setQuery(''); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const addReviewer = (id: string) => { + if (!value.includes(id)) { + onChange([...value, id]); + } + setQuery(''); + setShowDropdown(false); + }; + + const removeReviewer = (id: string) => { + onChange(value.filter(v => v !== id)); + }; + + const filteredUsers = Object.entries(users).filter(([name, id]) => + !value.includes(id) && name.toLowerCase().includes(query.toLowerCase()) + ); + + return ( +
+ {/* Selected tags */} +
+ {value.map(id => { + const name = feishuUserMap[id] || id; + return ( + + + {name.slice(-2)} + + {name} + + + ); + })} + + {/* Add button / input */} +
+ { setQuery(e.target.value); setShowDropdown(true); }} + onFocus={() => setShowDropdown(true)} + /> +
+
+ + {/* Dropdown */} + {showDropdown && filteredUsers.length > 0 && ( +
+
选择评审人
+ {filteredUsers.map(([name, id]) => ( +
addReviewer(id)}> +
{name.slice(-2)}
+ {name} + +
+ ))} +
+ )} + + +
+ ); +}; diff --git a/src/components/editor/TableView.tsx b/src/components/editor/TableView.tsx new file mode 100644 index 0000000..4f70260 --- /dev/null +++ b/src/components/editor/TableView.tsx @@ -0,0 +1,842 @@ +import React, { useState, useMemo } from 'react'; +import { useStore } from '../../store/useStore'; +import type { TestCaseNode, Priority } from '../../store/useStore'; +import { useToastStore } from '../layout/Toast'; +import MindMapView from './MindMapView'; +import { ChevronRight, ChevronDown, LayoutGrid, List, FolderOpen, Folder, FileText, Plus, Trash2, CheckCircle, Clock, X } from 'lucide-react'; +import { feishuUserMap } from './UserMentionInput'; + +import { UserMentionInput } from './UserMentionInput'; +import { ReviewersInput } from './ReviewersInput'; + +const TableView: React.FC = () => { + const { + spaces, currentSpaceId, setCurrentSpaceId, addSpace, deleteSpace, spaceData, + updateNode, selectedNodeId, setSelectedNodeId, editingNodeId, addNode, addSiblingNode, deleteNode, setShowImportModal, currentUser, batchUpdateNodes + } = useStore(); + + const [showBatchEditModal, setShowBatchEditModal] = useState(false); + const [batchMaintainer, setBatchMaintainer] = useState(''); + const [batchReviewers, setBatchReviewers] = useState([]); + + + + const [displayMode, setDisplayMode] = useState<'table' | 'mindmap'>('table'); + const [selectedModuleId, setSelectedModuleId] = useState(null); + const [expandedModules, setExpandedModules] = useState>(new Set(['root'])); + const [showSpaceMenu, setShowSpaceMenu] = useState(false); + const [showSpaceDialog, setShowSpaceDialog] = useState(false); + const [spaceName, setSpaceName] = useState(''); + + const currentSpace = spaces.find(s => s.id === currentSpaceId); + const testCases = spaceData[currentSpaceId] || []; + + // Batch selection state + const [selectedIds, setSelectedIds] = useState>(new Set()); + + const moduleNodes = useMemo(() => { + return testCases; + }, [testCases]); + + const visibleCases = useMemo(() => { + if (selectedModuleId === null) return testCases; + const find = (nodes: TestCaseNode[]): TestCaseNode | null => { + for (const n of nodes) { + if (n.id === selectedModuleId) return n; + if (n.children) { const r = find(n.children); if (r) return r; } + } + return null; + }; + const mod = find(testCases); + return mod ? [mod] : testCases; + }, [testCases, selectedModuleId]); + + const toggleModule = (id: string) => { + setExpandedModules(prev => { + const next = new Set(prev); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + }; + + const handleFeishuReview = async () => { + if (!currentUser) { + addToast('请先登录', 'error'); + return; + } + + addToast('正在发起飞书评审...', 'info'); + try { + // Resolve module name from selectedModuleId + const findNode = (nodes: TestCaseNode[], id: string): TestCaseNode | null => { + for (const n of nodes) { + if (n.id === id) return n; + if (n.children) { const r = findNode(n.children, id); if (r) return r; } + } + return null; + }; + const moduleNode = selectedModuleId ? findNode(testCases, selectedModuleId) : null; + const moduleName = moduleNode ? moduleNode.text : null; + + const response = await fetch('http://localhost:8000/api/reviews/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + caseIds: Array.from(selectedIds), + reviewerOpenId: currentUser.openId, + moduleName, + message: '请各位老板们协助评审这些用例。' + }) + }); + + const result = await response.json(); + if (result.message === 'success') { + addToast('飞书评审群已拉起并发送卡片', 'success'); + setSelectedIds(new Set()); + } else { + addToast('发起评审失败', 'error'); + } + } catch (e) { + addToast('网络请求失败', 'error'); + } + }; + + + + const toggleSelect = (id: string) => { + setSelectedIds(prev => { + const next = new Set(prev); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + }; + + const toggleSelectAll = (nodes: TestCaseNode[]) => { + const allIds: string[] = []; + const collect = (list: TestCaseNode[]) => { + list.forEach(n => { + allIds.push(n.id); + if (n.children) collect(n.children); + }); + }; + collect(nodes); + + if (selectedIds.size >= allIds.length) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(allIds)); + } + }; + + const handleBatchDelete = () => { + if (window.confirm(`确定要删除选中的 ${selectedIds.size} 条用例吗?`)) { + selectedIds.forEach(id => deleteNode(id)); + setSelectedIds(new Set()); + } + }; + + const handleBatchSetStatus = (status: any) => { + selectedIds.forEach(id => updateNode(id, { reviewStatus: status })); + setSelectedIds(new Set()); + }; + + const handleBatchUpdate = async () => { + if (selectedIds.size === 0) return; + const updates: any = {}; + if (batchMaintainer) updates.maintainer = batchMaintainer; + if (batchReviewers.length > 0) updates.reviewers = batchReviewers; + + if (Object.keys(updates).length === 0) { + addToast('请至少选择一个修改项', 'info'); + return; + } + + await batchUpdateNodes(Array.from(selectedIds), updates); + addToast(`已批量更新 ${selectedIds.size} 条用例`, 'success'); + setShowBatchEditModal(false); + setSelectedIds(new Set()); + setBatchMaintainer(''); + setBatchReviewers([]); + }; + + + + const [showModuleDialog, setShowModuleDialog] = useState(false); + const [moduleName, setModuleName] = useState(''); + const [contextMenu, setContextMenu] = useState<{ x: number, y: number, nodeId: string } | null>(null); + const { addToast } = useToastStore(); + + const handleCreateModule = () => { + if (!moduleName.trim()) return; + addNode(null, moduleName); // parentId=null adds to root level + addToast(`已创建用例集: ${moduleName}`, 'success'); + setShowModuleDialog(false); + setModuleName(''); + }; + + const handleContextMenu = (e: React.MouseEvent, nodeId: string) => { + e.preventDefault(); + setContextMenu({ x: e.clientX, y: e.clientY, nodeId }); + }; + + const closeContextMenu = () => setContextMenu(null); + + const renderDirTree = (nodes: TestCaseNode[], depth: number = 0): React.ReactNode => { + return nodes.map(node => { + const hasChildren = node.children && node.children.length > 0; + const isExpanded = expandedModules.has(node.id); + const isSelected = selectedModuleId === node.id; + + return ( + +
  • { + e.stopPropagation(); + setSelectedModuleId(isSelected ? null : node.id); + if (hasChildren) toggleModule(node.id); + }} + onContextMenu={(e) => handleContextMenu(e, node.id)} + > + {hasChildren ? ( + { e.stopPropagation(); toggleModule(node.id); }}> + {isExpanded ? : } + + ) : ( + + )} + + {hasChildren ? (isExpanded ? : ) : } + + {editingNodeId === node.id ? ( + updateNode(node.id, { text: e.target.value })} + onBlur={() => useStore.getState().setEditingNodeId(null)} + onKeyDown={(e) => e.key === 'Enter' && useStore.getState().setEditingNodeId(null)} + onClick={(e) => e.stopPropagation()} + /> + ) : ( + {node.text} + )} + {countLeaves(node)} +
  • + {hasChildren && isExpanded && ( +
      + {renderDirTree(node.children!, depth + 1)} +
    + )} +
    + ); + }); + }; + + const countLeaves = (node: TestCaseNode): number => { + if (!node.children || node.children.length === 0) return 1; + return node.children.reduce((sum, c) => sum + countLeaves(c), 0); + }; + + const renderRows = (nodes: TestCaseNode[], depth: number = 0): React.ReactNode => { + return nodes.map((node) => ( + + setSelectedNodeId(node.id)} + > + + toggleSelect(node.id)} + onClick={e => e.stopPropagation()} + /> + + +
    {node.caseId || '—'}
    + + +
    + + {node.children && node.children.length > 0 ? : null} + + updateNode(node.id, { text: e.target.value })} + onClick={e => e.stopPropagation()} + /> +
    + + + + + +
    {feishuUserMap[node.maintainer] || node.maintainer || '—'}
    + + + + {node.reviewStatus === 'Reviewed' ? '已评审' : + node.reviewStatus === 'PendingReview' ? '待评审' : + node.reviewStatus === 'Deprecated' ? '已废弃' : '草稿'} + + + +
    + + + + + {node.id !== 'root' && ( + + )} +
    + + + {node.children && renderRows(node.children, depth + 1)} +
    + )); + }; + + const handleAddSpace = () => { + if (!spaceName.trim()) return; + addSpace(spaceName); + setShowSpaceDialog(false); + setSpaceName(''); + addToast(`已创建用例空间: ${spaceName}`, 'success'); + }; + + return ( +
    { closeContextMenu(); setShowSpaceMenu(false); }}> +
    + {/* Space Selector */} +
    +
    { e.stopPropagation(); setShowSpaceMenu(!showSpaceMenu); }}> + {currentSpace?.name} + +
    + + {showSpaceMenu && ( +
    e.stopPropagation()}> + {spaces.map(space => ( +
    { setCurrentSpaceId(space.id); setShowSpaceMenu(false); }} + > + {space.name} + {spaces.length > 1 && ( + + )} +
    + ))} +
    +
    { setShowSpaceDialog(true); setShowSpaceMenu(false); }}> + 新建用例空间 +
    +
    + )} +
    + +
    + 项目目录 + +
    +
      + {renderDirTree(moduleNodes)} +
    + + +
    + +
    +
    +
    + + + + {selectedIds.size > 0 && ( +
    + 已选 {selectedIds.size} 项 + + + + + + +
    + )} + +
    + + +
    +
    +
    + +
    +
    + + {!selectedModuleId ? ( +
    + +

    请从左侧目录树中选择一个用例集以查看内容

    + +
    + ) : displayMode === 'table' ? ( +
    + + + + + + + + + + + + + + {renderRows(visibleCases)} + +
    + toggleSelectAll(visibleCases)} + checked={selectedIds.size > 0 && selectedIds.size >= visibleCases.length} + /> + ID用例标题优先级维护人状态操作
    +
    + ) : ( +
    + setSelectedModuleId(null)} + /> +
    + )} + +
    + + {showModuleDialog && ( +
    setShowModuleDialog(false)}> +
    e.stopPropagation()}> +
    +

    新建用例集

    + +
    +
    + + setModuleName(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleCreateModule()} + /> +
    +
    + + +
    +
    +
    + )} + + {contextMenu && ( +
    e.stopPropagation()} + > +
    { useStore.getState().setEditingNodeId(contextMenu.nodeId); closeContextMenu(); }}> + 编辑名称 +
    +
    { + if (selectedModuleId === contextMenu.nodeId) setSelectedModuleId(null); + deleteNode(contextMenu.nodeId); + addToast('已删除目录', 'info'); + closeContextMenu(); + }}> + 删除目录 +
    +
    + )} + + {showSpaceDialog && ( +
    setShowSpaceDialog(false)}> +
    e.stopPropagation()}> +
    +

    新建用例空间

    + +
    +
    + + setSpaceName(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleAddSpace()} + /> + +
    +
    + + +
    +
    +
    + )} + + {showBatchEditModal && ( +
    setShowBatchEditModal(false)}> +
    e.stopPropagation()}> +
    +

    批量修改信息 ({selectedIds.size} 条用例)

    + +
    +
    +
    + + +
    +
    + + +

    + 提示:留空则不修改该字段。 +

    +
    +
    +
    + + +
    +
    +
    + )} + + + + +
    + ); +}; + +export default TableView; diff --git a/src/components/editor/UserMentionInput.tsx b/src/components/editor/UserMentionInput.tsx new file mode 100644 index 0000000..d5e066c --- /dev/null +++ b/src/components/editor/UserMentionInput.tsx @@ -0,0 +1,221 @@ +import React, { useState, useRef, useEffect } from 'react'; +import usersData from '../../user.json'; +import { X } from 'lucide-react'; + +const users = usersData as Record; + +// For reverse lookup (ID -> Name) +export const feishuUserMap: Record = Object.entries(users).reduce((acc, [name, id]) => { + acc[id] = name; + return acc; +}, {} as Record); + +interface UserMentionInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; +} + +export const UserMentionInput: React.FC = ({ value, onChange, placeholder }) => { + const [inputValue, setInputValue] = useState(value); + const [showDropdown, setShowDropdown] = useState(false); + const wrapperRef = useRef(null); + + useEffect(() => { + if (feishuUserMap[value] || !value) { + setInputValue(value); + } + }, [value]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setShowDropdown(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleInputChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setInputValue(val); + onChange(val); + + const searchStr = val.startsWith('@') ? val.slice(1) : val.split('@').pop() || val; + const hasMatch = Object.keys(users).some(name => name.toLowerCase().includes(searchStr.toLowerCase())); + + if (val.includes('@')) { + setShowDropdown(true); + } else { + setShowDropdown(val.length > 0 && hasMatch); + } + }; + + const handleSelectUser = (name: string, id: string) => { + onChange(id); + setInputValue(id); + setShowDropdown(false); + }; + + if (value && feishuUserMap[value]) { + return ( +
    +
    + {feishuUserMap[value].slice(-2)} +
    + {feishuUserMap[value]} + + +
    + ); + } + + const searchStr = inputValue.startsWith('@') ? inputValue.slice(1) : inputValue.split('@').pop() || inputValue; + const filteredUsers = Object.entries(users).filter(([name]) => + name.toLowerCase().includes(searchStr.toLowerCase()) + ); + + return ( +
    + { + if(inputValue && filteredUsers.length > 0) setShowDropdown(true); + }} + placeholder={placeholder || ""} + /> + + + {showDropdown && filteredUsers.length > 0 && ( +
    +
    全部人员
    + {filteredUsers.map(([name, id]) => ( +
    handleSelectUser(name, id)} + > +
    + {name.slice(-2)} +
    +
    + {name} +
    +
    + ))} +
    + )} + + +
    + ); +}; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..6afce6e --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,565 @@ +import React, { useState } from 'react'; +import { LayoutDashboard, Library, ClipboardList, Bug, BarChart3, Plus, Tag, Calendar, Search } from 'lucide-react'; +import { useStore } from '../../store/useStore'; +import { UserMentionInput } from '../editor/UserMentionInput'; +import { ReviewersInput, feishuUserMap } from '../editor/ReviewersInput'; + + +import type { TestPlanType } from '../../store/useStore'; + + +const Sidebar: React.FC = () => { + const { + spaceData, currentSpaceId, testPlans, testTasks, + selectedPlanId, setSelectedPlanId, setSelectedTaskId, + addTestPlan, viewMode, setViewMode + } = useStore(); + + const testCases = spaceData[currentSpaceId] || []; + const [showAddModal, setShowAddModal] = useState(false); + + const [newPlanName, setNewPlanName] = useState(''); + const [newPlanType, setNewPlanType] = useState('Requirement'); + const [newAssignee, setNewAssignee] = useState([]); + + const [selectedPriorities, setSelectedPriorities] = useState(['P0', 'P1', 'P2', 'P3']); + const [newPlanKeyword, setNewPlanKeyword] = useState(''); + const [selectedModules, setSelectedModules] = useState([]); + + // Collect unique module names from the test case tree + const allModules = (() => { + const modules = new Set(); + const traverse = (nodes: any[]) => nodes.forEach(n => { + if (n.text && n.id !== 'root') modules.add(n.text); + if (n.children) traverse(n.children); + }); + traverse(testCases); + return Array.from(modules); + })(); + + // Collect only meaningful directory nodes (nodes that have children) + const topLevelModules = (() => { + const result: { id: string; text: string }[] = []; + const traverse = (nodes: any[]) => { + nodes.forEach(n => { + // A directory node: MUST have children to be considered a module/set + if (n.children && n.children.length > 0 && n.text) { + result.push({ id: n.id, text: n.text }); + traverse(n.children); + } + }); + }; + traverse(testCases); + return result; + })(); + + + + + + // Flatten test cases to compute matching cases + const getMatchingCaseIds = () => { + let ids: string[] = []; + const traverse = (nodes: any[], parentModuleId?: string, depth: number = 0, parentMatched: boolean = false) => { + nodes.forEach(node => { + const moduleId = depth === 0 ? node.id : parentModuleId; + const matchesPriority = selectedPriorities.length === 0 || !node.priority || selectedPriorities.includes(node.priority); + + const currentMatchesKeyword = !newPlanKeyword || + node.text.toLowerCase().includes(newPlanKeyword.toLowerCase()) || + (node.tags && node.tags.some((t: string) => t.toLowerCase().includes(newPlanKeyword.toLowerCase()))); + + const isMatched = parentMatched || currentMatchesKeyword; + const matchesModule = selectedModules.length === 0 || (moduleId && selectedModules.includes(moduleId)); + + const isLeaf = !node.children || node.children.length === 0; + const isCase = node.caseId || isLeaf; + + if (isCase && matchesPriority && isMatched && matchesModule) { + ids.push(node.id); + } + if (node.children) traverse(node.children, moduleId, depth + 1, isMatched); + }); + }; + traverse(testCases); + + return ids; + }; + + const matchingCaseIds = getMatchingCaseIds(); + + const handleAddPlan = async () => { + if (newPlanName) { + await addTestPlan(newPlanName, newPlanType, matchingCaseIds, newAssignee.length > 0 ? newAssignee : undefined); + setNewPlanName(''); + setNewAssignee([]); + + setShowAddModal(false); + } + }; + + + + const getPlanIcon = (type: TestPlanType) => { + switch (type) { + case 'Self-test': return ; + case 'Regression': return ; + case 'Requirement': return ; + default: return ; + } + }; + + return ( + + ); +}; + +export default Sidebar; + + + diff --git a/src/components/layout/Toast.tsx b/src/components/layout/Toast.tsx new file mode 100644 index 0000000..48e2ad7 --- /dev/null +++ b/src/components/layout/Toast.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react'; +import { create } from 'zustand'; +import { CheckCircle, AlertCircle, Info, X } from 'lucide-react'; + +type ToastType = 'success' | 'error' | 'info'; + +interface Toast { + id: string; + message: string; + type: ToastType; +} + +interface ToastState { + toasts: Toast[]; + addToast: (message: string, type?: ToastType) => void; + removeToast: (id: string) => void; +} + +export const useToastStore = create((set) => ({ + toasts: [], + addToast: (message, type = 'info') => { + const id = Math.random().toString(36).substring(2, 9); + set((state) => ({ + toasts: [...state.toasts, { id, message, type }], + })); + setTimeout(() => { + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })); + }, 3000); + }, + removeToast: (id) => + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })), +})); + +export const ToastContainer: React.FC = () => { + const { toasts, removeToast } = useToastStore(); + + return ( +
    + {toasts.map((toast) => ( +
    +
    + {toast.type === 'success' && } + {toast.type === 'error' && } + {toast.type === 'info' && } +
    +
    {toast.message}
    + +
    + ))} + +
    + ); +}; diff --git a/src/components/plans/PlanListView.tsx b/src/components/plans/PlanListView.tsx new file mode 100644 index 0000000..fea108b --- /dev/null +++ b/src/components/plans/PlanListView.tsx @@ -0,0 +1,840 @@ +import React, { useState, useMemo } from 'react'; +import { useStore } from '../../store/useStore'; +import type { TestPlanType, TestCaseNode } from '../../store/useStore'; +import { ClipboardList, Plus, Search, MoreHorizontal, Calendar, User, Activity, X, Tag, Trash2 } from 'lucide-react'; +import { UserMentionInput } from '../editor/UserMentionInput'; +import { ReviewersInput, feishuUserMap } from '../editor/ReviewersInput'; + + +const PlanListView: React.FC = () => { + const { + spaceData, currentSpaceId, testPlans, testTasks, + addTestPlan, deleteTestPlan, updateTestPlan, copyTestPlan, + setViewMode, setSelectedPlanId, setSelectedTaskId + } = useStore(); + + const testCases = spaceData[currentSpaceId] || []; + const [showCreateModal, setShowCreateModal] = useState(false); + const [planName, setPlanName] = useState(''); + const [planType, setPlanType] = useState('Requirement'); + const [assignees, setAssignees] = useState([]); + const [keyword, setKeyword] = useState(''); + const [selectedModules, setSelectedModules] = useState([]); + const [selectedPriorities, setSelectedPriorities] = useState(['P0', 'P1', 'P2', 'P3']); + + const [editingPlanId, setEditingPlanId] = useState(null); + const [showEditModal, setShowEditModal] = useState(false); + + + const [searchQuery, setSearchQuery] = useState(''); + const [activeMenuId, setActiveMenuId] = useState(null); + + // Close menu when clicking outside + React.useEffect(() => { + const handleClick = () => setActiveMenuId(null); + window.addEventListener('click', handleClick); + return () => window.removeEventListener('click', handleClick); + }, []); + + const topLevelModules = (() => { + const result: { id: string; text: string }[] = []; + const traverse = (nodes: any[]) => { + nodes.forEach(n => { + // Only treat nodes WITH children as modules/directories to avoid redundancy + if (n.children && n.children.length > 0 && n.text) { + result.push({ id: n.id, text: n.text }); + traverse(n.children); + } + }); + }; + traverse(testCases); + return result; + })(); + + + // Compute matching case IDs based on filters + const getMatchingCaseIds = () => { + const ids: string[] = []; + const traverse = (nodes: TestCaseNode[], parentModuleId?: string, depth: number = 0, parentMatched: boolean = false) => { + nodes.forEach(node => { + const moduleId = depth === 0 ? node.id : parentModuleId; + + // Priority: match if no filter selected, or node matches selected priority, or node has no priority + const matchesPriority = selectedPriorities.length === 0 || + !node.priority || selectedPriorities.includes(node.priority); + + // Keyword: match title or tags + const currentMatchesKeyword = !keyword || + node.text.toLowerCase().includes(keyword.toLowerCase()) || + (node.tags && node.tags.some(t => t.toLowerCase().includes(keyword.toLowerCase()))); + + const isMatched = parentMatched || currentMatchesKeyword; + + // Module: match if no module filter, or node belongs to selected module + const matchesModule = selectedModules.length === 0 || + (moduleId && selectedModules.includes(moduleId)); + + const isLeaf = !node.children || node.children.length === 0; + const isCase = node.caseId || isLeaf; + + if (isCase && matchesPriority && isMatched && matchesModule) { + ids.push(node.id); + } + if (node.children) traverse(node.children, moduleId, depth + 1, isMatched); + }); + }; + traverse(testCases); + return ids; + }; + + + const matchingIds = getMatchingCaseIds(); + + const handleCreate = async () => { + if (!planName.trim()) return; + await addTestPlan(planName, planType, matchingIds, assignees.length > 0 ? assignees : undefined); + setPlanName(''); + setAssignees([]); + + setKeyword(''); + setSelectedModules([]); + setSelectedPriorities([]); + setShowCreateModal(false); + }; + + const handleDeletePlan = (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + if (window.confirm('确定要删除这个测试计划吗?相关的执行任务也会被移除。')) { + deleteTestPlan(id); + } + }; + + const handleEditOpen = (plan: any) => { + setEditingPlanId(plan.id); + setPlanName(plan.name); + setPlanType(plan.type); + setAssignees(plan.assignees || []); + setShowEditModal(true); + setActiveMenuId(null); + }; + + const handleSaveEdit = async () => { + if (editingPlanId && planName.trim()) { + await updateTestPlan(editingPlanId, { + name: planName, + type: planType, + assignees: assignees + }); + setShowEditModal(false); + setEditingPlanId(null); + setPlanName(''); + setAssignees([]); + } + }; + + const handleCopy = async (id: string) => { + await copyTestPlan(id); + setActiveMenuId(null); + }; + const handleOpenTask = (planId: string) => { + const task = testTasks.find(t => t.planId === planId); + + if (task) { + setSelectedTaskId(task.id); + setViewMode('execution'); + } else { + // Fallback if task was not created properly + setSelectedPlanId(planId); + setViewMode('table'); + } + }; + + const filteredPlans = searchQuery + ? testPlans.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase())) + : testPlans; + + const typeLabels: Record = { + 'Requirement': '需求测试', + 'Regression': '回归测试', + 'Self-test': '自测', + 'Smoke': '冒烟测试', + }; + + const getProgress = (plan: any) => { + const total = plan.caseIds?.length || 0; + if (total === 0) return { pct: 0, total, executed: 0 }; + + let executed = 0; + const traverse = (nodes: TestCaseNode[]) => { + nodes.forEach(node => { + if (plan.caseIds.includes(node.id) && node.executionStatus && node.executionStatus !== 'UNTESTED') { + executed++; + } + if (node.children) traverse(node.children); + }); + }; + traverse(testCases); + + return { pct: Math.round((executed / total) * 100), total, executed }; + }; + + return ( +
    +
    +
    +

    测试计划

    + {testPlans.length} +
    +
    +
    + + setSearchQuery(e.target.value)} + /> +
    + +
    +
    + +
    + {filteredPlans.map(plan => { + const progress = getProgress(plan); + return ( +
    handleOpenTask(plan.id)}> +
    +
    + +
    +
    {typeLabels[plan.type] || plan.type}
    +
    +
    + + {activeMenuId === plan.id && ( +
    +
    { e.stopPropagation(); handleEditOpen(plan); }}>编辑计划
    +
    { e.stopPropagation(); handleCopy(plan.id); }}>复制计划
    +
    { e.stopPropagation(); handleDeletePlan(e, plan.id); }}>删除计划
    + +
    + )} +
    +
    + +
    + +
    +

    {plan.name}

    +
    +
    + + {plan.createdAt?.split('T')[0]} +
    +
    + +
    +
    + 执行进度 + {progress.executed}/{progress.total} ({progress.pct}%) +
    +
    +
    +
    +
    +
    + +
    +
    {plan.caseIds?.length || 0} 用例
    + +
    +
    + ); + })} + + {filteredPlans.length === 0 && ( +
    + +

    {searchQuery ? '没有匹配的计划' : '暂无测试计划,点击右上角创建'}

    +
    + )} +
    + + {/* ========== Create Plan Modal ========== */} + {showCreateModal && ( +
    setShowCreateModal(false)}> +
    e.stopPropagation()}> +
    +

    创建测试计划

    + +
    + +
    +
    + + setPlanName(e.target.value)} + autoFocus + /> +
    + +
    + + +
    + +
    + + +
    + + +
    + +
    + {topLevelModules.map(m => ( + + ))} +
    +
    + +
    + +
    + + setKeyword(e.target.value)} + /> +
    +
    + +
    + +
    + {['P0', 'P1', 'P2', 'P3'].map(p => ( + + ))} +
    +
    + +
    + 已匹配到 {matchingIds.length} 条相关用例 +
    +
    + +
    + + +
    +
    +
    + )} + + {showEditModal && ( +
    setShowEditModal(false)}> +
    e.stopPropagation()}> +
    +

    编辑测试计划

    + +
    +
    +
    + + setPlanName(e.target.value)} + autoFocus + /> +
    + +
    + + +
    + +
    + + +
    +
    + +
    + + +
    +
    +
    + )} + + +
    + ); +}; + +export default PlanListView; diff --git a/src/components/plans/TaskExecutionView.tsx b/src/components/plans/TaskExecutionView.tsx new file mode 100644 index 0000000..1c5b0fb --- /dev/null +++ b/src/components/plans/TaskExecutionView.tsx @@ -0,0 +1,411 @@ +import React, { useState, useEffect } from 'react'; +import { useStore } from '../../store/useStore'; +import { ChevronLeft, CheckCircle, XCircle, Ban, Play, Save, Bug, LayoutGrid, List } from 'lucide-react'; +import { useToastStore } from '../layout/Toast'; +import MindMapView from '../editor/MindMapView'; + +const TaskExecutionView: React.FC = () => { + const { spaceData, currentSpaceId, testTasks, selectedTaskId, setViewMode, testPlans, updateNode, updateTaskStatus, addBug } = useStore(); + const testCases = spaceData[currentSpaceId] || []; + const { addToast } = useToastStore(); + + const task = testTasks.find(t => t.id === selectedTaskId); + const plan = testPlans.find(p => p.id === task?.planId); + + const [displayMode, setDisplayMode] = useState<'list' | 'mindmap'>('list'); + + useEffect(() => { + if (plan && useStore.getState().selectedPlanId !== plan.id) { + useStore.getState().setSelectedPlanId(plan.id); + } + }, [plan]); + + // 查找属于该计划的用例 + const getPlanCases = () => { + // Robust search for the plan if testPlans is still loading + const currentPlan = plan || testPlans.find(p => p.id === (task?.planId || useStore.getState().selectedPlanId)); + if (!currentPlan || !currentPlan.caseIds) return []; + + const results: any[] = []; + const targetIds = new Set(currentPlan.caseIds); + + // Search across all spaceData if currentSpaceId is not set yet + const allData = currentSpaceId ? [testCases] : Object.values(spaceData); + + const traverse = (nodes: any[]) => { + nodes.forEach(node => { + if (targetIds.has(node.id)) { + results.push(node); + } + if (node.children) traverse(node.children); + }); + }; + + allData.forEach(tree => traverse(tree)); + return results; + }; + + + const planCases = getPlanCases(); + + // If we have plan but no cases found, and currentSpaceId is empty, we might need a sync + useEffect(() => { + if (plan && !currentSpaceId && Object.keys(spaceData).length > 0) { + // Try to find which space this plan belongs to + // (Simplified: for now just fetch data if empty) + } + }, [plan, currentSpaceId, spaceData]); + + + if (!task) return
    任务不存在
    ; + + return ( +
    +
    +
    + +
    +

    {task.name}

    +
    + {task.status} + {plan?.type || 'General'} +
    +
    +
    +
    +
    + + +
    + {task.status !== 'COMPLETED' && ( + + )} +
    +
    + + {displayMode === 'list' ? ( +
    +
    +

    用例列表 ({planCases.length})

    +
    + {planCases.map(c => ( +
    + + {c.text} +
    + ))} +
    +
    + +
    +
    + {planCases.map(c => ( +
    +
    +
    + {c.priority} +

    {c.text}

    +
    +
    + + + +
    +
    + + {c.steps && c.steps.length > 0 && ( +
    +
    执行步骤
    +
    + {c.steps.map((s: any, idx: number) => ( +
    + {idx + 1} + {s.action} + {s.expected} +
    + ))} +
    +
    + )} + +
    + {c.executionStatus === 'FAIL' && !c.bugId && ( + + )} + {c.bugId && ( +
    + + 已关联 Bug: {c.bugId} +
    + )} +
    +
    + ))} +
    +
    +
    + ) : ( +
    + +
    + )} + + +
    + ); +}; + +export default TaskExecutionView; diff --git a/src/components/shared/BugView.tsx b/src/components/shared/BugView.tsx new file mode 100644 index 0000000..013a62d --- /dev/null +++ b/src/components/shared/BugView.tsx @@ -0,0 +1,245 @@ +import React from 'react'; +import { useStore } from '../../store/useStore'; +import { Bug as BugIcon, CheckCircle, Clock } from 'lucide-react'; + +const BugView: React.FC = () => { + const { bugs, deleteBug } = useStore(); + + const [selectedBug, setSelectedBug] = React.useState(null); + + const handleDelete = (id: string) => { + if (window.confirm('确定要删除这个 Bug 记录吗?')) { + deleteBug(id); + } + }; + + return ( +
    +
    +

    缺陷管理 (Bug Tracker)

    +
    +
    + 总计: {bugs.length} +
    +
    + 处理中: {bugs.filter(b => b.status === 'OPEN').length} +
    +
    +
    + +
    + {bugs.length === 0 ? ( +
    当前没有记录任何 Bug。
    + ) : ( + + + + + + + + + + + + {bugs.map(bug => ( + + + + + + + + ))} + +
    Bug ID标题关联用例状态操作
    {bug.id}{bug.title}{bug.caseId} + + {bug.status === 'OPEN' ? '未解决' : + bug.status === 'RESOLVED' ? '已修复' : '已关闭'} + + +
    + + +
    +
    + )} +
    + + {/* Bug Details Modal */} + {selectedBug && ( +
    setSelectedBug(null)}> +
    e.stopPropagation()}> +
    +

    缺陷详情 - {selectedBug.id}

    + +
    +
    +
    + +
    {selectedBug.title}
    +
    +
    + +
    {selectedBug.caseId}
    +
    +
    + +
    + + {selectedBug.status === 'OPEN' ? '未解决' : + selectedBug.status === 'RESOLVED' ? '已修复' : '已关闭'} + +
    +
    +
    + +
    {selectedBug.description || '暂无详细描述'}
    +
    +
    +
    + +
    +
    +
    + )} + + + + +
    + ); +}; + +export default BugView; diff --git a/src/components/shared/DashboardView.tsx b/src/components/shared/DashboardView.tsx new file mode 100644 index 0000000..cafe26c --- /dev/null +++ b/src/components/shared/DashboardView.tsx @@ -0,0 +1,456 @@ +import React from 'react'; +import { useStore } from '../../store/useStore'; +import type { TestCaseNode } from '../../store/useStore'; + +import { BarChart3, PieChart, TrendingUp, CheckCircle2, XCircle, AlertCircle } from 'lucide-react'; + +const DashboardView: React.FC = () => { + const { spaceData, currentSpaceId, testTasks, setViewMode, setSelectedTaskId, currentUser } = useStore(); + + const testCases = spaceData[currentSpaceId] || []; + + const handleOpenTask = (taskId: string) => { + setSelectedTaskId(taskId); + setViewMode('execution'); + }; + + + + // Simple aggregation logic + const getStats = (nodes: TestCaseNode[]) => { + let stats = { total: 0, P0: 0, P1: 0, P2: 0, P3: 0, pass: 0, fail: 0, untested: 0 }; + + const traverse = (items: TestCaseNode[]) => { + items.forEach(node => { + stats.total++; + if (node.priority) stats[node.priority]++; + if (node.executionStatus === 'PASS') stats.pass++; + else if (node.executionStatus === 'FAIL') stats.fail++; + else stats.untested++; + + + if (node.children) traverse(node.children); + }); + }; + + traverse(nodes); + return stats; + }; + + const stats = getStats(testCases); + const passRate = stats.total > 0 ? Math.round((stats.pass / stats.total) * 100) : 0; + + return ( +
    +
    +
    +
    + 总用例数 + +
    +
    {stats.total}
    +
    + + 较上周 +12% +
    +
    + +
    +
    + 通过率 + +
    +
    {passRate}%
    +
    +
    +
    +
    + +
    +
    + P0 核心用例 + +
    +
    {stats.P0}
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +

    执行分布 (Execution Distribution)

    +
    +
    +
    + + 已通过 (Pass) +
    +
    +
    0 ? (stats.pass/stats.total)*100 : 0}%` }}>
    + {stats.pass} +
    +
    +
    +
    + + 未通过 (Fail) +
    +
    +
    0 ? (stats.fail/stats.total)*100 : 0}%` }}>
    + {stats.fail} +
    +
    +
    +
    + + 未开始 (Untested) +
    +
    +
    0 ? (stats.untested/stats.total)*100 : 0}%` }}>
    + {stats.untested} +
    +
    +
    +
    + +
    +

    优先级分布 (Priority Analysis)

    +
    + {['P0', 'P1', 'P2', 'P3'].map(p => { + const count = stats[p as keyof typeof stats] as number; + const percentage = stats.total > 0 ? (count / stats.total) * 100 : 0; + return ( +
    +
    +
    + {count} +
    +
    + {p} +
    + ); + })} +
    +
    + +
    +

    通过率概览 (Pass Rate Donut)

    +
    +
    +
    + {passRate}% + 通过 +
    +
    +
    +
    通过: {stats.pass}
    +
    失败: {stats.fail}
    +
    未执行: {stats.untested}
    +
    +
    +
    + +
    +

    任务看板 (Task Board)

    +
    + {testTasks.length === 0 ? ( +
    暂无执行任务
    + ) : ( + testTasks.map(task => ( +
    handleOpenTask(task.id)} + > +
    + {task.name} + {((task.assignee && task.assignee.startsWith('ou_')) || !task.assignee) ? (currentUser?.name || '未知用户') : task.assignee} · {task.createdAt?.split('T')[0]} + +
    +
    + {task.status === 'RUNNING' && '运行中'} + {task.status === 'PENDING' && '未运行'} + {task.status === 'COMPLETED' && '已完成'} +
    +
    + )) + )} +
    +
    + +
    + + +
    + ); +}; + +export default DashboardView; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..5f8ac58 --- /dev/null +++ b/src/index.css @@ -0,0 +1,134 @@ +:root { + --primary: #0066FF; + --primary-hover: #0052CC; + --bg-main: #F4F7FB; + --bg-surface: #FFFFFF; + --text-main: #1D2129; + --text-secondary: #4E5969; + --border-color: #E5E6EB; + --p0-color: #F53F3F; + --p1-color: #FF7D00; + --p2-color: #F7BA1E; + --p3-color: #165DFF; + --radius-md: 8px; + --radius-lg: 12px; + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --transition-base: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + background-color: var(--bg-main); + color: var(--text-main); + overflow: hidden; +} + +#root { + height: 100vh; + display: flex; + flex-direction: column; +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #C9CDD4; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #86909C; +} + +/* Glassmorphism Classes */ +.glass { + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.card { + background: var(--bg-surface); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + border: 1px solid var(--border-color); +} + +/* Global Button & Interactive Feedback */ +button, +[role="button"], +.clickable, +.interactive-item, +.dir-item, +.global-nav-list li, +.plan-list li, +.table-row, +.tool-btn, +.toggle-btn, +.action-btn { + transition: var(--transition-base); + cursor: pointer; + user-select: none; +} + +button:active, +[role="button"]:active, +.clickable:active, +.interactive-item:active, +.dir-item:active, +.global-nav-list li:active, +.plan-list li:active, +.tool-btn:active, +.toggle-btn:active, +.action-btn:active { + transform: scale(0.97); + opacity: 0.9; +} + +.table-row:active { + background-color: #F0F7FF !important; +} + +/* Base button styles to ensure consistency */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 16px; + border-radius: var(--radius-md); + font-weight: 500; + font-size: 14px; + border: 1px solid transparent; + transition: var(--transition-base); +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover { + background: var(--primary-hover); + box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2); +} + +.btn-primary:active { + transform: scale(0.96); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/store/useStore.ts b/src/store/useStore.ts new file mode 100644 index 0000000..4ef7378 --- /dev/null +++ b/src/store/useStore.ts @@ -0,0 +1,670 @@ +import { create } from 'zustand'; + +export type Priority = 'P0' | 'P1' | 'P2' | 'P3'; +export type TestPlanType = 'Self-test' | 'Regression' | 'Requirement' | 'Smoke'; + +export interface TestPlan { + id: string; + name: string; + type: TestPlanType; + createdAt: string; + caseIds?: string[]; + assignees?: string[]; +} + + +export interface TestTask { + id: string; + name: string; + status: 'PENDING' | 'RUNNING' | 'COMPLETED'; + planId: string; + assignees: string[]; + createdAt: string; +} + +export interface Bug { + + id: string; + title: string; + status: 'OPEN' | 'RESOLVED' | 'CLOSED'; + caseId: string; +} + +export interface TestCaseNode { + id: string; + caseId?: string; // e.g., TC-001 + text: string; // Title + module?: string; // Module category + type?: 'Web' | 'Mobile' | 'API' | 'General'; + priority?: Priority; + children?: TestCaseNode[]; + steps?: { action: string; expected: string }[]; + isExpanded?: boolean; + executionStatus?: 'PASS' | 'FAIL' | 'BLOCK' | 'UNTESTED'; + reviewStatus?: 'Draft' | 'PendingReview' | 'Reviewed' | 'Deprecated'; + maintainer?: string; + + requirementId?: string; + bugId?: string; + tags?: string[]; + reviewers?: string[]; // Array of openIds for Feishu group review +} + + + + + + +export interface TestReport { + id: string; + planId: string; + total: number; + pass: number; + fail: number; + date: string; +} + +export interface Space { + id: string; + name: string; +} + +interface AppState { + viewMode: 'mindmap' | 'table' | 'dashboard' | 'bugs' | 'execution' | 'plans'; + spaces: Space[]; + currentSpaceId: string; + spaceData: { [spaceId: string]: TestCaseNode[] }; + + testPlans: TestPlan[]; + testTasks: TestTask[]; + selectedPlanId: string | null; + selectedNodeId: string | null; + editingNodeId: string | null; + selectedTaskId: string | null; + showImportModal: boolean; + currentUser: { name: string; openId: string } | null; + + // Space Actions + addSpace: (name: string) => void; + deleteSpace: (id: string) => void; + setCurrentSpaceId: (id: string) => void; + + setViewMode: (mode: 'mindmap' | 'table' | 'dashboard' | 'bugs' | 'execution' | 'plans') => void; + fetchSpaces: () => Promise; + fetchData: () => Promise; + + fetchTasks: () => Promise; + fetchBugs: () => Promise; + importNodes: (nodes: any[]) => Promise; + addTask: (task: Partial) => Promise; + + updateTaskStatus: (taskId: string, status: TestTask['status']) => Promise; + + setSelectedNodeId: (id: string | null) => void; + setEditingNodeId: (id: string | null) => void; + setSelectedTaskId: (id: string | null) => void; + setSelectedPlanId: (id: string | null) => void; + setShowImportModal: (show: boolean) => void; + setCurrentUser: (user: { name: string; openId: string } | null) => void; + logout: () => void; + + addTestPlan: (name: string, type: TestPlanType, caseIds?: string[], assignees?: string[]) => void; + + + updateNode: (id: string, updates: Partial) => void; + updateNodeSteps: (id: string, steps: { action: string; expected: string }[]) => void; + addNode: (parentId: string | null, title?: string) => void; + addSiblingNode: (id: string, title?: string) => void; + deleteNode: (id: string) => void; + addBug: (caseId: string, title: string) => void; + deleteBug: (id: string) => void; + deleteTestPlan: (id: string) => void; + updateTestPlan: (id: string, updates: Partial) => Promise; + copyTestPlan: (id: string) => Promise; + batchUpdateNodes: (ids: string[], updates: Partial) => Promise; + + + getSelectedNode: () => TestCaseNode | null; + +} + + + + +const initialData: TestCaseNode[] = []; + + + + +export const useStore = create((set, get) => ({ + viewMode: 'dashboard', + spaces: [], + currentSpaceId: '', + + spaceData: {}, + + + testPlans: [], + testTasks: [], + bugs: [], + + selectedPlanId: null, + selectedNodeId: null, + editingNodeId: null, + selectedTaskId: null, + showImportModal: false, + currentUser: (() => { + try { + const saved = localStorage.getItem('quantum_user'); + return saved ? JSON.parse(saved) : null; + } catch { + return null; + } + })(), + + + fetchSpaces: async () => { + try { + const res = await fetch('http://localhost:8000/api/spaces'); + const data = await res.json(); + if (data.data && data.data.length > 0) { + const existingId = get().currentSpaceId; + const validId = data.data.find((s: {id: string}) => s.id === existingId) + ? existingId + : data.data[0].id; + set({ spaces: data.data, currentSpaceId: validId }); + // Auto-fetch cases for the resolved space + await get().fetchData(); + } else if (data.data) { + set({ spaces: data.data, currentSpaceId: '' }); + } + } catch (e) { + console.error('Failed to fetch spaces', e); + } + }, + + addSpace: async (name) => { + try { + const res = await fetch('http://localhost:8000/api/spaces', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }) + }); + const data = await res.json(); + if (data.id) { + set(state => ({ + spaces: [...state.spaces, { id: data.id, name }], + currentSpaceId: data.id, + spaceData: { ...state.spaceData, [data.id]: [] } + })); + } + } catch (e) { + console.error('Failed to add space', e); + } + }, + + deleteSpace: async (id) => { + try { + await fetch(`http://localhost:8000/api/spaces/${id}`, { method: 'DELETE' }); + set(state => { + const newSpaces = state.spaces.filter(s => s.id !== id); + const newCurrentId = state.currentSpaceId === id ? (newSpaces[0]?.id || '') : state.currentSpaceId; + return { spaces: newSpaces, currentSpaceId: newCurrentId }; + }); + } catch (e) { + console.error('Failed to delete space', e); + } + }, + + + setCurrentSpaceId: (currentSpaceId) => set({ currentSpaceId }), + + fetchData: async () => { + const sid = get().currentSpaceId; + if (!sid) return; + try { + const [casesRes, plansRes] = await Promise.all([ + fetch(`http://localhost:8000/api/cases?space_id=${sid}`), + fetch(`http://localhost:8000/api/plans?space_id=${sid}`), + ]); + + + const casesData = await casesRes.json(); + const plansData = await plansRes.json(); + + set(state => ({ + // 只要后端返回了 data 数组(即使是空数组),就覆盖本地状态,避免 fallback 到 initialData + spaceData: (casesData && Array.isArray(casesData.data)) + ? { ...state.spaceData, [sid]: casesData.data } + : state.spaceData, + testPlans: (plansData && Array.isArray(plansData.data)) + ? plansData.data + : state.testPlans, + })); + + } catch (e) { + console.warn('[DB] fetchData failed, using local state', e); + } + }, + + fetchTasks: async () => { + try { + const response = await fetch('http://localhost:8000/api/tasks'); + const data = await response.json(); + set({ testTasks: data.data || [] }); + } catch (e) { + console.error('Failed to fetch tasks', e); + } + }, + + fetchBugs: async () => { + try { + const response = await fetch('http://localhost:8000/api/bugs'); + const data = await response.json(); + set({ bugs: data.data || [] }); + } catch (e) { + console.error('Failed to fetch bugs', e); + } + }, + + + addTask: async (task) => { + try { + const response = await fetch('http://localhost:8000/api/tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(task) + }); + const result = await response.json(); + if (result.id) { + const fullTask = { ...task, id: result.id, createdAt: new Date().toISOString() } as TestTask; + set(state => ({ testTasks: [...state.testTasks, fullTask] })); + } + } catch (e) { + console.warn('Failed to add task to backend, falling back to local state'); + const fullTask = { ...task, id: `task-${Date.now()}`, createdAt: new Date().toISOString() } as TestTask; + set(state => ({ testTasks: [...state.testTasks, fullTask] })); + } + }, + + updateTaskStatus: async (taskId, status) => { + try { + await fetch(`http://localhost:8000/api/tasks/${taskId}/status`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status }) + }); + set(state => ({ + testTasks: state.testTasks.map(t => t.id === taskId ? { ...t, status } : t) + })); + } catch (e) { + console.error('Failed to update task status', e); + } + }, + + setViewMode: (viewMode) => set({ viewMode }), + + setSelectedNodeId: (selectedNodeId) => set({ selectedNodeId }), + setEditingNodeId: (editingNodeId) => set({ editingNodeId }), + setSelectedTaskId: (selectedTaskId) => set({ selectedTaskId }), + setSelectedPlanId: (selectedPlanId) => set({ selectedPlanId }), + setShowImportModal: (showImportModal) => set({ showImportModal }), + setCurrentUser: (currentUser) => { + if (currentUser) { + localStorage.setItem('quantum_user', JSON.stringify(currentUser)); + } else { + localStorage.removeItem('quantum_user'); + } + set({ currentUser }); + }, + + logout: () => { + localStorage.removeItem('quantum_user'); + set({ + currentUser: null, + testPlans: [], + testTasks: [], + bugs: [], + spaceData: {} + }); + }, + + + + addTestPlan: async (name, type, caseIds, assignees = []) => { + const planId = `p-${Date.now()}`; + const newPlan: TestPlan = { + id: planId, + name, + type, + caseIds: caseIds || [], + assignees: assignees, + createdAt: new Date().toISOString().split('T')[0] + }; + // Optimistic update + set((state) => ({ testPlans: [...state.testPlans, newPlan] })); + const sid = get().currentSpaceId; + // Persist to DB + fetch(`http://localhost:8000/api/plans?space_id=${sid}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: planId, name, type, caseIds: caseIds || [], assignees, createdAt: newPlan.createdAt }), + }).catch(e => console.warn('[DB] addTestPlan failed', e)); + + + + const state = get(); + await state.addTask({ + name: `【执行任务】${name}`, + status: 'PENDING', + planId, + assignees: assignees, + }); + + // Ensure data is synced + await get().fetchTasks(); + await get().fetchData(); + + }, + + updateTestPlan: async (id, updates) => { + set(state => ({ + testPlans: state.testPlans.map(p => p.id === id ? { ...p, ...updates } : p) + })); + await fetch(`http://localhost:8000/api/plans/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); + // Refresh to get full sync + get().fetchTasks(); + }, + + copyTestPlan: async (id) => { + const res = await fetch(`http://localhost:8000/api/plans/${id}/copy`, { method: 'POST' }); + const data = await res.json(); + if (data.id) { + // Refresh lists + get().fetchSpaces(); + get().fetchTasks(); + } + }, + + deleteTestPlan: async (id) => { + + // Optimistic delete + set((state) => ({ + testPlans: state.testPlans.filter(p => p.id !== id), + testTasks: state.testTasks.filter(t => t.planId !== id) + })); + // Persist to DB + fetch(`http://localhost:8000/api/plans/${id}`, { method: 'DELETE' }) + .catch(e => console.warn('[DB] deleteTestPlan failed', e)); + }, + + + updateNodeSteps: (id, steps) => { + set((state) => { + const sid = state.currentSpaceId; + const currentTree = state.spaceData[sid]; + const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] => + nodes.map((node) => { + if (node.id === id) return { ...node, steps }; + if (node.children) return { ...node, children: updateRecursive(node.children) }; + return node; + }); + return { spaceData: { ...state.spaceData, [sid]: updateRecursive(currentTree) } }; + }); + // Persist to DB + fetch(`http://localhost:8000/api/cases/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, steps }), + }).catch(e => console.warn('[DB] updateNodeSteps failed', e)); + }, + + updateNode: (id, updates) => { + // 1. Optimistic update + set((state) => { + const sid = state.currentSpaceId; + const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] => + nodes.map((node) => + node.id === id + ? { ...node, ...updates } + : { ...node, children: node.children ? updateRecursive(node.children) : [] } + ); + return { spaceData: { ...state.spaceData, [sid]: updateRecursive(state.spaceData[sid] || []) } }; + }); + // 2. Persist to DB + fetch(`http://localhost:8000/api/cases/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, ...updates }), + }).catch(e => console.warn('[DB] updateNode failed', e)); + }, + + addNode: (parentId, title = '') => { + const sid = get().currentSpaceId; + const currentTree = get().spaceData[sid] || []; + const newNodeId = `node-${Date.now()}`; + + let inheritedPriority: Priority | undefined; + + let nextTree: TestCaseNode[]; + if (!parentId) { + nextTree = [...currentTree, { id: newNodeId, text: title, priority: 'P2' }]; + } else { + const traverseAndAdd = (nodes: TestCaseNode[]): TestCaseNode[] => + nodes.map((node) => { + if (node.id === parentId) { + inheritedPriority = node.priority; + return { + ...node, + children: [...(node.children || []), { id: newNodeId, text: title, priority: node.priority || 'P2' }] + }; + } + if (node.children) return { ...node, children: traverseAndAdd(node.children) }; + return node; + }); + nextTree = traverseAndAdd(currentTree); + } + + set({ spaceData: { ...get().spaceData, [sid]: nextTree }, selectedNodeId: newNodeId, editingNodeId: newNodeId }); + + // Persist to DB + fetch(`http://localhost:8000/api/cases?space_id=${sid}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: newNodeId, + text: title, + parentId, + priority: inheritedPriority || 'P2' + }), + }).catch(e => console.warn('[DB] addNode failed', e)); + }, + + + addSiblingNode: (id, title = '') => { + const sid = get().currentSpaceId; + const currentTree = get().spaceData[sid]; + const newNodeId = `node-${Date.now()}`; + let inheritedPriority: Priority | undefined; + + const traverseAndAddSibling = (nodes: TestCaseNode[]): TestCaseNode[] => { + const index = nodes.findIndex((n) => n.id === id); + if (index !== -1) { + inheritedPriority = nodes[index].priority; + const newNodes = [...nodes]; + newNodes.splice(index + 1, 0, { id: newNodeId, text: title, priority: inheritedPriority || 'P2' }); + return newNodes; + } + return nodes.map((node) => { + if (node.children) { + const result = traverseAndAddSibling(node.children); + if (result !== node.children) return { ...node, children: result }; + } + return node; + }); + }; + const nextTree = traverseAndAddSibling(currentTree); + set({ spaceData: { ...get().spaceData, [sid]: nextTree }, selectedNodeId: newNodeId, editingNodeId: newNodeId }); + + // Persist to DB + fetch(`http://localhost:8000/api/cases?space_id=${sid}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: newNodeId, + text: title, + priority: inheritedPriority || 'P2' + }), + }).catch(e => console.warn('[DB] addSiblingNode failed', e)); + }, + + + deleteNode: (id) => { + // Optimistic delete + set((state) => { + const sid = state.currentSpaceId; + const deleteRecursive = (nodes: TestCaseNode[]): TestCaseNode[] => + nodes.filter((n) => n.id !== id).map((node) => ({ + ...node, + children: node.children ? deleteRecursive(node.children) : [], + })); + return { + spaceData: { ...state.spaceData, [sid]: deleteRecursive(state.spaceData[sid] || []) }, + selectedNodeId: state.selectedNodeId === id ? null : state.selectedNodeId, + }; + }); + // Persist to DB (cascades on backend) + fetch(`http://localhost:8000/api/cases/${id}`, { method: 'DELETE' }) + .catch(e => console.warn('[DB] deleteNode failed', e)); + }, + + addBug: (caseId, title) => { + const newBug: Bug = { + id: `BUG-${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`, + title, + status: 'OPEN', + caseId, + }; + set((state) => { + const sid = state.currentSpaceId; + const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] => + nodes.map((node) => { + if (node.id === caseId || node.caseId === caseId) return { ...node, bugId: newBug.id }; + if (node.children) return { ...node, children: updateRecursive(node.children) }; + return node; + }); + return { + bugs: [...state.bugs, newBug], + spaceData: { ...state.spaceData, [sid]: updateRecursive(state.spaceData[sid] || []) }, + }; + }); + // Persist bug to DB + fetch('http://localhost:8000/api/bugs', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: newBug.id, title, status: 'OPEN', caseId }), + }).catch(e => console.warn('[DB] addBug failed', e)); + }, + + deleteBug: async (id) => { + // Optimistic delete + set((state) => ({ + bugs: state.bugs.filter(b => b.id !== id) + })); + // Persist to DB + fetch(`http://localhost:8000/api/bugs/${id}`, { method: 'DELETE' }) + .catch(e => console.warn('[DB] deleteBug failed', e)); + }, + + + getSelectedNode: () => { + const state = get(); + if (!state.selectedNodeId) return null; + const currentTree = state.spaceData[state.currentSpaceId] || []; + + const findRecursive = (nodes: TestCaseNode[]): TestCaseNode | null => { + for (const node of nodes) { + if (node.id === state.selectedNodeId) return node; + if (node.children) { + const found = findRecursive(node.children); + if (found) return found; + } + } + return null; + }; + return findRecursive(currentTree); + }, + importNodes: async (nodes) => { + + const sid = get().currentSpaceId; + if (!sid) return; + + // Flatten the tree for backend (backend expects flat list with parentId) + const flatNodes: any[] = []; + const flatten = (items: any[], pid: string | null = null) => { + items.forEach(item => { + flatNodes.push({ + id: item.id, + text: item.text, + parentId: pid, + steps: item.steps || [], + tags: item.tags || [] + }); + if (item.children && item.children.length > 0) { + flatten(item.children, item.id); + } + }); + }; + flatten(nodes); + + try { + await fetch(`http://localhost:8000/api/cases/batch?space_id=${sid}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(flatNodes) + }); + // Refresh local data after import + await get().fetchData(); + } catch (e) { + console.error('Failed to batch import nodes', e); + } + }, + + batchUpdateNodes: async (ids, updates) => { + // 1. Optimistic update + set((state) => { + const sid = state.currentSpaceId; + const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] => + nodes.map((node) => { + const newNode = ids.includes(node.id) ? { ...node, ...updates } : node; + if (newNode.children) { + newNode.children = updateRecursive(newNode.children); + } + return newNode; + }); + return { spaceData: { ...state.spaceData, [sid]: updateRecursive(state.spaceData[sid] || []) } }; + }); + + // 2. Persist to DB + try { + await fetch('http://localhost:8000/api/cases/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + caseIds: ids, + maintainer: updates.maintainer, + reviewers: updates.reviewers + }) + }); + } catch (e) { + console.error('Failed to batch update nodes', e); + } + }, +})); + + + diff --git a/src/user.json b/src/user.json new file mode 100644 index 0000000..327be28 --- /dev/null +++ b/src/user.json @@ -0,0 +1,27 @@ +{ + "陶航宇": "ou_530d280ca8119a99fba2c0b31d5135ee", + "祖立峰": "ou_90b9d4c9645363f502ca5a825ecd7e83", + "付程玲": "ou_9334a6c5aab73d32419e1059763cbf26", + "王伟东": "ou_a8be7e4fbb78b8181ddfb6c883448709", + "孙悦": "ou_dafde22c2bc376d6440a39c819cf877c", + "侯志勇": "ou_f2cc58ae8ae5c43e59243a7fc976c25f", + "杨磊": "ou_7dd26678a97b59a082340e759ca569a9", + "王斌": "ou_798bd0a50d8c5f3eeb2924ec0bd65acf", + "刘亚琼": "ou_2351be9c2c6e465f37582f4cef5c32f4", + "杜凯": "ou_545c99a3dca03b175af61a30b124bfe9", + "路强": "ou_95187be353a3ccdfafa7061b0e42c411", + "张家浩": "ou_746a541759dc523290ef7062d42ae533", + "赵晗": "ou_812d3705b105ea342000d0dc4376a146", + "陈适": "ou_3b9699365ed8d28ba7f577f75a291644", + "杨祎伟": "ou_3587ec1084d6fac875e4e0d1fc0e9715", + "辛海丹": "ou_700f829a3de5af1685f6f3d90a9e200a", + "圣正杰": "ou_4ef3536f42ee674221b4cb9ffa469d4b", + "汪明": "ou_dccc3e3296ec78ef16a10d5a07e45799", + "李莹": "ou_e1c0c73e10d0a5a2f20e3ae853d5eb13", + "周天如": "ou_66bdd9ce844efaed6bd71a92325cfbb0", + "吴龙涛": "ou_6696b998749b8456729e4cfa4b295559", + "吴超": "ou_675dea340907ecb416f2c9d96f634192", + "郝建新": "ou_f9681314b63b485bf3e8650f7c43a411", + "董红帅": "ou_775ff733bd2d407ab21b7a9c4fa7fd3b", + "陈钦洋": "ou_6f1269a7e024a1711cfedc5d96052351" +} \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..7f42e5f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})