diff --git a/README.md b/README.md index 1fc9e48..fe540d8 100644 --- a/README.md +++ b/README.md @@ -506,4 +506,19 @@ BioDockLab is a research software prototype. It is not a medical device, diagnostic tool, clinical decision-making system, or validated biological prediction system. -All simulation and analysis results are for software demonstration and research workflow exploration only. \ No newline at end of file +All simulation and analysis results are for software demonstration and research workflow exploration only. +--- + +## Hand-Drawn System Concept + +Before implementation, BioDockLab was organized through hand-drawn system sketches to clarify the platform architecture, medical/bio data flow, AI analysis layer, simulation direction, and role-based access model. + +### System Overview + +![BioDockLab Handdrawn System Overview](assets/concept/biodocklab_handdrawn_system_overview.png) + +### Medical / Bio AI Map + +![BioDockLab Medical AI Map](assets/concept/biodocklab_handdrawn_medical_ai_map.png) + +More details: [Handdrawn System Concept](docs/architecture/Handdrawn_System_Concept.md) diff --git a/assets/concept/biodocklab_handdrawn_medical_ai_map.png b/assets/concept/biodocklab_handdrawn_medical_ai_map.png new file mode 100644 index 0000000..f21f61d Binary files /dev/null and b/assets/concept/biodocklab_handdrawn_medical_ai_map.png differ diff --git a/assets/concept/biodocklab_handdrawn_system_overview.png b/assets/concept/biodocklab_handdrawn_system_overview.png new file mode 100644 index 0000000..04134dc Binary files /dev/null and b/assets/concept/biodocklab_handdrawn_system_overview.png differ diff --git a/backend/main.py b/backend/main.py index 6af4ee2..3795c5e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,35 +1,385 @@ -from fastapi import FastAPI from pathlib import Path import json -app = FastAPI(title="BioDockLab API", version="2.1.0") +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +ROOT_DIR = Path(__file__).resolve().parents[1] +DATA_DIR = ROOT_DIR / "sample_data" +CORE_DATA_DIR = ROOT_DIR / "data" / "sample" + +app = FastAPI( + title="BioDockLab API", + version="2.7.0", + description="Patient digital twin, vital sign, clinical decision support API", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://127.0.0.1:5173", + "http://127.0.0.1:5174", + "http://localhost:5173", + "http://localhost:5174", + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) -DATA_PATH = Path(__file__).resolve().parent.parent / "data" / "sample" / "bio_experiments.json" +def load_json(path: Path, fallback): + if not path.exists(): + return fallback -def load_data(): - with open(DATA_PATH, "r", encoding="utf-8") as f: - return json.load(f) + try: + with path.open("r", encoding="utf-8") as file: + return json.load(file) + except Exception: + return fallback @app.get("/") def root(): return { "project": "BioDockLab", - "version": "2.1", - "message": "Bio AI research software platform API" + "version": "2.7", + "message": "Patient digital twin hospital AI platform API", + } + + +@app.get("/health") +def health(): + return { + "status": "ok", + "service": "biodocklab-api", + "version": "2.7.0", } +@app.get("/roles") +def get_roles(): + return [ + { + "id": "patient", + "name": "Patient", + "description": "검사 결과와 설명 리포트를 확인하는 사용자", + "focus": "알 권리, 결과 이해, 상담 준비", + }, + { + "id": "nurse", + "name": "Nurse", + "description": "바이탈사인과 인수인계 정보를 확인하는 사용자", + "focus": "바이탈사인, 투약 전 확인, 상태 변화", + }, + { + "id": "doctor", + "name": "Doctor", + "description": "검사 결과와 위험도 평가를 기반으로 판단을 보조받는 사용자", + "focus": "진단 보조, 치료 방향, 위험도 해석", + }, + { + "id": "pharmacist", + "name": "Pharmacist", + "description": "처방, 약물상호작용, 유전자 적합성을 검토하는 사용자", + "focus": "처방 검토, 상호작용, 안전성", + }, + { + "id": "security", + "name": "Security Officer", + "description": "접근 로그와 민감정보 사용을 감사하는 사용자", + "focus": "접근 제어, 감사 로그, 보안 정책", + }, + ] + + +@app.get("/role-detail/{role_id}") +def get_role_detail(role_id: str): + role_details = { + "patient": { + "title": "Patient Right-to-Know View", + "summary": "환자가 자신의 검사 결과, 위험도, 설명 리포트를 이해할 수 있도록 제공하는 화면", + "data_scope": ["검사 결과 요약", "위험도 설명", "의료진 상담 준비 자료", "환자 친화형 리포트"], + "restricted": ["타 환자 정보", "내부 의료진 메모", "보안 로그"], + }, + "nurse": { + "title": "Nurse Vital Sign & Handoff View", + "summary": "간호사가 바이탈사인, 인수인계, 상태 변화를 빠르게 확인하는 화면", + "data_scope": ["바이탈사인", "투약 전 확인", "인수인계 메모", "상태 변화 알림"], + "restricted": ["관리자 보안 정책", "연구자 전용 실험 데이터"], + }, + "doctor": { + "title": "Doctor Decision Support View", + "summary": "의사가 검사 결과와 AI 위험도 분석을 기반으로 판단을 보조받는 화면", + "data_scope": ["검사 결과", "AI 위험도 평가", "치료 방향 후보", "환자 상담 자료"], + "restricted": ["원무 서류 처리 내역", "시스템 내부 보안 설정"], + }, + "security": { + "title": "Security Audit View", + "summary": "보안관리자가 접근 기록, 권한 위반, 민감정보 접근을 감사하는 화면", + "data_scope": ["접근 로그", "권한 정책", "위험 이벤트", "민감정보 접근 기록"], + "restricted": ["환자 친화형 설명 화면 조작", "실험 결과 임의 수정"], + }, + } + + return role_details.get( + role_id, + { + "title": "Unknown Role", + "summary": "등록되지 않은 역할이다.", + "data_scope": [], + "restricted": [], + }, + ) + + @app.get("/experiments") def get_experiments(): - return load_data() + fallback = [ + { + "id": "ORG-001", + "domain": "Organoid", + "sample": "intestinal organoid", + "condition": "growth factor A + drug candidate X", + "success_rate": 78, + "risk_level": "Medium", + }, + { + "id": "CFPS-001", + "domain": "CFPS", + "sample": "cell-free protein synthesis", + "condition": "enzyme mix B + amino acid pool", + "success_rate": 84, + "risk_level": "Low", + }, + { + "id": "DT-001", + "domain": "Digital Twin", + "sample": "virtual patient model", + "condition": "treatment response prediction", + "success_rate": 69, + "risk_level": "High", + }, + ] + + data = load_json(CORE_DATA_DIR / "bio_experiments.json", fallback) + + if isinstance(data, dict): + return data.get("experiments", fallback) + + return data -@app.get("/experiments/{experiment_id}") -def get_experiment(experiment_id: str): - experiments = load_data() +@app.get("/analysis") +def get_analysis(): + experiments = get_experiments() + results = [] + for exp in experiments: - if exp["id"] == experiment_id: - return exp - return {"error": "Experiment not found"} \ No newline at end of file + success_rate = exp.get("success_rate", 0) + risk_level = exp.get("risk_level", "Medium") + + if success_rate >= 80 and risk_level == "Low": + priority = "High Priority" + recommendation = "Proceed to validation report" + elif success_rate >= 70: + priority = "Review" + recommendation = "Compare with baseline and request secondary review" + else: + priority = "Needs Improvement" + recommendation = "Adjust experiment condition and rerun analysis" + + results.append( + { + "id": exp.get("id"), + "priority": priority, + "risk_level": risk_level, + "recommendation": recommendation, + } + ) + + return results + + +@app.get("/platform-summary") +def get_platform_summary(): + experiments = get_experiments() + analysis = get_analysis() + roles = get_roles() + + high_priority = sum(1 for item in analysis if item["priority"] == "High Priority") + avg_success = round( + sum(exp.get("success_rate", 0) for exp in experiments) / max(len(experiments), 1), + 1, + ) + + return { + "roles": len(roles), + "experiments": len(experiments), + "high_priority": high_priority, + "average_success_rate": avg_success, + "research_directions": ["Organoid", "Surgery AI", "Quantum Biocomputing", "Digital Twin", "CFPS"], + } + + +@app.get("/vital-signs") +def get_vital_signs(): + return load_json(DATA_DIR / "vital_signs.json", []) + + +@app.get("/patient-reports") +def get_patient_reports(): + return load_json(DATA_DIR / "patient_reports.json", []) + + +@app.get("/patient-reports/{patient_id}") +def get_patient_report(patient_id: str): + reports = get_patient_reports() + + for report in reports: + if report.get("patient_id") == patient_id: + return report + + return { + "patient_id": patient_id, + "title": "Report Not Found", + "summary": "해당 환자 설명 리포트가 없다.", + "explanation": "", + "right_to_know": [], + } + + +@app.get("/hospital-summary") +def get_hospital_summary(): + vital_signs = get_vital_signs() + watch_count = sum(1 for item in vital_signs if item.get("status") == "Watch") + stable_count = sum(1 for item in vital_signs if item.get("status") == "Stable") + + avg_spo2 = round( + sum(item.get("spo2", 0) for item in vital_signs) / max(len(vital_signs), 1), + 1, + ) + + return { + "patients": len(vital_signs), + "stable": stable_count, + "watch": watch_count, + "average_spo2": avg_spo2, + } + + +@app.get("/doctor-decisions") +def get_doctor_decisions(): + return load_json(DATA_DIR / "doctor_decisions.json", []) + + +@app.get("/doctor-decisions/{patient_id}") +def get_doctor_decision(patient_id: str): + decisions = get_doctor_decisions() + + for decision in decisions: + if decision.get("patient_id") == patient_id: + return decision + + return { + "patient_id": patient_id, + "risk_level": "Unknown", + "clinical_summary": "해당 환자 판단 보조 데이터가 없다.", + "decision_support": [], + "next_action": "No action", + } + + +@app.get("/clinical-timeline") +def get_clinical_timeline(): + return load_json(DATA_DIR / "clinical_timeline.json", []) + + +@app.get("/clinical-timeline/{patient_id}") +def get_clinical_timeline_by_patient(patient_id: str): + timeline = get_clinical_timeline() + return [item for item in timeline if item.get("patient_id") == patient_id] + + +@app.get("/hospital-audit-events") +def get_hospital_audit_events(): + return load_json(DATA_DIR / "hospital_audit_events.json", []) + + +@app.get("/hospital-audit-summary") +def get_hospital_audit_summary(): + events = get_hospital_audit_events() + allowed = sum(1 for item in events if item.get("result") == "Allowed") + denied = sum(1 for item in events if item.get("result") == "Denied") + + return { + "events": len(events), + "allowed": allowed, + "denied": denied, + } + + +@app.get("/digital-twin-findings") +def get_digital_twin_findings(): + fallback = [ + { + "patient_id": "P-002", + "model_type": "human-trauma-digital-twin", + "overall_risk": "High", + "summary": "혈압 저하, 심박수 증가, 산소포화도 저하가 함께 관찰되어 내부 출혈 가능성을 시각적으로 추적한다.", + "findings": [ + { + "id": "BLEED-ABD-001", + "region": "Left Upper Abdomen", + "label": "복부 상부 출혈 의심", + "severity": "High", + "description": "저혈압과 빈맥이 동반되어 복부 내부 출혈 가능성이 있다.", + "x": 50, + "y": 44, + }, + { + "id": "BLEED-PEL-002", + "region": "Pelvic Region", + "label": "골반 부위 출혈 의심", + "severity": "High", + "description": "골반 부위 손상 또는 내부 출혈 가능성을 모니터링한다.", + "x": 50, + "y": 58, + }, + { + "id": "INJ-FEM-003", + "region": "Left Femur", + "label": "좌측 대퇴부 손상", + "severity": "Medium", + "description": "대퇴부 손상과 출혈 가능성을 함께 추적한다.", + "x": 42, + "y": 75, + }, + ], + "patient_explanation": [ + "현재 몸 안쪽에서 출혈이 의심되는 위치를 모형으로 보여준다.", + "빨간색은 의료진이 빠르게 확인해야 하는 위험 위치를 의미한다.", + "이 정보는 환자가 자신의 상태를 이해하고 의료진에게 질문할 수 있도록 돕는다.", + ], + } + ] + + return load_json(DATA_DIR / "digital_twin_findings.json", fallback) + + +@app.get("/digital-twin-findings/{patient_id}") +def get_digital_twin_finding_by_patient(patient_id: str): + twins = get_digital_twin_findings() + + for twin in twins: + if twin.get("patient_id") == patient_id: + return twin + + return { + "patient_id": patient_id, + "model_type": "unknown", + "overall_risk": "Unknown", + "summary": "해당 환자의 디지털 트윈 데이터가 없다.", + "findings": [], + "patient_explanation": [], + } diff --git a/docs/architecture/Handdrawn_System_Concept.md b/docs/architecture/Handdrawn_System_Concept.md new file mode 100644 index 0000000..97c1438 --- /dev/null +++ b/docs/architecture/Handdrawn_System_Concept.md @@ -0,0 +1,92 @@ +# BioDockLab Hand-Drawn System Concept + +BioDockLab is a role-based medical and bio AI research platform that connects medical data, biological experiment records, AI analysis, simulation, and explainable reports. + +This document contains early hand-drawn system sketches for organizing the platform structure before full implementation. + +--- + +## 1. System Overview + +![BioDockLab Handdrawn System Overview](../../assets/concept/biodocklab_handdrawn_system_overview.png) + +The first concept sketch shows the overall BioDockLab architecture. + +Core structure: + +```text +Role-Based Access +→ Data Input +→ AI Analysis +→ Risk / Priority Scoring +→ Simulation +→ Report +→ Explainable Output +The purpose is to connect patient-understandable data, medical staff decision support, and reusable research data into one software platform. + +2. Medical / Bio AI Concept Map + +The second concept sketch focuses on the medical and bio AI direction of BioDockLab. + +Key components: + +Vital sign data +Experiment data +AI risk scoring +Response prediction +Digital twin simulation +Organoid / 3D model expansion +CFPS expansion +Role-based access +Future medical technology tracking +3. Developer Position + +BioDockLab is not designed as a clinical replacement system. + +The developer position of this project is to design the software structure that connects: + +medical data +biological research data +AI analysis +simulation logic +report automation +explainable user interfaces + +The project focuses on building a platform architecture where medical and biological data can be structured, analyzed, simulated, and explained. + +4. Design Principles + +BioDockLab follows four design principles. + +Patient-understandable data +Medical staff decision support +Research data reusability +Expandable software architecture + +The core idea is: + +Data becomes meaningful when it can be understood, connected, and reused. + +5. Module Direction +Vital Sign Module +→ Patient status summary / nurse handoff / basic monitoring + +Experiment Module +→ experiment condition / success rate / risk level / notes + +AI Analysis Module +→ risk classification / priority scoring / response prediction + +Simulation Module +→ digital twin / organoid / CFPS simulation + +Report Module +→ Markdown report / PDF report / summary output + +Security Module +→ role-based access / audit log / sensitive data control +6. Status + +These sketches are early architecture notes. + +They are used to guide the development direction of BioDockLab before implementation is fully connected across frontend, backend, AI analysis, simulation, and reporting layers. diff --git a/index.html b/index.html new file mode 100644 index 0000000..f1344a3 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + BioDockLab v2.7 + + +
+ + + diff --git a/package-lock.json b/package-lock.json index 092b8de..b6f6070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,418 @@ { - "name": "BioDockLab", + "name": "biodocklab", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "biodocklab", + "version": "2.7.0", "license": "UNLICENSED", "dependencies": { "lucide-react": "^1.16.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "recharts": "^3.8.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.0.0", + "typescript": "^5.0.0", + "vite": "^8.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@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/@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.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "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.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@reduxjs/toolkit": { @@ -46,106 +451,540 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "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-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-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-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-color": "*" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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-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==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-time": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-path": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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-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/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/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-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-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-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-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-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-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/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-color": { @@ -166,145 +1005,634 @@ "node": ">=12" } }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://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-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "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/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "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.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "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/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/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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" - }, + "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" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", + "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" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, + "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" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, + "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" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, + "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" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "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": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" + "yallist": "^3.0.2" } }, - "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==", + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", "license": "ISC", - "engines": { - "node": ">=12" + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "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/es-toolkit": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", - "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "node_modules/nanoid": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", + "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "engines": { + "node": ">=18" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", + "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/lucide-react": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", - "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, "node_modules/react": { @@ -312,7 +1640,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -322,7 +1649,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -360,6 +1686,16 @@ } } }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/recharts": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", @@ -411,12 +1747,72 @@ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "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", - "peer": true + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/tiny-invariant": { "version": "1.3.3", @@ -424,6 +1820,76 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/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/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", @@ -454,6 +1920,91 @@ "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" } } } diff --git a/package.json b/package.json index b9ab6d8..20fda12 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,23 @@ { + "name": "biodocklab", + "version": "2.7.0", + "private": true, + "license": "UNLICENSED", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1 --port 4173" + }, "dependencies": { "lucide-react": "^1.16.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "recharts": "^3.8.1" }, - "private": true, - "license": "UNLICENSED" -} \ No newline at end of file + "devDependencies": { + "@vitejs/plugin-react": "^5.0.0", + "typescript": "^5.0.0", + "vite": "^8.0.0" + } +} diff --git a/sample_data/clinical_timeline.json b/sample_data/clinical_timeline.json new file mode 100644 index 0000000..1d381b7 --- /dev/null +++ b/sample_data/clinical_timeline.json @@ -0,0 +1,50 @@ +[ + { + "time": "08:00", + "patient_id": "P-001", + "type": "Routine Check", + "role": "Nurse", + "title": "정기 바이탈 확인", + "description": "주요 바이탈사인이 안정 범위에 있어 정기 관찰을 유지한다." + }, + { + "time": "08:10", + "patient_id": "P-002", + "type": "Vital Sign Alert", + "role": "Nurse", + "title": "산소포화도 저하 감지", + "description": "SpO₂ 92%, 호흡수 24/min으로 주의 관찰 기준에 진입했다." + }, + { + "time": "08:18", + "patient_id": "P-002", + "type": "Nurse Handoff", + "role": "Nurse", + "title": "간호사 재확인 요청", + "description": "호흡 불편감 문진 및 산소포화도 추세 확인이 필요하다." + }, + { + "time": "08:27", + "patient_id": "P-002", + "type": "Doctor Decision", + "role": "Doctor", + "title": "Clinical review required", + "description": "담당 의료진 확인과 추가 모니터링이 필요하다." + }, + { + "time": "08:35", + "patient_id": "P-002", + "type": "Patient Report", + "role": "Patient", + "title": "환자 설명 리포트 생성", + "description": "환자가 현재 상태를 이해할 수 있도록 쉬운 설명 리포트를 제공한다." + }, + { + "time": "08:45", + "patient_id": "P-003", + "type": "Recovery Check", + "role": "Nurse", + "title": "회복 경과 관찰", + "description": "바이탈 안정 상태를 유지하고 있으며 정기 관찰을 지속한다." + } +] diff --git a/sample_data/digital_twin_findings.json b/sample_data/digital_twin_findings.json new file mode 100644 index 0000000..abdc106 --- /dev/null +++ b/sample_data/digital_twin_findings.json @@ -0,0 +1,71 @@ +[ + { + "patient_id": "P-001", + "model_type": "prototype-risk-map", + "overall_risk": "Low", + "summary": "현재 신체 위험 위치 시각화에서 급성 출혈 의심 부위는 낮게 표시된다.", + "findings": [ + { + "id": "MON-001", + "region": "Chest", + "label": "정기 관찰", + "severity": "Low", + "description": "현재 급성 위험 소견은 낮으며 정기 관찰을 유지한다.", + "x": 50, + "y": 38 + } + ] + }, + { + "patient_id": "P-002", + "model_type": "prototype-risk-map", + "overall_risk": "High", + "summary": "실제 3D 인체 모델이 아니라, 바이탈사인 기반 위험 위치를 시각화하는 초기 디지털 트윈 모형입니다.", + "findings": [ + { + "id": "BLEED-ABD-001", + "region": "Abdomen", + "label": "복부 출혈 의심", + "severity": "High", + "description": "저혈압과 빈맥이 동반되어 내부 출혈 가능성을 표시합니다.", + "x": 50, + "y": 47 + }, + { + "id": "BLEED-PEL-002", + "region": "Pelvic Region", + "label": "골반 부위 출혈 의심", + "severity": "High", + "description": "골반 손상 가능성을 함께 추적합니다.", + "x": 50, + "y": 59 + }, + { + "id": "INJ-FEM-003", + "region": "Left Femur", + "label": "대퇴부 손상 가능성", + "severity": "Medium", + "description": "대퇴부 손상과 출혈 가능성을 추적합니다.", + "x": 43, + "y": 76 + } + ] + }, + { + "patient_id": "P-003", + "model_type": "prototype-risk-map", + "overall_risk": "Low", + "summary": "현재 위험 위치 시각화에서 이상 징후는 낮으며 회복 경과를 관찰한다.", + "findings": [ + { + "id": "MON-003", + "region": "General", + "label": "회복 관찰", + "severity": "Low", + "description": "현재 주요 위험 위치는 낮게 표시되며 회복 경과를 확인한다.", + "x": 50, + "y": 52 + } + ] + } +] diff --git a/sample_data/doctor_decisions.json b/sample_data/doctor_decisions.json new file mode 100644 index 0000000..69be51b --- /dev/null +++ b/sample_data/doctor_decisions.json @@ -0,0 +1,36 @@ +[ + { + "patient_id": "P-001", + "risk_level": "Low", + "clinical_summary": "현재 바이탈사인은 안정적이며 급성 악화 신호는 낮다.", + "decision_support": [ + "정기 관찰 유지", + "환자 설명 리포트 제공", + "추가 검사 필요성은 낮음" + ], + "next_action": "Routine monitoring" + }, + { + "patient_id": "P-002", + "risk_level": "Medium-High", + "clinical_summary": "산소포화도 저하와 호흡수 증가가 관찰되어 의료진 확인이 필요하다.", + "decision_support": [ + "간호사 바이탈 재확인", + "산소포화도 추세 확인", + "호흡 불편감 문진", + "필요 시 담당의 알림" + ], + "next_action": "Clinical review required" + }, + { + "patient_id": "P-003", + "risk_level": "Low", + "clinical_summary": "바이탈이 안정 범위에 있으며 정기 관찰을 유지할 수 있다.", + "decision_support": [ + "정기 관찰 유지", + "환자 상태 변화 시 재평가", + "퇴원 또는 회복 계획 검토 가능" + ], + "next_action": "Continue observation" + } +] diff --git a/sample_data/hospital_audit_events.json b/sample_data/hospital_audit_events.json new file mode 100644 index 0000000..7a01492 --- /dev/null +++ b/sample_data/hospital_audit_events.json @@ -0,0 +1,68 @@ +[ + { + "id": "AUD-001", + "time": "08:02", + "actor": "Nurse", + "actor_name": "간호사 A", + "patient_id": "P-001", + "action": "Viewed vital signs", + "data_scope": "Vital Signs", + "result": "Allowed", + "reason": "정기 바이탈 확인" + }, + { + "id": "AUD-002", + "time": "08:10", + "actor": "Nurse", + "actor_name": "간호사 B", + "patient_id": "P-002", + "action": "Viewed vital signs", + "data_scope": "Vital Signs", + "result": "Allowed", + "reason": "산소포화도 저하 확인" + }, + { + "id": "AUD-003", + "time": "08:27", + "actor": "Doctor", + "actor_name": "담당의 A", + "patient_id": "P-002", + "action": "Viewed doctor decision support", + "data_scope": "Doctor Decision", + "result": "Allowed", + "reason": "Clinical review required" + }, + { + "id": "AUD-004", + "time": "08:35", + "actor": "Patient", + "actor_name": "Sample Patient B", + "patient_id": "P-002", + "action": "Viewed patient explanation report", + "data_scope": "Patient Report", + "result": "Allowed", + "reason": "환자 알 권리 리포트 확인" + }, + { + "id": "AUD-005", + "time": "08:39", + "actor": "Researcher", + "actor_name": "연구자 계정", + "patient_id": "P-002", + "action": "Tried to access identifiable patient report", + "data_scope": "Patient Report", + "result": "Denied", + "reason": "연구자 권한은 식별 가능한 환자 리포트 접근 불가" + }, + { + "id": "AUD-006", + "time": "08:45", + "actor": "Security Officer", + "actor_name": "보안관리자", + "patient_id": "P-002", + "action": "Reviewed access log", + "data_scope": "Audit Log", + "result": "Allowed", + "reason": "민감정보 접근 이력 점검" + } +] diff --git a/sample_data/patient_reports.json b/sample_data/patient_reports.json new file mode 100644 index 0000000..100b9c5 --- /dev/null +++ b/sample_data/patient_reports.json @@ -0,0 +1,35 @@ +[ + { + "patient_id": "P-001", + "title": "안정 상태 설명 리포트", + "summary": "현재 주요 바이탈사인은 안정 범위에 있으며 급성 위험 신호는 낮다.", + "explanation": "심박수, 혈압, 체온, 산소포화도가 안정적으로 유지되고 있어 정기 관찰을 유지한다.", + "right_to_know": [ + "현재 바이탈사인이 안정 범위에 있음을 확인할 수 있다.", + "정기 관찰이 필요한 이유를 이해할 수 있다.", + "의료진 상담 전에 현재 상태를 쉽게 파악할 수 있다." + ] + }, + { + "patient_id": "P-002", + "title": "주의 관찰 설명 리포트", + "summary": "산소포화도 저하와 호흡수 증가가 관찰되어 의료진 확인이 필요하다.", + "explanation": "산소포화도와 호흡수가 정상 범위에서 벗어날 가능성이 있어 간호사 또는 담당 의료진의 재확인이 필요하다.", + "right_to_know": [ + "산소포화도가 낮아졌다는 의미를 이해할 수 있다.", + "호흡수가 증가했을 때 어떤 확인이 필요한지 알 수 있다.", + "의료진에게 현재 증상과 불편감을 정확히 설명할 수 있다." + ] + }, + { + "patient_id": "P-003", + "title": "정기 관찰 설명 리포트", + "summary": "현재 바이탈은 안정 범위에 있으며 정기 관찰을 유지할 수 있다.", + "explanation": "특별한 급성 위험 신호는 낮지만, 회복 경과를 보기 위해 정기적인 바이탈 확인을 유지한다.", + "right_to_know": [ + "현재 상태가 안정적이라는 점을 확인할 수 있다.", + "왜 계속 관찰이 필요한지 이해할 수 있다.", + "상태 변화가 있을 때 어떤 점을 말해야 하는지 알 수 있다." + ] + } +] diff --git a/src/app/main.jsx b/src/app/main.jsx new file mode 100644 index 0000000..1a1631a --- /dev/null +++ b/src/app/main.jsx @@ -0,0 +1,518 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; +import "./style.css"; + +const API_BASE = "http://127.0.0.1:8000"; + +const fallbackVitals = [ + { + patient_id: "P-001", + name: "Sample Patient A", + age: 42, + heart_rate: 88, + blood_pressure: "124/82", + temperature: 36.8, + spo2: 97, + respiratory_rate: 18, + status: "Stable", + note: "수술 후 회복 관찰 중. 산소포화도 안정적." + }, + { + patient_id: "P-002", + name: "Sample Patient B", + age: 67, + heart_rate: 112, + blood_pressure: "148/92", + temperature: 37.9, + spo2: 92, + respiratory_rate: 24, + status: "Watch", + note: "호흡수 증가 및 산소포화도 저하. 간호사 재확인 필요." + }, + { + patient_id: "P-003", + name: "Sample Patient C", + age: 55, + heart_rate: 76, + blood_pressure: "118/78", + temperature: 36.5, + spo2: 98, + respiratory_rate: 16, + status: "Stable", + note: "바이탈 안정. 정기 관찰 유지." + } +]; + +const fallbackReports = [ + { + patient_id: "P-002", + title: "주의 관찰 설명 리포트", + summary: "산소포화도 저하와 호흡수 증가가 관찰되어 의료진 확인이 필요하다.", + explanation: "환자가 자신의 상태를 이해할 수 있도록 위험 신호를 쉬운 언어로 설명합니다.", + right_to_know: [ + "산소포화도가 낮아졌다는 의미를 이해할 수 있다.", + "호흡수가 증가했을 때 어떤 확인이 필요한지 알 수 있다.", + "의료진에게 현재 증상과 불편감을 정확히 설명할 수 있다." + ] + } +]; + +const fallbackDecisions = [ + { + patient_id: "P-002", + risk_level: "Medium-High", + clinical_summary: "산소포화도 저하와 호흡수 증가가 관찰되어 의료진 확인이 필요하다.", + decision_support: [ + "간호사 바이탈 재확인", + "산소포화도 추세 확인", + "호흡 불편감 문진", + "필요 시 담당의 알림" + ], + next_action: "Clinical review required" + } +]; + +const fallbackTimeline = [ + { + time: "08:10", + type: "Vital Sign Alert", + role: "Nurse", + title: "산소포화도 저하 감지", + description: "SpO₂ 92%, 호흡수 24/min으로 주의 관찰 기준에 진입했다." + }, + { + time: "08:18", + type: "Nurse Handoff", + role: "Nurse", + title: "간호사 재확인 요청", + description: "호흡 불편감 문진 및 산소포화도 추세 확인이 필요하다." + }, + { + time: "08:27", + type: "Doctor Decision", + role: "Doctor", + title: "Clinical review required", + description: "담당 의료진 확인과 추가 모니터링이 필요하다." + }, + { + time: "08:35", + type: "Patient Report", + role: "Patient", + title: "환자 설명 리포트 생성", + description: "환자가 현재 상태를 이해할 수 있도록 쉬운 설명 리포트를 제공한다." + } +]; + + +const fallbackAuditEvents = [ + { + id: "AUD-001", + time: "08:02", + actor: "Nurse", + actor_name: "간호사 A", + patient_id: "P-001", + action: "Viewed vital signs", + data_scope: "Vital Signs", + result: "Allowed", + reason: "정기 바이탈 확인" + }, + { + id: "AUD-005", + time: "08:39", + actor: "Researcher", + actor_name: "연구자 계정", + patient_id: "P-002", + action: "Tried to access identifiable patient report", + data_scope: "Patient Report", + result: "Denied", + reason: "연구자 권한은 식별 가능한 환자 리포트 접근 불가" + } +]; + +const fallbackTwin = { + patient_id: "P-002", + model_type: "prototype-risk-map", + overall_risk: "High", + summary: "실제 3D 인체 모델이 아니라, 바이탈사인 기반 위험 위치를 시각화하는 초기 디지털 트윈 모형입니다.", + findings: [ + { + id: "BLEED-ABD-001", + label: "복부 출혈 의심", + region: "Abdomen", + severity: "High", + description: "저혈압과 빈맥이 동반되어 내부 출혈 가능성을 표시합니다.", + x: 50, + y: 47 + }, + { + id: "BLEED-PEL-002", + label: "골반 부위 출혈 의심", + region: "Pelvic Region", + severity: "High", + description: "골반 손상 가능성을 함께 추적합니다.", + x: 50, + y: 59 + }, + { + id: "INJ-FEM-003", + label: "대퇴부 손상 가능성", + region: "Left Femur", + severity: "Medium", + description: "대퇴부 손상과 출혈 가능성을 추적합니다.", + x: 43, + y: 76 + } + ] +}; + +async function getJson(path, fallback) { + try { + const res = await fetch(`${API_BASE}${path}`); + if (!res.ok) return fallback; + return await res.json(); + } catch { + return fallback; + } +} + +function App() { + const [apiStatus, setApiStatus] = useState("fallback mode"); + const [vitals, setVitals] = useState(fallbackVitals); + const [reports, setReports] = useState(fallbackReports); + const [decisions, setDecisions] = useState(fallbackDecisions); + const [allTimeline, setAllTimeline] = useState(fallbackTimeline); + const [digitalTwin, setDigitalTwin] = useState(fallbackTwin); + const [auditEvents, setAuditEvents] = useState(fallbackAuditEvents); + const [selectedPatientId, setSelectedPatientId] = useState("P-002"); + const [roleMode, setRoleMode] = useState("dashboard"); + + useEffect(() => { + async function load() { + const [health, vitalData, reportData, decisionData, timelineData, twinData, auditData] = + await Promise.all([ + getJson("/health", null), + getJson("/vital-signs", fallbackVitals), + getJson("/patient-reports", fallbackReports), + getJson("/doctor-decisions", fallbackDecisions), + getJson("/clinical-timeline", fallbackTimeline), + getJson("/digital-twin-findings/P-002", fallbackTwin), + getJson("/hospital-audit-events", fallbackAuditEvents) + ]); + + setApiStatus(health ? "connected" : "fallback mode"); + setVitals(vitalData.length ? vitalData : fallbackVitals); + setReports(reportData.length ? reportData : fallbackReports); + setDecisions(decisionData.length ? decisionData : fallbackDecisions); + setTimeline(timelineData.length ? timelineData : fallbackTimeline); + setDigitalTwin(twinData?.findings ? twinData : fallbackTwin); + setAuditEvents(auditData.length ? auditData : fallbackAuditEvents); + } + + load(); + }, []); + + const selectedPatient = useMemo(() => { + return vitals.find((item) => item.patient_id === selectedPatientId) || vitals[0]; + }, [vitals, selectedPatientId]); + + const selectedReport = useMemo(() => { + return reports.find((item) => item.patient_id === selectedPatientId) || reports[0]; + }, [reports, selectedPatientId]); + + const selectedDecision = useMemo(() => { + return decisions.find((item) => item.patient_id === selectedPatientId) || decisions[0]; + }, [decisions, selectedPatientId]); + + const selectedTimeline = useMemo(() => { + const filtered = allTimeline.filter((item) => item.patient_id === selectedPatientId); + return filtered.length ? filtered : fallbackTimeline.filter((item) => item.patient_id === selectedPatientId); + }, [allTimeline, selectedPatientId]); + + const selectedTwin = useMemo(() => { + if (digitalTwin?.patient_id === selectedPatientId) return digitalTwin; + const fallbackList = Array.isArray(fallbackTwin) ? fallbackTwin : [fallbackTwin]; + return fallbackList.find((item) => item.patient_id === selectedPatientId) || digitalTwin; + }, [digitalTwin, selectedPatientId]); + + const selectedAuditEvents = useMemo(() => { + return auditEvents.filter((item) => item.patient_id === selectedPatientId); + }, [auditEvents, selectedPatientId]); + + const deniedAuditCount = auditEvents.filter((item) => item.result === "Denied").length; + + const watchCount = vitals.filter((item) => item.status === "Watch").length; + const reportsReady = reports.length; + const doctorReviews = decisions.filter((item) => item.risk_level !== "Low").length; + + return ( +
+ + +
+
+
+ BioDockLab v3 · Hospital Main Dashboard +

병원 역할 기반 의료 데이터 플랫폼

+

+ 환자, 간호사, 의사, 보안관리자가 각자 필요한 의료 데이터를 확인하고 + 환자의 알 권리와 의료진 판단을 연결하는 제품 화면입니다. +

+
+ +
+ Backend API + {apiStatus} + v2 data science core +
+
+ + +
+
+ Current Role View + + {roleMode === "dashboard" && "Hospital Dashboard"} + {roleMode === "patient" && "Patient Right-to-Know View"} + {roleMode === "nurse" && "Nurse Vital Monitoring View"} + {roleMode === "doctor" && "Doctor Decision Support View"} + {roleMode === "security" && "Security Audit View"} + +

+ {roleMode === "dashboard" && "전체 병원 운영 현황과 환자 상태를 한 화면에서 확인합니다."} + {roleMode === "patient" && "환자가 본인의 상태와 검사 결과를 이해할 수 있도록 쉬운 설명을 우선 표시합니다."} + {roleMode === "nurse" && "간호사가 바이탈사인, 주의 환자, 인수인계 흐름을 빠르게 확인합니다."} + {roleMode === "doctor" && "의사가 위험도, 판단 보조, 디지털 트윈 위험 위치를 중심으로 확인합니다."} + {roleMode === "security" && "보안관리자가 접근 기록과 민감정보 접근 흐름을 확인합니다."} +

+
+
+ +
+
+ Active Patients + {vitals.length} +

현재 관리 중인 환자

+
+
+ Watch Cases + {watchCount} +

주의 관찰 필요

+
+
+ Doctor Review + {doctorReviews} +

의사 확인 필요

+
+
+ Reports Ready + {reportsReady} +

환자 설명 리포트

+
+
+ Denied Access + {deniedAuditCount} +

권한 차단 이벤트

+
+
+ +
+
+
+

Patient List

+ 오늘 모니터링 대상 +
+ + {vitals.map((patient) => ( + + ))} +
+ +
+
+
+ {selectedPatient.patient_id} +

{selectedPatient.name}

+

{selectedPatient.note}

+
+ + {selectedPatient.status} + +
+ +
+
+ Heart Rate + {selectedPatient.heart_rate} + bpm +
+
+ Blood Pressure + {selectedPatient.blood_pressure} + mmHg +
+
+ Temperature + {selectedPatient.temperature}℃ + body temp +
+
+ SpO₂ + {selectedPatient.spo2}% + oxygen +
+
+ +
+ Doctor Decision Support +

{selectedDecision?.next_action}

+

{selectedDecision?.clinical_summary}

+
    + {selectedDecision?.decision_support?.map((item) => ( +
  • {item}
  • + ))} +
+
+
+ + +
+ +
+
+
+

Clinical Timeline

+ 바이탈 → 판단 → 설명 리포트 흐름 +
+ + {selectedTimeline.map((item) => ( +
+ {item.time} +
+ {item.type} · {item.role} +

{item.title}

+
+
+ ))} +
+ + +
+
+

Security Audit Log

+ 선택 환자 기준 접근 기록 +
+ + {selectedAuditEvents.length === 0 && ( +

선택 환자의 감사 로그가 없습니다.

+ )} + + {selectedAuditEvents.map((event) => ( +
+
+ {event.time} + {event.actor} · {event.actor_name} +
+
+

{event.action}

+

{event.data_scope} · {event.reason}

+
+ {event.result} +
+ ))} +
+ +
+
+

Digital Twin Findings

+ 의료진 확인용 위험 부위 요약 +
+ + {selectedTwin.findings.map((finding) => ( +
+
+ {finding.label} + {finding.region} +

{finding.description}

+
+ + {finding.severity} + +
+ ))} +
+
+
+
+ ); +} + +createRoot(document.getElementById("root")).render(); diff --git a/src/app/main.v3.backup.jsx b/src/app/main.v3.backup.jsx new file mode 100644 index 0000000..40a5994 --- /dev/null +++ b/src/app/main.v3.backup.jsx @@ -0,0 +1,432 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; +import "./style.css"; + +const API_BASE = "http://127.0.0.1:8000"; + +const fallbackVitals = [ + { + patient_id: "P-001", + name: "Sample Patient A", + age: 42, + heart_rate: 88, + blood_pressure: "124/82", + temperature: 36.8, + spo2: 97, + respiratory_rate: 18, + status: "Stable", + note: "수술 후 회복 관찰 중. 산소포화도 안정적." + }, + { + patient_id: "P-002", + name: "Sample Patient B", + age: 67, + heart_rate: 112, + blood_pressure: "148/92", + temperature: 37.9, + spo2: 92, + respiratory_rate: 24, + status: "Watch", + note: "호흡수 증가 및 산소포화도 저하. 간호사 재확인 필요." + }, + { + patient_id: "P-003", + name: "Sample Patient C", + age: 55, + heart_rate: 76, + blood_pressure: "118/78", + temperature: 36.5, + spo2: 98, + respiratory_rate: 16, + status: "Stable", + note: "바이탈 안정. 정기 관찰 유지." + } +]; + +const fallbackReports = [ + { + patient_id: "P-002", + title: "주의 관찰 설명 리포트", + summary: "산소포화도 저하와 호흡수 증가가 관찰되어 의료진 확인이 필요하다.", + explanation: "환자가 자신의 상태를 이해할 수 있도록 위험 신호를 쉬운 언어로 설명합니다.", + right_to_know: [ + "산소포화도가 낮아졌다는 의미를 이해할 수 있다.", + "호흡수가 증가했을 때 어떤 확인이 필요한지 알 수 있다.", + "의료진에게 현재 증상과 불편감을 정확히 설명할 수 있다." + ] + } +]; + +const fallbackDecisions = [ + { + patient_id: "P-002", + risk_level: "Medium-High", + clinical_summary: "산소포화도 저하와 호흡수 증가가 관찰되어 의료진 확인이 필요하다.", + decision_support: [ + "간호사 바이탈 재확인", + "산소포화도 추세 확인", + "호흡 불편감 문진", + "필요 시 담당의 알림" + ], + next_action: "Clinical review required" + } +]; + +const fallbackTimeline = [ + { + time: "08:10", + type: "Vital Sign Alert", + role: "Nurse", + title: "산소포화도 저하 감지", + description: "SpO₂ 92%, 호흡수 24/min으로 주의 관찰 기준에 진입했다." + }, + { + time: "08:18", + type: "Nurse Handoff", + role: "Nurse", + title: "간호사 재확인 요청", + description: "호흡 불편감 문진 및 산소포화도 추세 확인이 필요하다." + }, + { + time: "08:27", + type: "Doctor Decision", + role: "Doctor", + title: "Clinical review required", + description: "담당 의료진 확인과 추가 모니터링이 필요하다." + }, + { + time: "08:35", + type: "Patient Report", + role: "Patient", + title: "환자 설명 리포트 생성", + description: "환자가 현재 상태를 이해할 수 있도록 쉬운 설명 리포트를 제공한다." + } +]; + +const fallbackTwin = { + patient_id: "P-002", + model_type: "prototype-risk-map", + overall_risk: "High", + summary: "실제 3D 인체 모델이 아니라, 바이탈사인 기반 위험 위치를 시각화하는 초기 디지털 트윈 모형입니다.", + findings: [ + { + id: "BLEED-ABD-001", + label: "복부 출혈 의심", + region: "Abdomen", + severity: "High", + description: "저혈압과 빈맥이 동반되어 내부 출혈 가능성을 표시합니다.", + x: 50, + y: 47 + }, + { + id: "BLEED-PEL-002", + label: "골반 부위 출혈 의심", + region: "Pelvic Region", + severity: "High", + description: "골반 손상 가능성을 함께 추적합니다.", + x: 50, + y: 59 + }, + { + id: "INJ-FEM-003", + label: "대퇴부 손상 가능성", + region: "Left Femur", + severity: "Medium", + description: "대퇴부 손상과 출혈 가능성을 추적합니다.", + x: 43, + y: 76 + } + ] +}; + +async function getJson(path, fallback) { + try { + const res = await fetch(`${API_BASE}${path}`); + if (!res.ok) return fallback; + return await res.json(); + } catch { + return fallback; + } +} + +function App() { + const [apiStatus, setApiStatus] = useState("fallback mode"); + const [vitals, setVitals] = useState(fallbackVitals); + const [reports, setReports] = useState(fallbackReports); + const [decisions, setDecisions] = useState(fallbackDecisions); + const [allTimeline, setAllTimeline] = useState(fallbackTimeline); + const [digitalTwin, setDigitalTwin] = useState(fallbackTwin); + const [selectedPatientId, setSelectedPatientId] = useState("P-002"); + + useEffect(() => { + async function load() { + const [health, vitalData, reportData, decisionData, timelineData, twinData] = + await Promise.all([ + getJson("/health", null), + getJson("/vital-signs", fallbackVitals), + getJson("/patient-reports", fallbackReports), + getJson("/doctor-decisions", fallbackDecisions), + getJson("/clinical-timeline", fallbackTimeline), + getJson("/digital-twin-findings/P-002", fallbackTwin) + ]); + + setApiStatus(health ? "connected" : "fallback mode"); + setVitals(vitalData.length ? vitalData : fallbackVitals); + setReports(reportData.length ? reportData : fallbackReports); + setDecisions(decisionData.length ? decisionData : fallbackDecisions); + setTimeline(timelineData.length ? timelineData : fallbackTimeline); + setDigitalTwin(twinData?.findings ? twinData : fallbackTwin); + } + + load(); + }, []); + + const selectedPatient = useMemo(() => { + return vitals.find((item) => item.patient_id === selectedPatientId) || vitals[0]; + }, [vitals, selectedPatientId]); + + const selectedReport = useMemo(() => { + return reports.find((item) => item.patient_id === selectedPatientId) || reports[0]; + }, [reports, selectedPatientId]); + + const selectedDecision = useMemo(() => { + return decisions.find((item) => item.patient_id === selectedPatientId) || decisions[0]; + }, [decisions, selectedPatientId]); + + const selectedTimeline = useMemo(() => { + const filtered = allTimeline.filter((item) => item.patient_id === selectedPatientId); + return filtered.length ? filtered : fallbackTimeline.filter((item) => item.patient_id === selectedPatientId); + }, [allTimeline, selectedPatientId]); + + const selectedTwin = useMemo(() => { + if (digitalTwin?.patient_id === selectedPatientId) return digitalTwin; + const fallbackList = Array.isArray(fallbackTwin) ? fallbackTwin : [fallbackTwin]; + return fallbackList.find((item) => item.patient_id === selectedPatientId) || digitalTwin; + }, [digitalTwin, selectedPatientId]); + + const watchCount = vitals.filter((item) => item.status === "Watch").length; + const reportsReady = reports.length; + const doctorReviews = decisions.filter((item) => item.risk_level !== "Low").length; + + return ( +
+ + +
+
+
+ BioDockLab v3 · Hospital Main Dashboard +

병원 역할 기반 의료 데이터 플랫폼

+

+ 환자, 간호사, 의사, 보안관리자가 각자 필요한 의료 데이터를 확인하고 + 환자의 알 권리와 의료진 판단을 연결하는 제품 화면입니다. +

+
+ +
+ Backend API + {apiStatus} + v2 data science core +
+
+ +
+
+ Active Patients + {vitals.length} +

현재 관리 중인 환자

+
+
+ Watch Cases + {watchCount} +

주의 관찰 필요

+
+
+ Doctor Review + {doctorReviews} +

의사 확인 필요

+
+
+ Reports Ready + {reportsReady} +

환자 설명 리포트

+
+
+ +
+
+
+

Patient List

+ 오늘 모니터링 대상 +
+ + {vitals.map((patient) => ( + + ))} +
+ +
+
+
+ {selectedPatient.patient_id} +

{selectedPatient.name}

+

{selectedPatient.note}

+
+ + {selectedPatient.status} + +
+ +
+
+ Heart Rate + {selectedPatient.heart_rate} + bpm +
+
+ Blood Pressure + {selectedPatient.blood_pressure} + mmHg +
+
+ Temperature + {selectedPatient.temperature}℃ + body temp +
+
+ SpO₂ + {selectedPatient.spo2}% + oxygen +
+
+ +
+ Doctor Decision Support +

{selectedDecision?.next_action}

+

{selectedDecision?.clinical_summary}

+
    + {selectedDecision?.decision_support?.map((item) => ( +
  • {item}
  • + ))} +
+
+
+ + +
+ +
+
+
+

Clinical Timeline

+ 바이탈 → 판단 → 설명 리포트 흐름 +
+ + {selectedTimeline.map((item) => ( +
+ {item.time} +
+ {item.type} · {item.role} +

{item.title}

+
+
+ ))} +
+ +
+
+

Digital Twin Findings

+ 의료진 확인용 위험 부위 요약 +
+ + {selectedTwin.findings.map((finding) => ( +
+
+ {finding.label} + {finding.region} +

{finding.description}

+
+ + {finding.severity} + +
+ ))} +
+
+
+
+ ); +} + +createRoot(document.getElementById("root")).render(); diff --git a/src/app/style.css b/src/app/style.css new file mode 100644 index 0000000..ec35d0c --- /dev/null +++ b/src/app/style.css @@ -0,0 +1,641 @@ +:root { + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: #172033; + background: #f4f7fb; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: #f4f7fb; +} + +button { + font: inherit; +} + +.v3-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 260px 1fr; +} + +.sidebar { + height: 100vh; + position: sticky; + top: 0; + background: #ffffff; + border-right: 1px solid #e1e9f3; + padding: 24px 18px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; +} + +.brand-mark { + width: 44px; + height: 44px; + border-radius: 14px; + background: linear-gradient(135deg, #2563eb, #14b8a6); + color: white; + display: grid; + place-items: center; + font-weight: 900; +} + +.brand strong { + display: block; + font-size: 18px; + color: #0f172a; +} + +.brand span { + display: block; + margin-top: 4px; + color: #64748b; + font-size: 12px; +} + +.side-nav { + display: grid; + gap: 8px; +} + +.side-nav button { + border: 0; + text-align: left; + padding: 12px 14px; + border-radius: 14px; + background: transparent; + color: #475569; + cursor: pointer; +} + +.side-nav button.active { + background: #eff6ff; + color: #1d4ed8; + font-weight: 800; +} + +.side-card { + margin-top: auto; + padding: 16px; + border-radius: 18px; + background: #f0fdfa; + border: 1px solid #ccfbf1; +} + +.side-card strong, +.side-card span { + display: block; +} + +.side-card span { + margin-top: 8px; + color: #0f766e; + line-height: 1.5; + font-size: 13px; +} + +.content { + padding: 34px; +} + +.header { + display: flex; + justify-content: space-between; + gap: 24px; + align-items: flex-start; + margin-bottom: 24px; +} + +.eyebrow { + color: #2563eb; + font-size: 13px; + font-weight: 900; +} + +h1 { + margin: 10px 0; + font-size: 38px; + line-height: 1.1; + letter-spacing: -0.04em; +} + +.header p { + max-width: 840px; + color: #64748b; + line-height: 1.6; +} + +.api-card { + min-width: 220px; + padding: 18px; + border-radius: 20px; + background: white; + border: 1px solid #e1e9f3; + box-shadow: 0 16px 48px rgba(15, 23, 42, 0.06); +} + +.api-card span, +.api-card strong, +.api-card small { + display: block; +} + +.api-card strong { + margin: 8px 0; + color: #16a34a; +} + +.api-card.fallback strong { + color: #f59e0b; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 18px; +} + +.stat-card, +.panel { + background: #ffffff; + border: 1px solid #e1e9f3; + border-radius: 24px; + box-shadow: 0 16px 48px rgba(15, 23, 42, 0.05); +} + +.stat-card { + padding: 22px; +} + +.stat-card span { + color: #64748b; + font-size: 13px; +} + +.stat-card strong { + display: block; + margin-top: 8px; + font-size: 34px; + color: #0f172a; +} + +.stat-card.warning strong { + color: #f59e0b; +} + +.stat-card p { + margin: 8px 0 0; + color: #94a3b8; + font-size: 13px; +} + +.main-grid { + display: grid; + grid-template-columns: 300px minmax(0, 1fr) 380px; + gap: 18px; +} + +.panel { + padding: 22px; +} + +.panel-title { + margin-bottom: 16px; +} + +.panel-title h2 { + margin: 0; + font-size: 20px; +} + +.panel-title span { + display: block; + margin-top: 6px; + color: #64748b; + font-size: 13px; +} + +.patient-list { + display: grid; + gap: 12px; + align-content: start; +} + +.patient-row { + border: 1px solid #e2e8f0; + background: #f8fafc; + border-radius: 18px; + padding: 14px; + display: flex; + justify-content: space-between; + gap: 12px; + text-align: left; + cursor: pointer; +} + +.patient-row.selected { + background: #eff6ff; + border-color: #bfdbfe; +} + +.patient-row strong, +.patient-row span { + display: block; +} + +.patient-row span { + margin-top: 5px; + color: #64748b; + font-size: 13px; +} + +em { + font-style: normal; + padding: 7px 10px; + border-radius: 999px; + background: #f0fdf4; + color: #15803d; + font-weight: 900; + font-size: 12px; +} + +em.watch { + background: #fffbeb; + color: #b45309; +} + +em.big { + font-size: 14px; + align-self: flex-start; +} + +.patient-hero { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; + margin-bottom: 18px; +} + +.patient-chip { + display: inline-flex; + padding: 7px 10px; + border-radius: 999px; + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + font-size: 12px; + font-weight: 900; +} + +.patient-hero h2 { + margin: 12px 0 8px; +} + +.patient-hero p, +.doctor-box p, +.report-panel p, +.twin-panel p, +.finding-row p { + color: #64748b; + line-height: 1.55; +} + +.vital-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; +} + +.vital-grid div { + padding: 14px; + border-radius: 18px; + background: #f8fafc; + border: 1px solid #e2e8f0; +} + +.vital-grid span, +.vital-grid small { + color: #64748b; + font-size: 12px; +} + +.vital-grid strong { + display: block; + margin: 6px 0; + font-size: 22px; +} + +.doctor-box { + margin-top: 18px; + padding: 18px; + border-radius: 20px; + background: #fffbeb; + border: 1px solid #fde68a; +} + +.doctor-box h3 { + margin: 10px 0; +} + +.doctor-box li, +.report-panel li { + margin-bottom: 8px; + color: #334155; +} + +.right-column { + display: grid; + gap: 18px; +} + +.mini-body { + position: relative; + height: 280px; + border-radius: 22px; + background: linear-gradient(180deg, #f8fbff, #eef6ff); + border: 1px solid #dbeafe; + margin-bottom: 14px; + overflow: hidden; +} + +.body-head, +.body-torso, +.body-leg { + position: absolute; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(180deg, rgba(148, 163, 184, 0.45), rgba(219, 234, 254, 0.75)); + border: 1px solid rgba(37, 99, 235, 0.18); +} + +.body-head { + top: 26px; + width: 44px; + height: 52px; + border-radius: 50%; +} + +.body-torso { + top: 86px; + width: 86px; + height: 112px; + border-radius: 44px 44px 28px 28px; +} + +.body-leg { + top: 190px; + width: 28px; + height: 72px; + border-radius: 999px; +} + +.body-leg.left { + left: 45%; +} + +.body-leg.right { + left: 55%; +} + +.marker { + position: absolute; + width: 20px; + height: 20px; + transform: translate(-50%, -50%); + border-radius: 999px; + border: 3px solid white; + background: #ef4444; + box-shadow: 0 0 0 7px rgba(239, 68, 68, 0.15); +} + +.marker.medium { + background: #f59e0b; + box-shadow: 0 0 0 7px rgba(245, 158, 11, 0.15); +} + +.bottom-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; + margin-top: 18px; +} + +.timeline-row { + display: grid; + grid-template-columns: 70px 1fr; + gap: 14px; + padding: 14px 0; + border-top: 1px solid #e2e8f0; +} + +.timeline-row:first-of-type { + border-top: 0; +} + +.timeline-row strong { + color: #1d4ed8; +} + +.timeline-row span { + color: #64748b; + font-size: 13px; +} + +.timeline-row p { + margin: 5px 0 0; + color: #0f172a; +} + +.finding-row { + display: flex; + justify-content: space-between; + gap: 14px; + padding: 14px 0; + border-top: 1px solid #e2e8f0; +} + +.finding-row:first-of-type { + border-top: 0; +} + +.finding-row strong, +.finding-row span { + display: block; +} + +.finding-row span { + margin-top: 4px; + color: #64748b; + font-size: 13px; +} + +@media (max-width: 1200px) { + .v3-shell, + .main-grid, + .bottom-grid, + .header, + .stats-grid, + .vital-grid { + grid-template-columns: 1fr; + display: grid; + } + + .sidebar { + position: relative; + height: auto; + } +} + +.role-mode-banner { + margin-bottom: 18px; + padding: 20px 22px; + border-radius: 24px; + background: linear-gradient(135deg, #eff6ff, #f0fdfa); + border: 1px solid #bfdbfe; +} + +.role-mode-banner strong { + display: block; + margin-top: 8px; + font-size: 24px; + color: #0f172a; +} + +.role-mode-banner p { + margin: 8px 0 0; + color: #475569; + line-height: 1.55; +} + +/* Patient View: 환자 설명 리포트와 이해 가능한 상태 중심 */ +.role-patient .doctor-box, +.role-patient .twin-finding-panel { + display: none; +} + +.role-patient .report-panel { + border-color: #14b8a6; + background: #f0fdfa; +} + +/* Nurse View: 바이탈과 타임라인 중심 */ +.role-nurse .report-panel, +.role-nurse .twin-finding-panel { + display: none; +} + +.role-nurse .patient-detail { + border-color: #2563eb; +} + +.role-nurse .timeline-panel { + border-color: #14b8a6; +} + +/* Doctor View: 판단보조와 디지털 트윈 중심 */ +.role-doctor .report-panel { + display: none; +} + +.role-doctor .doctor-box, +.role-doctor .twin-panel, +.role-doctor .twin-finding-panel { + border-color: #f59e0b; +} + +/* Security View: 감사 로그 / 타임라인 중심 */ +.role-security .patient-detail, +.role-security .report-panel, +.role-security .twin-panel, +.role-security .twin-finding-panel { + display: none; +} + +.role-security .timeline-panel { + grid-column: 1 / -1; + border-color: #8b5cf6; + background: #faf5ff; +} + +.stat-card.danger strong { + color: #ef4444; +} + +.empty-text { + color: #94a3b8; + line-height: 1.6; +} + +.audit-panel { + grid-column: span 1; +} + +.audit-row { + display: grid; + grid-template-columns: 110px 1fr auto; + gap: 14px; + align-items: start; + padding: 14px 0; + border-top: 1px solid #e2e8f0; +} + +.audit-row:first-of-type { + border-top: 0; +} + +.audit-row strong { + display: block; + color: #1d4ed8; +} + +.audit-row span { + display: block; + margin-top: 5px; + color: #64748b; + font-size: 13px; +} + +.audit-row h3 { + margin: 0 0 6px; + font-size: 15px; + color: #0f172a; +} + +.audit-row p { + margin: 0; + color: #64748b; + line-height: 1.5; +} + +.audit-row.denied { + padding-left: 12px; + border-left: 4px solid #ef4444; + background: #fef2f2; + border-radius: 14px; +} + +.role-security .audit-panel { + grid-column: 1 / -1; + border-color: #8b5cf6; + background: #faf5ff; +} + +@media (max-width: 1200px) { + .audit-row { + grid-template-columns: 1fr; + } +}