From a424ac9d1385929d496ff43bcbb4417446f5718d Mon Sep 17 00:00:00 2001 From: Kisa Date: Tue, 26 May 2026 09:45:02 -0400 Subject: [PATCH] feat: add reason strings per patient, fix export headers, add signal-ui source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _build_reason() to backend — per-patient reason strings with specific day counts (e.g. "Supply lapsed 70 days ago. Prescriber contact required.") - Add reason field to RecordOut model and backend /api/export CSV - Fix export column headers: Coverage End Date → Resupply End Date, Days Until Coverage End → Days Until Resupply End - Pass reason through apiRecordToLocal in frontend api.js - Display reason as muted sub-line under status badge in WorklistTable - Add reason column to client-side CSVExport - Add signal-ui React source to repo (was untracked) - CLAUDE.md: add Billing and CMS integrations to Phase 2 deferred table - research: restore Section 14 stat verification (May 23 recovery) Deployed to Railway production — health check confirmed live. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 + python-backend/api/main.py | 29 +- research/dmepos-research-v3.md | 36 + signal-ui/.gitignore | 2 + signal-ui/eslint.config.js | 21 + signal-ui/index.html | 19 + signal-ui/package.json | 29 + signal-ui/pnpm-lock.yaml | 1707 ++++++++++++++++++++ signal-ui/src/App.jsx | 137 ++ signal-ui/src/ThemeContext.js | 3 + signal-ui/src/ThemeProvider.jsx | 27 + signal-ui/src/components/Badge.jsx | 48 + signal-ui/src/components/CSVExport.jsx | 59 + signal-ui/src/components/CSVImport.jsx | 28 + signal-ui/src/components/Sidebar.jsx | 132 ++ signal-ui/src/components/StatCard.jsx | 32 + signal-ui/src/components/ThemeToggle.jsx | 14 + signal-ui/src/components/Toast.jsx | 24 + signal-ui/src/components/WorklistTable.jsx | 206 +++ signal-ui/src/index.css | 168 ++ signal-ui/src/lib/api.js | 48 + signal-ui/src/lib/coverage.js | 165 ++ signal-ui/src/lib/toast.js | 6 + signal-ui/src/main.jsx | 10 + signal-ui/src/useTheme.js | 8 + signal-ui/tokens/tokens.json | 136 ++ signal-ui/vite.config.js | 11 + 27 files changed, 3105 insertions(+), 2 deletions(-) create mode 100644 signal-ui/.gitignore create mode 100644 signal-ui/eslint.config.js create mode 100644 signal-ui/index.html create mode 100644 signal-ui/package.json create mode 100644 signal-ui/pnpm-lock.yaml create mode 100644 signal-ui/src/App.jsx create mode 100644 signal-ui/src/ThemeContext.js create mode 100644 signal-ui/src/ThemeProvider.jsx create mode 100644 signal-ui/src/components/Badge.jsx create mode 100644 signal-ui/src/components/CSVExport.jsx create mode 100644 signal-ui/src/components/CSVImport.jsx create mode 100644 signal-ui/src/components/Sidebar.jsx create mode 100644 signal-ui/src/components/StatCard.jsx create mode 100644 signal-ui/src/components/ThemeToggle.jsx create mode 100644 signal-ui/src/components/Toast.jsx create mode 100644 signal-ui/src/components/WorklistTable.jsx create mode 100644 signal-ui/src/index.css create mode 100644 signal-ui/src/lib/api.js create mode 100644 signal-ui/src/lib/coverage.js create mode 100644 signal-ui/src/lib/toast.js create mode 100644 signal-ui/src/main.jsx create mode 100644 signal-ui/src/useTheme.js create mode 100644 signal-ui/tokens/tokens.json create mode 100644 signal-ui/vite.config.js diff --git a/CLAUDE.md b/CLAUDE.md index c504e75..bbf5f88 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,6 +58,8 @@ Two-curve graph showing supplier staff time over months: | Item | Phase | Reason deferred | |------|-------|----------------| | Dexcom OAuth API integration | Phase 2 | Requires vendor agreement + PHI scope expansion | +| Billing system API integration (Brightree, Bonafide, Fastrack, etc.) | Phase 2 | Build after pilot reveals which systems suppliers use; vendor agreements required; PHI scope expands significantly | +| CMS and contact management integration (Salesforce, etc.) | Phase 2 | Pilot feedback will reveal if suppliers have CMS and what they use; do not assume | | Prescriber fax automation | Phase 2 | Workflow complexity; Level 1 manual outreach sufficient | | Patient-facing SMS (Twilio, not SignalWire) | Phase 3 | PHI transmission to third-party; requires BAA + consent layer | | Consortium strategy (Level 2/3) | 18–24 months | Depends on 15+ paying Level 1 suppliers first | diff --git a/python-backend/api/main.py b/python-backend/api/main.py index 28c6df7..75ac673 100644 --- a/python-backend/api/main.py +++ b/python-backend/api/main.py @@ -67,6 +67,7 @@ class RecordOut(BaseModel): next_visit_due_date: Optional[str] = None action: str status_label: str + reason: str class UploadResponse(BaseModel): @@ -77,6 +78,27 @@ class UploadResponse(BaseModel): stats: dict +def _build_reason(flag_val: str, days_until_end: int, days_until_visit: Optional[int]) -> str: + if flag_val == "OUT_OF_COVERAGE": + ago = abs(days_until_end) + unit = "day" if ago == 1 else "days" + return f"Supply lapsed {ago} {unit} ago. Prescriber contact required before next shipment." + if flag_val == "VISIT_DUE": + if days_until_visit is not None and days_until_visit <= 0: + overdue = abs(days_until_visit) + unit = "day" if overdue == 1 else "days" + return f"Qualifying visit overdue by {overdue} {unit}. Confirm documentation immediately." + if days_until_visit is not None: + unit = "day" if days_until_visit == 1 else "days" + return f"Qualifying visit due in {days_until_visit} {unit}. Confirm visit documentation before resupply." + return "Qualifying visit renewal required. Confirm documentation before resupply." + if flag_val == "REFILL_WINDOW": + unit = "day" if days_until_end == 1 else "days" + return f"Coverage ends in {days_until_end} {unit}. Patient is within resupply window — initiate shipment now." + unit = "day" if days_until_end == 1 else "days" + return f"Coverage on track. Resupply window opens in approximately {days_until_end} {unit}." + + def _to_record_out(r) -> RecordOut: flag_val = r.flag.value if hasattr(r.flag, "value") else str(r.flag) return RecordOut( @@ -93,6 +115,7 @@ def _to_record_out(r) -> RecordOut: next_visit_due_date=r.next_visit_due_date.isoformat() if r.next_visit_due_date else None, action=FLAG_ACTIONS.get(flag_val, "Review"), status_label=FLAG_LABELS.get(flag_val, flag_val), + reason=_build_reason(flag_val, r.days_until_coverage_end, r.days_until_visit_due), ) @@ -158,10 +181,11 @@ async def export_work_queue(records: list[RecordOut]): "Payer", "Status", "Priority Score", - "Days Until Coverage End", + "Days Until Resupply End", "Next Visit Due", "Recommended Action", - "Coverage End Date", + "Resupply End Date", + "Reason", ]) for r in records: writer.writerow([ @@ -174,6 +198,7 @@ async def export_work_queue(records: list[RecordOut]): r.next_visit_due_date or "", r.action, r.coverage_end_date, + r.reason, ]) output.seek(0) diff --git a/research/dmepos-research-v3.md b/research/dmepos-research-v3.md index b38855b..4c6345a 100644 --- a/research/dmepos-research-v3.md +++ b/research/dmepos-research-v3.md @@ -901,6 +901,42 @@ SIGNAL CGM VALUE STACK --- +## 14. Verified Stat Index — May 2026 + +*Stat verification completed May 23, 2026. Use this table before citing any Signal-related statistic. The "usable" column reflects what is citable with a direct source URL.* + +### What Is Verified With Direct Citations + +| Stat | Source | What It Measures | Usable for Signal | +|------|--------|-----------------|-------------------| +| 32.8% error rate on glucose monitor claims | CERT 2019 annual report | Random sample of PAID claims reviewed post-payment | Yes — locked stat. "Nearly 1 in 3." | +| 68.6% of those errors from insufficient documentation | CERT 2019 annual report | Share of error-rate claims with doc problems | Yes — locked. "Over two-thirds from docs." | +| 25.2% CGM improper payment rate | CMS 2024 MLN compliance page (direct URL) | Claims paid that had documentation problems — post-payment audit exposure | Yes — audit exposure narrative | +| 67.6% absent documentation | CMS 2024 MLN compliance page (direct URL) | Share of those improper payments with no docs at all | Yes — use for whitepaper/gate framing | +| **30.86% pre-pay review error rate** | **CGS MAC Jurisdiction B Q2 2024** | **Claims reviewed before or at payment — near submission** | **Best source for denial prevention pitch** | +| 18.52% TPE error rate | MAC B/C 2025 | Claims reviewed at audit | Strong supporting evidence | +| $1.9B DMEPOS improper payments FY2024 | OIG (URL exists) | All DMEPOS categories, not CGM specifically | Market context only | +| DMEPOS 22.5% vs 7.38% overall Medicare | Post-payment comparison | DMEPOS vs. all Medicare improper payment rate | Market context | +| 63.9% MA appeal success at Level 2 | KFF 2024 (direct URL) | Medicare Advantage only — not FFS | MA context only, qualify if used | + +### What Is Derived or Not Directly Citable + +| Stat | Problem | What to Use Instead | +|------|---------|-------------------| +| "94.2% of CGM denials are documentation failures" | Derived sum of two CMS MLN line items — not stated directly by CMS | CERT 2019: "over two-thirds from docs" | +| "35–45% of CGM claims denied" | Scenario-based, not universal | "First-pass denial rates vary significantly by supplier documentation maturity" | +| "63% of denied CGM claims are written off permanently" | Derived model, not citable | Do not use as a standalone stat | + +### Why CGS MAC Pre-Pay Wins for Signal's Pitch + +The CGS Jurisdiction B pre-pay review measures claims being stopped and reviewed near submission — not years later in a post-payment audit. Every top-10 denial reason in the CGS data is a documentation failure. This is the only publicly available source that measures documentation risk at the point Signal addresses it: before supplies ship and before the claim is filed. + +The CERT and CMS MLN stats measure what happened after the fact. The CGS pre-pay stat measures the same problem Signal solves, at the same point in the workflow Signal operates. + +**Use CGS MAC pre-pay for denial prevention framing. Use CERT 2019 for LinkedIn and public-facing stats.** + +--- + ## Sources Research compiled April 2026 from: diff --git a/signal-ui/.gitignore b/signal-ui/.gitignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/signal-ui/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/signal-ui/eslint.config.js b/signal-ui/eslint.config.js new file mode 100644 index 0000000..ea36dd3 --- /dev/null +++ b/signal-ui/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + }, +]) diff --git a/signal-ui/index.html b/signal-ui/index.html new file mode 100644 index 0000000..3cd9179 --- /dev/null +++ b/signal-ui/index.html @@ -0,0 +1,19 @@ + + + + + + Signal CGM — Outreach Worklist + + + + + + +
+ + + \ No newline at end of file diff --git a/signal-ui/package.json b/signal-ui/package.json new file mode 100644 index 0000000..c0eeb23 --- /dev/null +++ b/signal-ui/package.json @@ -0,0 +1,29 @@ +{ + "name": "signal-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "tailwindcss": "^4.3.0", + "vite": "^8.0.12" + } +} diff --git a/signal-ui/pnpm-lock.yaml b/signal-ui/pnpm-lock.yaml new file mode 100644 index 0000000..b5679fe --- /dev/null +++ b/signal-ui/pnpm-lock.yaml @@ -0,0 +1,1707 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^19.2.6 + version: 19.2.6 + react-dom: + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.4.0(jiti@2.7.0)) + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@8.0.14(jiti@2.7.0)) + '@types/react': + specifier: ^19.2.14 + version: 19.2.15 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.15) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.2(vite@8.0.14(jiti@2.7.0)) + eslint: + specifier: ^10.3.0 + version: 10.4.0(jiti@2.7.0) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.4.0(jiti@2.7.0)) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@10.4.0(jiti@2.7.0)) + globals: + specifier: ^17.6.0 + version: 17.6.0 + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + vite: + specifier: ^8.0.12 + version: 8.0.14(jiti@2.7.0) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} + + enhanced-resolve@5.22.0: + resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==} + engines: {node: '>=10.13.0'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.0: + resolution: {integrity: sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.0(jiti@2.7.0))': + dependencies: + eslint: 10.4.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.4.0(jiti@2.7.0))': + optionalDependencies: + eslint: 10.4.0(jiti@2.7.0) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.132.0': {} + + '@rolldown/binding-android-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.2': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.2': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.2': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.2': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@8.0.14(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 8.0.14(jiti@2.7.0) + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/react-dom@19.2.3(@types/react@19.2.15)': + dependencies: + '@types/react': 19.2.15 + + '@types/react@19.2.15': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.0.2(vite@8.0.14(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.14(jiti@2.7.0) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.32: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.361 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + caniuse-lite@1.0.30001793: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.361: {} + + enhanced-resolve@5.22.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@10.4.0(jiti@2.7.0)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + eslint: 10.4.0(jiti@2.7.0) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@10.4.0(jiti@2.7.0)): + dependencies: + eslint: 10.4.0(jiti@2.7.0) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.0(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.6.0: {} + + graceful-fs@4.2.11: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.46: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + rolldown@1.0.2: + dependencies: + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.14(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.2 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.7.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/signal-ui/src/App.jsx b/signal-ui/src/App.jsx new file mode 100644 index 0000000..21777ca --- /dev/null +++ b/signal-ui/src/App.jsx @@ -0,0 +1,137 @@ +import { useState, useCallback, useRef } from "react"; +import { ThemeProvider } from "./ThemeProvider"; +import Sidebar from "./components/Sidebar"; +import StatCard from "./components/StatCard"; +import WorklistTable from "./components/WorklistTable"; +import CSVImport from "./components/CSVImport"; +import CSVExport from "./components/CSVExport"; +import ThemeToggle from "./components/ThemeToggle"; +import Toast from "./components/Toast"; +import { showToast } from "./lib/toast"; +import { uploadToBackend, apiRecordToLocal } from "./lib/api"; +import { parseCSV, processBatch } from "./lib/coverage"; + +function AppInner() { + const [records, setRecords] = useState([]); + const [activeFilter, setActiveFilter] = useState("all"); + const [importLabel, setImportLabel] = useState("No data imported"); + const csvImportRef = useRef(null); + + const ooc = records.filter((r) => r.flag === "OUT_OF_COVERAGE").length; + const visitDue = records.filter((r) => r.flag === "VISIT_DUE").length; + const refill = records.filter((r) => r.flag === "REFILL_WINDOW").length; + const okCount = records.filter((r) => r.flag === "OK").length; + const urgent = ooc + visitDue; + + const handleResults = useCallback((file) => { + const label = new Date().toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + + uploadToBackend(file).then((data) => { + if (data) { + const results = data.records.map(apiRecordToLocal); + setRecords(results); + setImportLabel(`${file.name} · ${label} · via Signal API`); + let msg = `Loaded ${data.total} patient${data.total !== 1 ? "s" : ""} from ${file.name}`; + if (data.skipped) msg += ` · ${data.skipped} skipped`; + showToast(msg); + return; + } + + // Backend unreachable — process locally + const reader = new FileReader(); + reader.onload = (e) => { + const rows = parseCSV(e.target.result); + const { results, skipped } = processBatch(rows); + setRecords(results); + setImportLabel(`${file.name} · ${label} · local processing`); + let msg = `Loaded ${results.length} patient${results.length !== 1 ? "s" : ""} from ${file.name}`; + if (skipped.length) msg += ` · ${skipped.length} skipped`; + showToast(msg); + }; + reader.readAsText(file); + }); + }, []); + + return ( +
+ {/* Sidebar */} + csvImportRef.current?.trigger()} + /> + + {/* Hidden CSV import trigger */} + + + {/* Main */} +
+ {/* Topbar */} +
+
+

+ Outreach Worklist +

+ + {importLabel} + +
+
+ + +
+
+ + {/* Content */} +
+ {/* Stats row */} +
+ + + +
+ + {/* Worklist */} + +
+
+ + +
+ ); +} + +export default function App() { + return ( + + + + ); +} \ No newline at end of file diff --git a/signal-ui/src/ThemeContext.js b/signal-ui/src/ThemeContext.js new file mode 100644 index 0000000..abe9e71 --- /dev/null +++ b/signal-ui/src/ThemeContext.js @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const ThemeContext = createContext(null); \ No newline at end of file diff --git a/signal-ui/src/ThemeProvider.jsx b/signal-ui/src/ThemeProvider.jsx new file mode 100644 index 0000000..695ce27 --- /dev/null +++ b/signal-ui/src/ThemeProvider.jsx @@ -0,0 +1,27 @@ +import { useState, useEffect, useCallback } from "react"; +import { ThemeContext } from "./ThemeContext"; + +export function ThemeProvider({ children }) { + const [dark, setDark] = useState(() => { + const stored = localStorage.getItem("signal-theme"); + if (stored) return stored === "dark"; + return true; + }); + + useEffect(() => { + if (dark) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + localStorage.setItem("signal-theme", dark ? "dark" : "light"); + }, [dark]); + + const toggle = useCallback(() => setDark((prev) => !prev), []); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/signal-ui/src/components/Badge.jsx b/signal-ui/src/components/Badge.jsx new file mode 100644 index 0000000..7db8b58 --- /dev/null +++ b/signal-ui/src/components/Badge.jsx @@ -0,0 +1,48 @@ +import { getFlagLabel, getFlagAction } from "../lib/coverage"; + +function Badge({ flag, dark }) { + const style = badgeStyles(flag, dark); + + return ( + + {style.icon} + {getFlagLabel(flag)} + + ); +} + +function badgeStyles(flag, dark) { + if (dark) return darkBadge(flag); + return lightBadge(flag); +} + +function darkBadge(flag) { + switch (flag) { + case "OUT_OF_COVERAGE": + return { icon: "✕", className: "bg-[rgba(200,48,48,0.22)] text-[#FF7070]" }; + case "VISIT_DUE": + return { icon: "⚡", className: "bg-[rgba(240,120,64,0.18)] text-[#FFB070] border border-[rgba(240,120,64,0.3)]" }; + case "REFILL_WINDOW": + return { icon: "⚠", className: "bg-[rgba(168,90,24,0.16)] text-[#F0B464]" }; + default: + return { icon: "✓", className: "bg-[rgba(26,122,78,0.16)] text-[#4AE899]" }; + } +} + +function lightBadge(flag) { + switch (flag) { + case "OUT_OF_COVERAGE": + return { icon: "✕", className: "bg-[#F8CCCC] text-[#A02020]" }; + case "VISIT_DUE": + return { icon: "⚡", className: "bg-[#FFE4CC] text-[#903A14] border border-[rgba(144,58,20,0.2)]" }; + case "REFILL_WINDOW": + return { icon: "⚠", className: "bg-[#FDECD5] text-[#A85A18]" }; + default: + return { icon: "✓", className: "bg-[#C8EDD8] text-[#146040]" }; + } +} + +export default Badge; \ No newline at end of file diff --git a/signal-ui/src/components/CSVExport.jsx b/signal-ui/src/components/CSVExport.jsx new file mode 100644 index 0000000..bc16cd4 --- /dev/null +++ b/signal-ui/src/components/CSVExport.jsx @@ -0,0 +1,59 @@ +import { getFlagLabel, getFlagAction, getDeviceDisplay } from "../lib/coverage"; + +export default function CSVExport({ records }) { + const today = new Date().toISOString().slice(0, 10); + + const exportCSV = () => { + if (!records || records.length === 0) return; + + const headers = [ + "Patient ID", + "Device", + "Payer", + "Status", + "Priority Score", + "Days Until Resupply End", + "Recommended Action", + "Resupply End Date", + "Reason", + ]; + + const rows = records.map((r) => [ + r.patient_id, + getDeviceDisplay(r.device_type), + r.payer, + getFlagLabel(r.flag), + r.priority, + r.daysUntilEnd, + getFlagAction(r.flag), + r.coverageEndDate || "", + r.reason || "", + ]); + + const csv = [headers, ...rows] + .map((row) => + row.map((v) => `"${String(v).replace(/"/g, '""')}"`).join(",") + ) + .join("\r\n"); + + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `signal-work-queue-${today}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/signal-ui/src/components/CSVImport.jsx b/signal-ui/src/components/CSVImport.jsx new file mode 100644 index 0000000..c61afdd --- /dev/null +++ b/signal-ui/src/components/CSVImport.jsx @@ -0,0 +1,28 @@ +import { useRef, forwardRef, useImperativeHandle } from "react"; + +const CSVImport = forwardRef(function CSVImport({ onResults }, ref) { + const inputRef = useRef(null); + + useImperativeHandle(ref, () => ({ + trigger: () => inputRef.current?.click(), + })); + + const handleFile = (e) => { + const file = e.target.files?.[0]; + if (!file) return; + e.target.value = ""; + onResults(file); + }; + + return ( + + ); +}); + +export default CSVImport; \ No newline at end of file diff --git a/signal-ui/src/components/Sidebar.jsx b/signal-ui/src/components/Sidebar.jsx new file mode 100644 index 0000000..28edba1 --- /dev/null +++ b/signal-ui/src/components/Sidebar.jsx @@ -0,0 +1,132 @@ +export default function Sidebar({ + oocCount, + visitDueCount, + refillCount, + activeFilter, + onFilterChange, + onImportClick, +}) { + const navItems = [ + { + label: "All Patients", + key: "all", + icon: "≡", + badge: null, + }, + { + label: "Resupply Ready", + key: "REFILL_WINDOW", + icon: "⚠", + badge: refillCount, + badgeWarn: true, + }, + { + label: "Renewal Due", + key: "VISIT_DUE", + icon: "⚡", + badge: visitDueCount, + }, + { + label: "Supply Lapsed", + key: "OUT_OF_COVERAGE", + icon: "✕", + badge: oocCount, + }, + ]; + + return ( + + ); +} \ No newline at end of file diff --git a/signal-ui/src/components/StatCard.jsx b/signal-ui/src/components/StatCard.jsx new file mode 100644 index 0000000..aa8af4c --- /dev/null +++ b/signal-ui/src/components/StatCard.jsx @@ -0,0 +1,32 @@ +export default function StatCard({ + label, + value, + sub, + priority = false, +}) { + return ( +
+
+ {label} +
+
+ {value} +
+ {sub && ( +
+ {sub} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/signal-ui/src/components/ThemeToggle.jsx b/signal-ui/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..b71f6b2 --- /dev/null +++ b/signal-ui/src/components/ThemeToggle.jsx @@ -0,0 +1,14 @@ +import { useTheme } from "../useTheme"; + +export default function ThemeToggle() { + const { dark, toggle } = useTheme(); + + return ( + + ); +} \ No newline at end of file diff --git a/signal-ui/src/components/Toast.jsx b/signal-ui/src/components/Toast.jsx new file mode 100644 index 0000000..be18d5c --- /dev/null +++ b/signal-ui/src/components/Toast.jsx @@ -0,0 +1,24 @@ +import { useState, useEffect } from "react"; + +export default function Toast() { + const [message, setMessage] = useState(""); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const handler = (e) => { + setMessage(e.detail.message); + setVisible(true); + setTimeout(() => setVisible(false), 3500); + }; + window.addEventListener("signal-toast", handler); + return () => window.removeEventListener("signal-toast", handler); + }, []); + + if (!visible) return null; + + return ( +
+ {message} +
+ ); +} \ No newline at end of file diff --git a/signal-ui/src/components/WorklistTable.jsx b/signal-ui/src/components/WorklistTable.jsx new file mode 100644 index 0000000..4f70dff --- /dev/null +++ b/signal-ui/src/components/WorklistTable.jsx @@ -0,0 +1,206 @@ +import { useTheme } from "../useTheme"; +import Badge from "./Badge"; +import { getDeviceDisplay } from "../lib/coverage"; + +function daysLabel(days) { + if (days < 0) + return Expired {Math.abs(days)}d ago; + if (days <= 7) + return {days} days; + if (days <= 30) + return {days} days; + return {days} days; +} + +function scoreClass(priority) { + if (priority >= 500) return "text-[var(--accent-text)]"; + if (priority >= 200) return "text-[var(--text-secondary)]"; + return "text-[var(--text-muted)]"; +} + +function isHighPriority(flag) { + return flag === "OUT_OF_COVERAGE" || flag === "VISIT_DUE"; +} + +const FILTERS = [ + { key: "all", label: "All" }, + { key: "OUT_OF_COVERAGE", label: "Supply Lapsed" }, + { key: "VISIT_DUE", label: "Renewal Due" }, + { key: "REFILL_WINDOW", label: "Resupply Ready" }, + { key: "OK", label: "Active" }, +]; + +export default function WorklistTable({ records, activeFilter, onFilterChange }) { + const { dark } = useTheme(); + + const filtered = + activeFilter === "all" + ? records + : records.filter((r) => r.flag === activeFilter); + + const todayStr = new Date().toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + + return ( +
+ {/* Header */} +
+
+
+ Outreach Worklist +
+
+ {records.length} patients · sorted by priority score · {todayStr} +
+
+
+ {FILTERS.map((f) => ( + + ))} +
+
+ + {/* Table */} + + + + + + + + + + + + + + {filtered.map((r, i) => { + const hp = isHighPriority(r.flag); + return ( + + + + + + + + + + ); + })} + {filtered.length === 0 && ( + + + + )} + +
+ Patient ID + + Device + + Payer + + Days Left + + Status + + Priority + + Action +
+
+ {r.patient_id} +
+ {i === 0 && ( +
+ ★ TOP PRIORITY +
+ )} +
+ {getDeviceDisplay(r.device_type)} + + {r.payer} + + {daysLabel(r.daysUntilEnd)} + + + {r.reason && ( +
+ {r.reason} +
+ )} +
+ + {r.priority} + + + +
+ No patients match this filter. +
+ + {/* Footer */} +
+ + 🔒 PHI-safe — patient names and DOBs never stored. Crosswalk: patient_id only. + + + {activeFilter === "all" + ? `${records.length} patients · all results shown` + : `${filtered.length} of ${records.length} · filtered`} + +
+
+ ); +} + +function ActionButton({ flag }) { + const base = + "bg-transparent border border-[var(--border-color)] text-[var(--text-secondary)] px-[13px] py-[5px] rounded-md text-xs cursor-pointer font-body whitespace-nowrap transition-all hover:border-[var(--brand)] hover:text-[var(--brand)]"; + + if (flag === "OUT_OF_COVERAGE" || flag === "VISIT_DUE") { + const label = + flag === "OUT_OF_COVERAGE" ? "Contact Prescriber" : "Request Renewal →"; + return ; + } + if (flag === "REFILL_WINDOW") { + return ; + } + return ; +} \ No newline at end of file diff --git a/signal-ui/src/index.css b/signal-ui/src/index.css new file mode 100644 index 0000000..4c961f7 --- /dev/null +++ b/signal-ui/src/index.css @@ -0,0 +1,168 @@ +@import "tailwindcss"; + +/* ── Signal Design Tokens — v1.0 ── + Source: docs/design-tokens-v1.json + docs/sttil-brand-system-v1.md + Accent family: tangerine (warm gold on dark, restrained copper on light) +*/ + +@theme { + /* ── Color: Teal (dominant system color) ── */ + --color-teal-50: #EEF8F8; + --color-teal-100: #CBE9E9; + --color-teal-200: #97D3D3; + --color-teal-300: #5BBBBB; + --color-teal-400: #2EA3A3; + --color-teal-500: #1A8A8A; + --color-teal-600: #147A7A; + --color-teal-700: #0F5E5E; + --color-teal-800: #0A4444; + --color-teal-900: #072E2E; + --color-teal-950: #041A1A; + + /* ── Color: Tangerine (accent) ── */ + --color-tng-50: #FFF4EE; + --color-tng-100: #FFE4CC; + --color-tng-200: #FFC090; + --color-tng-300: #FFB070; + --color-tng-400: #F07840; + --color-tng-500: #E06028; + --color-tng-600: #C04E1C; + --color-tng-700: #903A14; + --color-tng-800: #6A2A0C; + --color-tng-900: #3E1808; + + /* ── Color: Neutral (teal-toned) ── */ + --color-neutral-0: #FFFFFF; + --color-neutral-50: #F4F9F9; + --color-neutral-100: #E5EEEE; + --color-neutral-200: #C8D8D8; + --color-neutral-300: #A3BEBE; + --color-neutral-400: #7A9E9E; + --color-neutral-500: #5A7E7E; + --color-neutral-600: #426060; + --color-neutral-700: #2E4444; + --color-neutral-800: #1C2C2C; + --color-neutral-900: #111A1A; + + /* ── Color: Semantic ── */ + --color-success-100: #C8EDD8; + --color-success-500: #1A7A4E; + --color-success-600: #146040; + --color-warning-100: #FDECD5; + --color-warning-400: #D97B35; + --color-warning-600: #A85A18; + --color-error-100: #F8CCCC; + --color-error-500: #C83030; + --color-error-600: #A02020; + --color-info-100: #C8E0EE; + --color-info-500: #1A6A9A; + --color-purple-100: #E2D8F0; + --color-purple-500: #7A5EA0; + + /* ── Color: Mode-specific surfaces ── */ + --color-warm-white: #FFFAF6; + --color-page-dark: #041A1A; + --color-card-dark: #072E2E; + --color-elevated-dark: #0A4444; + --color-border-dark: #0F5E5E; + + /* ── Typography ── */ + --font-heading: 'Plus Jakarta Sans', sans-serif; + --font-body: 'Inter', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + --leading-tight: 1.15; + --leading-snug: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.6; + + --tracking-tight: -0.02em; + --tracking-normal: 0em; + --tracking-wide: 0.05em; + --tracking-wider: 0.08em; + --tracking-widest: 0.1em; + + /* ── Border radius ── */ + --radius-sm: 0.25rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-2xl: 1.5rem; + + /* ── Shadows ── */ + --shadow-card-sm: 0 1px 3px rgba(7,46,46,0.10); + --shadow-card-md: 0 2px 10px rgba(4,26,26,0.14); + --shadow-card-lg: 0 4px 20px rgba(4,26,26,0.18); + --shadow-copper-dark: 0 0 24px rgba(240,120,64,0.30), 0 2px 10px rgba(4,26,26,0.70); + --shadow-copper-light: 0 0 16px rgba(224,96,40,0.20), 0 2px 8px rgba(0,0,0,0.08); +} + +/* ── Base layer ── */ +@layer base { + *, *::before, *::after { + box-sizing: border-box; + } + + body { + margin: 0; + font-family: var(--font-body); + font-weight: var(--font-weight-regular); + font-size: var(--text-sm); + line-height: var(--leading-normal); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} + +/* ── Light mode (default) ── */ +:root { + --bg-page: var(--color-neutral-50); + --bg-card: var(--color-neutral-0); + --bg-elevated: var(--color-neutral-100); + --text-heading: #0A3030; + --text-primary: var(--color-neutral-800); + --text-secondary:var(--color-neutral-500); + --text-muted: var(--color-neutral-400); + --border-color: var(--color-neutral-200); + --border-subtle: rgba(200,216,216,0.7); + --brand: var(--color-teal-600); + --brand-hover: var(--color-teal-700); + --accent: var(--color-tng-500); + --accent-text: var(--color-tng-500); + --accent-glow: var(--shadow-copper-light); + --row-hover: rgba(20,122,122,0.04); +} + +/* ── Dark mode ── */ +.dark { + --bg-page: var(--color-page-dark); + --bg-card: var(--color-card-dark); + --bg-elevated: var(--color-elevated-dark); + --text-heading: var(--color-warm-white); + --text-primary: #F0F4F4; + --text-secondary:var(--color-teal-300); + --text-muted: #5A8080; + --border-color: var(--color-border-dark); + --border-subtle: rgba(15,94,94,0.45); + --brand: var(--color-teal-400); + --brand-hover: #1A8A8A; + --accent: var(--color-tng-400); + --accent-text: var(--color-tng-300); + --accent-glow: var(--shadow-copper-dark); + --row-hover: rgba(46,163,163,0.06); +} \ No newline at end of file diff --git a/signal-ui/src/lib/api.js b/signal-ui/src/lib/api.js new file mode 100644 index 0000000..a831fed --- /dev/null +++ b/signal-ui/src/lib/api.js @@ -0,0 +1,48 @@ +/** + * Signal API helper — tries the backend, returns null on failure so the + * app can fall back to client-side coverage calculation. + */ + +const BACKEND_URL = "https://signal-api-production-91c2.up.railway.app"; + +/** + * Upload a CSV file to the backend scoring endpoint. + * @param {File} file + * @returns {Promise} API response or null on failure + */ +export async function uploadToBackend(file) { + const formData = new FormData(); + formData.append("file", file); + try { + const resp = await fetch(`${BACKEND_URL}/api/upload`, { + method: "POST", + body: formData, + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(err.detail?.message || `API error ${resp.status}`); + } + return resp.json(); + } catch { + return null; + } +} + +/** + * Convert backend record shape to local record shape. + */ +export function apiRecordToLocal(r) { + return { + patient_id: r.patient_id, + device_type: r.device_type, + payer: r.payer, + component: r.component, + flag: r.flag, + daysUntilEnd: r.days_until_coverage_end, + daysUntilVisit: r.days_until_visit_due, + priority: r.priority_score, + coverageEndDate: r.coverage_end_date, + nextVisitDueDate: r.next_visit_due_date, + reason: r.reason || "", + }; +} \ No newline at end of file diff --git a/signal-ui/src/lib/coverage.js b/signal-ui/src/lib/coverage.js new file mode 100644 index 0000000..9d522c5 --- /dev/null +++ b/signal-ui/src/lib/coverage.js @@ -0,0 +1,165 @@ +/** + * Client-side CGM coverage calculator — mirrors python-backend/core/coverage_calculator.py. + * Used as fallback when the Signal API is unreachable. + * + * PHI CONTRACT: This module receives only patient_id, device_type, shipment_date, + * quantity, payer, component. No names, SSNs, DOBs, or contact fields. + */ + +export const FLAG = { + OUT_OF_COVERAGE: "OUT_OF_COVERAGE", + VISIT_DUE: "VISIT_DUE", + REFILL_WINDOW: "REFILL_WINDOW", + OK: "OK", +}; + +const FLAG_LABELS = { + [FLAG.OUT_OF_COVERAGE]: "Supply Lapsed", + [FLAG.VISIT_DUE]: "Renewal Due", + [FLAG.REFILL_WINDOW]: "Resupply Ready", + [FLAG.OK]: "Active", +}; + +const FLAG_ACTIONS = { + [FLAG.OUT_OF_COVERAGE]: "Contact Prescriber", + [FLAG.VISIT_DUE]: "Request Renewal", + [FLAG.REFILL_WINDOW]: "Initiate Resupply", + [FLAG.OK]: "No action needed", +}; + +/** Wear-day rules — mirrors payer_rules.json */ +const DEVICE_RULES = { + dexcom_g7: { display: "Dexcom G7", sensor: 10 }, + dexcom_g6: { display: "Dexcom G6", sensor: 10, transmitter: 90 }, + freestyle_libre_2: { display: "FreeStyle Libre 2", sensor: 14 }, + freestyle_libre_3: { display: "FreeStyle Libre 3", sensor: 14 }, + omnipod_5: { display: "Omnipod 5", pod: 3, sensor: 14 }, +}; + +export function getDeviceDisplay(deviceType) { + return DEVICE_RULES[deviceType]?.display ?? deviceType; +} + +export function getFlagLabel(flag) { + return FLAG_LABELS[flag] ?? flag; +} + +export function getFlagAction(flag) { + return FLAG_ACTIONS[flag] ?? "Review"; +} + +function today() { + const d = new Date(); + d.setHours(0, 0, 0, 0); + return d; +} + +function daysDiff(a, b) { + return Math.round((a.getTime() - b.getTime()) / 86400000); +} + +function addDays(dateStr, days) { + const d = new Date(dateStr + "T00:00:00"); + d.setDate(d.getDate() + days); + return d; +} + +function getWearDays(deviceType, component) { + const device = DEVICE_RULES[deviceType]; + if (!device) return null; + return device[component] ?? null; +} + +function getPayerConfig(payer) { + const p = payer.toLowerCase(); + if (p.includes("medicare")) { + return { visitRenewalDays: 180, refillWindowDays: 30 }; + } + return { visitRenewalDays: null, refillWindowDays: 30 }; +} + +function computePriority(flag, daysUntilEnd) { + if (flag === FLAG.OUT_OF_COVERAGE) return 1000 + Math.abs(daysUntilEnd); + if (flag === FLAG.VISIT_DUE) return 500 + Math.max(0, 90 - daysUntilEnd); + if (flag === FLAG.REFILL_WINDOW) return 200 + Math.max(0, 30 - daysUntilEnd); + return 0; +} + +/** + * Calculate coverage for a single record. + * @param {{ patient_id, device_type, shipment_date, quantity, payer, component }} record + * @returns {{ flag, daysUntilEnd, daysUntilVisit, priority }} or null on error + */ +export function calculateCoverage(record) { + const wearDays = getWearDays(record.device_type, record.component); + if (wearDays === null) return null; + + const now = today(); + const totalWear = wearDays * parseInt(record.quantity, 10); + const coverageEnd = addDays(record.shipment_date, totalWear); + const daysUntilEnd = daysDiff(coverageEnd, now); + + const payerCfg = getPayerConfig(record.payer); + + let daysUntilVisit = null; + if (payerCfg.visitRenewalDays) { + const visitDue = addDays(record.shipment_date, payerCfg.visitRenewalDays); + daysUntilVisit = daysDiff(visitDue, now); + } + + let flag; + if (daysUntilEnd < 0) { + flag = FLAG.OUT_OF_COVERAGE; + } else if (daysUntilVisit !== null && daysUntilVisit <= 30) { + flag = FLAG.VISIT_DUE; + } else if (daysUntilEnd <= payerCfg.refillWindowDays) { + flag = FLAG.REFILL_WINDOW; + } else { + flag = FLAG.OK; + } + + return { + ...record, + flag, + daysUntilEnd, + daysUntilVisit, + priority: computePriority(flag, daysUntilEnd), + coverageEndDate: coverageEnd.toISOString().slice(0, 10), + }; +} + +/** + * Parse raw CSV text into record objects. + */ +export function parseCSV(text) { + const lines = text.trim().split(/\r?\n/); + if (lines.length < 2) return []; + const headers = lines[0].split(",").map((h) => h.trim().toLowerCase()); + return lines + .slice(1) + .filter((l) => l.trim()) + .map((line) => { + const cols = line.split(",").map((c) => c.trim()); + const row = {}; + headers.forEach((h, i) => (row[h] = cols[i] || "")); + return row; + }); +} + +/** + * Process a batch of parsed CSV rows and return sorted results + skip list. + */ +export function processBatch(rows) { + const results = []; + const skipped = []; + for (const row of rows) { + const result = calculateCoverage(row); + if (result) { + results.push(result); + } else { + skipped.push(row.patient_id || "?"); + } + } + results.sort((a, b) => b.priority - a.priority); + return { results, skipped }; +} \ No newline at end of file diff --git a/signal-ui/src/lib/toast.js b/signal-ui/src/lib/toast.js new file mode 100644 index 0000000..8844e80 --- /dev/null +++ b/signal-ui/src/lib/toast.js @@ -0,0 +1,6 @@ +/** Fire a toast notification from anywhere in the app. */ +export function showToast(message) { + window.dispatchEvent( + new CustomEvent("signal-toast", { detail: { message } }) + ); +} \ No newline at end of file diff --git a/signal-ui/src/main.jsx b/signal-ui/src/main.jsx new file mode 100644 index 0000000..54b1c91 --- /dev/null +++ b/signal-ui/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; +import "./index.css"; + +createRoot(document.getElementById("root")).render( + + + +); \ No newline at end of file diff --git a/signal-ui/src/useTheme.js b/signal-ui/src/useTheme.js new file mode 100644 index 0000000..a8baa78 --- /dev/null +++ b/signal-ui/src/useTheme.js @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { ThemeContext } from "./ThemeContext.js"; + +export function useTheme() { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); + return ctx; +} \ No newline at end of file diff --git a/signal-ui/tokens/tokens.json b/signal-ui/tokens/tokens.json new file mode 100644 index 0000000..8bd3068 --- /dev/null +++ b/signal-ui/tokens/tokens.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://design-tokens.github.io/community-group/format/", + "$metadata": { + "version": "1.0.0", + "updated": "2026-04-23", + "scope": "STTIL Solutions — master token set (Signal CGM product expression)", + "accent-family": "tangerine", + "source": "docs/design-tokens-v1.json" + }, + + "color": { + "teal": { + "50": { "$value": "#EEF8F8", "$type": "color" }, + "100": { "$value": "#CBE9E9", "$type": "color" }, + "200": { "$value": "#97D3D3", "$type": "color" }, + "300": { "$value": "#5BBBBB", "$type": "color" }, + "400": { "$value": "#2EA3A3", "$type": "color" }, + "500": { "$value": "#1A8A8A", "$type": "color" }, + "600": { "$value": "#147A7A", "$type": "color" }, + "700": { "$value": "#0F5E5E", "$type": "color" }, + "800": { "$value": "#0A4444", "$type": "color" }, + "900": { "$value": "#072E2E", "$type": "color" }, + "950": { "$value": "#041A1A", "$type": "color" } + }, + "tangerine": { + "50": { "$value": "#FFF4EE", "$type": "color" }, + "100": { "$value": "#FFE4CC", "$type": "color" }, + "200": { "$value": "#FFC090", "$type": "color" }, + "300": { "$value": "#FFB070", "$type": "color" }, + "400": { "$value": "#F07840", "$type": "color" }, + "500": { "$value": "#E06028", "$type": "color" }, + "600": { "$value": "#C04E1C", "$type": "color" }, + "700": { "$value": "#903A14", "$type": "color" }, + "800": { "$value": "#6A2A0C", "$type": "color" }, + "900": { "$value": "#3E1808", "$type": "color" } + }, + "neutral": { + "0": { "$value": "#FFFFFF", "$type": "color" }, + "50": { "$value": "#F4F9F9", "$type": "color" }, + "100": { "$value": "#E5EEEE", "$type": "color" }, + "200": { "$value": "#C8D8D8", "$type": "color" }, + "300": { "$value": "#A3BEBE", "$type": "color" }, + "400": { "$value": "#7A9E9E", "$type": "color" }, + "500": { "$value": "#5A7E7E", "$type": "color" }, + "600": { "$value": "#426060", "$type": "color" }, + "700": { "$value": "#2E4444", "$type": "color" }, + "800": { "$value": "#1C2C2C", "$type": "color" }, + "900": { "$value": "#111A1A", "$type": "color" } + }, + "semantic": { + "success": { + "100": { "$value": "#C8EDD8", "$type": "color" }, + "500": { "$value": "#1A7A4E", "$type": "color" }, + "600": { "$value": "#146040", "$type": "color" } + }, + "warning": { + "100": { "$value": "#FDECD5", "$type": "color" }, + "400": { "$value": "#D97B35", "$type": "color" }, + "600": { "$value": "#A85A18", "$type": "color" } + }, + "error": { + "100": { "$value": "#F8CCCC", "$type": "color" }, + "500": { "$value": "#C83030", "$type": "color" }, + "600": { "$value": "#A02020", "$type": "color" } + }, + "info": { + "100": { "$value": "#C8E0EE", "$type": "color" }, + "500": { "$value": "#1A6A9A", "$type": "color" } + }, + "purple": { + "100": { "$value": "#E2D8F0", "$type": "color" }, + "500": { "$value": "#7A5EA0", "$type": "color" } + } + } + }, + + "typography": { + "fontFamily": { + "heading": { "$value": "'Plus Jakarta Sans', sans-serif", "$type": "fontFamily" }, + "body": { "$value": "'Inter', sans-serif", "$type": "fontFamily" }, + "mono": { "$value": "'JetBrains Mono', monospace", "$type": "fontFamily" } + }, + "fontSize": { + "xs": { "$value": "0.75rem" }, + "sm": { "$value": "0.875rem" }, + "base": { "$value": "1rem" }, + "lg": { "$value": "1.125rem" }, + "xl": { "$value": "1.25rem" }, + "2xl": { "$value": "1.5rem" }, + "3xl": { "$value": "1.875rem" }, + "4xl": { "$value": "2.25rem" }, + "5xl": { "$value": "3rem" } + }, + "fontWeight": { + "regular": { "$value": 400 }, + "medium": { "$value": 500 }, + "semibold": { "$value": 600 }, + "bold": { "$value": 700 } + } + }, + + "spacing": { + "0": { "$value": "0rem" }, + "1": { "$value": "0.25rem" }, + "2": { "$value": "0.5rem" }, + "3": { "$value": "0.75rem" }, + "4": { "$value": "1rem" }, + "5": { "$value": "1.25rem" }, + "6": { "$value": "1.5rem" }, + "8": { "$value": "2rem" }, + "10": { "$value": "2.5rem" }, + "12": { "$value": "3rem" }, + "16": { "$value": "4rem" }, + "20": { "$value": "5rem" }, + "24": { "$value": "6rem" } + }, + + "borderRadius": { + "none": { "$value": "0" }, + "sm": { "$value": "0.25rem" }, + "md": { "$value": "0.5rem" }, + "lg": { "$value": "0.75rem" }, + "xl": { "$value": "1rem" }, + "2xl": { "$value": "1.5rem" }, + "full": { "$value": "9999px" } + }, + + "shadow": { + "sm": { "$value": "0 1px 3px rgba(7,46,46,0.10)" }, + "md": { "$value": "0 2px 10px rgba(4,26,26,0.14)" }, + "lg": { "$value": "0 4px 20px rgba(4,26,26,0.18)" }, + "xl": { "$value": "0 8px 32px rgba(4,26,26,0.22)" }, + "tangerine-glow-dark": { "$value": "0 0 24px rgba(240,120,64,0.30), 0 2px 10px rgba(4,26,26,0.70)" }, + "tangerine-glow-light": { "$value": "0 0 16px rgba(224,96,40,0.20), 0 2px 8px rgba(0,0,0,0.08)" } + } +} \ No newline at end of file diff --git a/signal-ui/vite.config.js b/signal-ui/vite.config.js new file mode 100644 index 0000000..c3f3c35 --- /dev/null +++ b/signal-ui/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [tailwindcss(), react()], + build: { + outDir: 'dist', + assetsDir: 'assets', + }, +}) \ No newline at end of file