diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..2c2d7b1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,49 @@
+name: Bug Report
+description: Report a bug with reproduction details.
+title: "[Bug] "
+labels:
+ - bug
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: Bug Summary
+ description: What is broken?
+ placeholder: Briefly describe the bug.
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: Steps to Reproduce
+ description: How can someone reproduce the issue?
+ placeholder: |
+ 1. Go to ...
+ 2. Click ...
+ 3. See error ...
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected Behavior
+ placeholder: Describe what should have happened.
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual
+ attributes:
+ label: Actual Behavior
+ placeholder: Describe what actually happened.
+ validations:
+ required: true
+
+ - type: textarea
+ id: environment
+ attributes:
+ label: Environment
+ description: App version, OS, branch, logs, screenshots, or other context.
+ placeholder: macOS 15, desktop app, local branch, console output...
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3ba13e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml
new file mode 100644
index 0000000..60f19ee
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature.yml
@@ -0,0 +1,37 @@
+name: Feature Request
+description: Propose a new feature or user-facing enhancement.
+title: "[Feature] "
+labels:
+ - feature
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem
+ description: What user or product problem does this solve?
+ placeholder: Describe the pain point or missing capability.
+ validations:
+ required: true
+
+ - type: textarea
+ id: proposal
+ attributes:
+ label: Proposed Solution
+ description: What should the feature do?
+ placeholder: Describe the expected behavior or UX.
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives Considered
+ description: Other approaches you considered.
+ placeholder: Simpler approach, tradeoffs, rejected options...
+
+ - type: textarea
+ id: impact
+ attributes:
+ label: Expected Impact
+ description: Which layer or users will this affect?
+ placeholder: Desktop only, server API, shared core, docs, onboarding...
diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml
new file mode 100644
index 0000000..4dd1d58
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/task.yml
@@ -0,0 +1,41 @@
+name: Task
+description: Track a concrete implementation task.
+title: "[Task] "
+labels:
+ - task
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: Summary
+ description: What needs to be done?
+ placeholder: Describe the task in one or two sentences.
+ validations:
+ required: true
+
+ - type: textarea
+ id: scope
+ attributes:
+ label: Scope
+ description: Which areas are affected?
+ placeholder: apps/desktop, apps/server, crates/frilday-core, docs...
+ validations:
+ required: true
+
+ - type: textarea
+ id: acceptance
+ attributes:
+ label: Acceptance Criteria
+ description: What should be true when this task is done?
+ placeholder: |
+ - [ ] ...
+ - [ ] ...
+ validations:
+ required: true
+
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes
+ description: Extra context, references, or dependencies.
+ placeholder: Related issues, risks, implementation notes...
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..145a226
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,24 @@
+## Summary
+
+- what changed?
+- why did it change?
+
+## Scope
+
+- `apps/desktop`
+- `apps/server`
+- `crates/frilday-core`
+- `docs`
+
+Delete any lines that do not apply.
+
+## Validation
+
+- [ ] `apps/desktop`: `npm run lint`
+- [ ] `apps/desktop`: `npm run build`
+- [ ] `apps/server`: `cargo check --manifest-path apps/server/Cargo.toml`
+- [ ] `crates/frilday-core`: `cargo check --manifest-path crates/frilday-core/Cargo.toml`
+
+## Notes
+
+- architectural impact, follow-up work, or risks
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b6eaf5b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,52 @@
+name: CI
+
+on:
+ pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
+
+jobs:
+ desktop:
+ name: Desktop Build
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: apps/desktop
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Lint desktop app
+ run: npm run lint
+
+ - name: Build desktop app
+ run: npm run build
+
+ rust:
+ name: Rust Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Check server crate
+ run: cargo check --manifest-path apps/server/Cargo.toml
+
+ - name: Check core crate
+ run: cargo check --manifest-path crates/frilday-core/Cargo.toml
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f558d75
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+.DS_Store
+
+# Node / frontend
+node_modules/
+dist/
+.vite/
+coverage/
+
+# Rust / Cargo
+target/
+
+# Tauri generated outputs
+apps/desktop/src-tauri/target/
+apps/desktop/src-tauri/gen/schemas/
+
+# Environment / local settings
+.env
+.env.*
+!.env.example
+
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+bun-debug.log*
+
+# Editor
+.idea/
+.vscode/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0be7478
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# FrilDay
+
+FrilDay is a productivity app centered on planned time, completion, and repeatable routines.
+
+The repository is being organized as a monorepo with a desktop-first release path and a longer-term cloud architecture.
+
+## Workspace
+
+```text
+apps/
+ desktop/ Tauri + React client
+ server/ Axum server skeleton
+
+crates/
+ frilday-core/ Shared core crate skeleton
+
+docs/
+ ARCHITECTURE.md
+```
+
+## Current Status
+
+- `apps/desktop` is the active application
+- `apps/server` exists as a bootstrap crate
+- `crates/frilday-core` exists as a bootstrap crate
+- architecture direction is documented in [docs/ARCHITECTURE.md](/Users/mars112/code/project/frilday/docs/ARCHITECTURE.md)
+
+## Desktop App
+
+From the repo root:
+
+```bash
+cd apps/desktop
+npm run build
+bunx tauri build
+```
+
+Local packaged app output:
+
+- `apps/desktop/src-tauri/target/release/bundle/macos/dailycheck.app`
+
+## Workflow
+
+- keep `main` buildable
+- use short-lived branches
+- prefer small PRs by layer
+- avoid committing generated outputs
+
+More detail lives in [docs/ARCHITECTURE.md](/Users/mars112/code/project/frilday/docs/ARCHITECTURE.md).
diff --git a/apps/desktop/README.ko.md b/apps/desktop/README.ko.md
new file mode 100644
index 0000000..c115981
--- /dev/null
+++ b/apps/desktop/README.ko.md
@@ -0,0 +1,307 @@
+# 시간 기반 습관 관리 (Time-Based Habit Tracker)
+
+이 프로젝트는 **단순한 Todo 앱이 아니라, 시간 기반 습관 관리 시스템**입니다.
+
+일반적인 할 일 관리 앱은 보통 다음만 기록합니다.
+
+```
+✔ 했는가 / 안 했는가
+```
+
+하지만 실제 루틴 관리에서는 이것만으로 부족합니다.
+
+예를 들어
+
+- 운동 1시간
+- 독서 30분
+- 코딩 2시간
+
+이런 활동은 **시간을 얼마나 투자했는지**가 훨씬 중요합니다.
+
+이 프로젝트는 다음 두 가지를 함께 기록합니다.
+
+```
+1️⃣ 완료 여부 (Completion)
+2️⃣ 투자한 시간 (Time Tracking)
+```
+
+이를 통해 **하루 루틴을 더 현실적으로 관리할 수 있는 시스템**을 만드는 것이 목표입니다.
+
+---
+
+# 주요 기능
+
+## 1. 요일 기반 일정 관리
+
+각 작업(Task)은 **특정 요일에만 수행하도록 설정**할 수 있습니다.
+
+예시
+
+```
+운동 → 월 / 수 / 금
+독서 → 매일
+코딩 공부 → 화 / 목
+```
+
+오늘 수행해야 하는 작업만 **Today 페이지에 표시됩니다.**
+
+---
+
+## 2. 타이머 기반 시간 기록
+
+각 작업은 타이머로 시간을 측정할 수 있습니다.
+
+```
+Start → Stop
+```
+
+시간이 기록되면 다음과 같은 데이터가 저장됩니다.
+
+```
+TimeEntry
+{
+ taskId
+ date
+ startedAt
+ endedAt
+ minutes
+}
+```
+
+이를 통해 하루 동안 **어떤 작업에 얼마나 시간을 사용했는지** 확인할 수 있습니다.
+
+---
+
+## 3. 완료 체크
+
+시간 기록과 별개로 작업을 **완료 처리(Done)** 할 수 있습니다.
+
+예시
+
+```
+독서 → 완료 체크
+운동 → 타이머로 시간 기록
+```
+
+완료 데이터는 다음과 같은 형태로 저장됩니다.
+
+```
+Completion
+{
+ taskId
+ date
+}
+```
+
+---
+
+## 4. 오늘 진행률 확인
+
+Today 페이지에서는 다음 정보를 확인할 수 있습니다.
+
+- 오늘 예정된 작업 수
+- 완료된 작업 수
+- 완료율
+- 전체 진행 시간
+- 진행률 Progress Bar
+
+예시
+
+```
+2h 10m / 3h
+```
+
+```
+██████████░░░░░░░
+70%
+```
+
+이를 통해 **오늘 하루 목표 대비 얼마나 진행되었는지** 직관적으로 확인할 수 있습니다.
+
+---
+
+## 5. 작업 아카이브
+
+작업을 삭제하지 않고 **Archive 상태로 보관**할 수 있습니다.
+
+아카이브된 작업은
+
+- 기본 리스트에서는 숨겨짐
+- 필요할 때 복원 가능
+- 기존 완료 기록과 시간 기록 유지
+
+---
+
+# Today 페이지 구조
+
+```
+Today Page
+
+좌측 패널
+ ├─ 오늘 날짜
+ ├─ Scheduled tasks
+ ├─ Done
+ └─ Completion progress
+
+우측 영역
+ ├─ Time progress
+ │ └─ 오늘 시간 사용량
+ │
+ └─ Today's tasks
+ ├─ Start / Stop 타이머
+ ├─ Done 체크
+ └─ Archive
+```
+
+---
+
+# 데이터 모델
+
+이 프로젝트는 단순한 도메인 모델을 사용합니다.
+
+## Task
+
+작업 정보
+
+```
+Task
+{
+ id
+ title
+ category
+ durationMinutes
+ daysOfWeek
+ isActive
+}
+```
+
+---
+
+## Completion
+
+완료 기록
+
+```
+Completion
+{
+ taskId
+ date
+}
+```
+
+---
+
+## TimeEntry
+
+시간 기록
+
+```
+TimeEntry
+{
+ taskId
+ date
+ startedAt
+ endedAt
+ minutes
+}
+```
+
+---
+
+# 프로젝트 구조
+
+도메인 로직과 UI를 분리하는 구조로 설계되어 있습니다.
+
+```
+domain/
+ ├─ types
+ ├─ completion
+ ├─ schedule
+ ├─ stats
+ └─ date
+
+ui/
+ ├─ components
+ └─ pages
+```
+
+이 구조를 통해
+
+- UI 변경
+- 로직 확장
+- 테스트
+
+를 쉽게 할 수 있도록 구성했습니다.
+
+---
+
+# 기술 스택
+
+Frontend
+
+- React
+- TypeScript
+- TailwindCSS
+
+UI
+
+- Lucide Icons
+
+---
+
+# 설계 철학
+
+이 프로젝트는 다음 원칙을 중심으로 설계되었습니다.
+
+### 1. 단순한 도메인 모델
+
+불필요한 추상화를 최소화했습니다.
+
+---
+
+### 2. UI와 로직 분리
+
+도메인 로직은 `domain` 폴더에서 관리됩니다.
+
+---
+
+### 3. 예측 가능한 데이터 흐름
+
+모든 계산은 **순수 함수 기반으로 처리**됩니다.
+
+예시
+
+```
+isScheduledOn()
+isDoneOn()
+diffMinutes()
+```
+
+---
+
+# 향후 확장 아이디어
+
+앞으로 다음 기능을 추가할 수 있습니다.
+
+- 주간 / 월간 통계
+- 스트릭(Streak) 추적
+- 목표 기반 습관 관리
+- 데이터 백업 / 동기화
+- 모바일 UI 최적화
+
+---
+
+# 프로젝트 목적
+
+이 프로젝트는 다음 질문에서 시작되었습니다.
+
+> 단순히 “할 일을 했는가”만으로
+> 생산성을 측정할 수 있을까?
+
+많은 경우 더 중요한 것은 다음입니다.
+
+```
+얼마나 시간을 투자했는가
+```
+
+이 프로젝트는 **완료 기반 Todo와 시간 기반 생산성 추적을 결합한 시스템**을 실험하는 프로젝트입니다.
diff --git a/apps/desktop/README.md b/apps/desktop/README.md
new file mode 100644
index 0000000..466cc23
--- /dev/null
+++ b/apps/desktop/README.md
@@ -0,0 +1,50 @@
+# FrilDay Desktop
+
+Desktop client for FrilDay built with Tauri, React, TypeScript, and SQLite.
+
+The current app focuses on daily task scheduling, completion tracking, timer-based progress, and local persistence.
+
+## Main Features
+
+- schedule tasks by weekday
+- track completion separately from spent time
+- run timers for time-based habits
+- store local data with Tauri-backed SQLite
+- package as a native desktop app
+
+## App Structure
+
+```text
+src/
+ app/ app wiring, pages, store, layout
+ domain/ desktop-side domain rules
+ features/ UI feature components
+ i18n/ locale messages and translation
+ infrastructure/ storage, notification, tauri adapters
+ shared/ shared frontend types and utilities
+
+src-tauri/
+ src/ Rust entrypoints
+ capabilities/ Tauri capabilities
+```
+
+## Commands
+
+```bash
+npm run dev
+npm run build
+bunx tauri dev
+bunx tauri build
+```
+
+## Build Output
+
+macOS bundle output:
+
+- `src-tauri/target/release/bundle/macos/dailycheck.app`
+
+## Notes
+
+- this app is the active product surface right now
+- server and shared core extraction are planned at the repo level
+- broader direction is documented in [../../docs/ARCHITECTURE.md](/Users/mars112/code/project/frilday/docs/ARCHITECTURE.md)
diff --git a/apps/desktop/bun.lock b/apps/desktop/bun.lock
new file mode 100644
index 0000000..92a3ae9
--- /dev/null
+++ b/apps/desktop/bun.lock
@@ -0,0 +1,625 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "dailycheck",
+ "dependencies": {
+ "@hookform/resolvers": "^5.2.2",
+ "@tailwindcss/vite": "^4.2.1",
+ "@tauri-apps/plugin-notification": "^2.3.3",
+ "@tauri-apps/plugin-sql": "^2.3.2",
+ "@tauri-apps/plugin-store": "^2.4.2",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.575.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-hook-form": "^7.71.2",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.11",
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@tauri-apps/cli": "^2.10.0",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "autoprefixer": "^10.4.27",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.2.1",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.48.0",
+ "vite": "^7.3.1",
+ },
+ },
+ },
+ "packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="],
+
+ "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+
+ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
+
+ "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
+
+ "@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="],
+
+ "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ=="],
+
+ "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw=="],
+
+ "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w=="],
+
+ "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA=="],
+
+ "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg=="],
+
+ "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.1", "", { "os": "linux", "cpu": "none" }, "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw=="],
+
+ "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw=="],
+
+ "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ=="],
+
+ "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg=="],
+
+ "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw=="],
+
+ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="],
+
+ "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="],
+
+ "@tauri-apps/plugin-sql": ["@tauri-apps/plugin-sql@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-4VDXhcKXVpyh5KKpnTGAn6q2DikPHH+TXGh9ZDQzULmG/JEz1RDvzQStgBJKddiukRbYEZ8CGIA2kskx+T+PpA=="],
+
+ "@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A=="],
+
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="],
+
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
+
+ "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001776", "", {}, "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="],
+
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
+
+ "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="],
+
+ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
+
+ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "lucide-react": ["lucide-react@0.575.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+
+ "react-hook-form": ["react-hook-form@7.71.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA=="],
+
+ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
+
+ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
+
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+
+ "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+ }
+}
diff --git a/apps/desktop/eslint.config.js b/apps/desktop/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/apps/desktop/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/apps/desktop/index.html b/apps/desktop/index.html
new file mode 100644
index 0000000..cde2525
--- /dev/null
+++ b/apps/desktop/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ dailycheck
+
+
+
+
+
+
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
new file mode 100644
index 0000000..0937fbd
--- /dev/null
+++ b/apps/desktop/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "dailycheck",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "tauri:dev": "tauri dev",
+ "tauri:build": "tauri build"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^5.2.2",
+ "@tailwindcss/vite": "^4.2.1",
+ "@tauri-apps/plugin-notification": "^2.3.3",
+ "@tauri-apps/plugin-sql": "^2.3.2",
+ "@tauri-apps/plugin-store": "^2.4.2",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.575.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-hook-form": "^7.71.2",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.11"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@tauri-apps/cli": "^2.10.0",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "autoprefixer": "^10.4.27",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.2.1",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.48.0",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/apps/desktop/public/vite.svg b/apps/desktop/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/apps/desktop/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/src-tauri/.gitignore b/apps/desktop/src-tauri/.gitignore
new file mode 100644
index 0000000..502406b
--- /dev/null
+++ b/apps/desktop/src-tauri/.gitignore
@@ -0,0 +1,4 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+/gen/schemas
diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock
new file mode 100644
index 0000000..59c8f06
--- /dev/null
+++ b/apps/desktop/src-tauri/Cargo.lock
@@ -0,0 +1,6393 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_log-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
+
+[[package]]
+name = "android_logger"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
+dependencies = [
+ "android_log-sys",
+ "env_filter",
+ "log",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "app"
+version = "0.1.0"
+dependencies = [
+ "log",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-log",
+ "tauri-plugin-notification",
+ "tauri-plugin-sql",
+ "tauri-plugin-store",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "borsh"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
+dependencies = [
+ "borsh-derive",
+ "cfg_aliases",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate 3.4.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "byte-unit"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d"
+dependencies = [
+ "rust_decimal",
+ "schemars 1.2.1",
+ "serde",
+ "utf8-width",
+]
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.11.0",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
+dependencies = [
+ "serde",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
+dependencies = [
+ "bitflags 2.11.0",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.11.0",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.29.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "matches",
+ "phf 0.10.1",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ctor"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "embed-resource"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 0.9.12+spec-1.1.0",
+ "vswhom",
+ "winreg",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "env_filter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "fern"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.11.0",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "match_token",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core 0.62.2",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+dependencies = [
+ "byteorder",
+ "png",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "iri-string"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "js-sys"
+version = "0.3.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+dependencies = [
+ "bitflags 2.11.0",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "kuchikiki"
+version = "0.8.8-speedreader"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
+dependencies = [
+ "cssparser",
+ "html5ever",
+ "indexmap 2.13.0",
+ "selectors",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.182"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
+dependencies = [
+ "bitflags 2.11.0",
+ "libc",
+ "plain",
+ "redox_syscall 0.7.3",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+dependencies = [
+ "value-bag",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "mac-notification-sys"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621"
+dependencies = [
+ "cc",
+ "objc2",
+ "objc2-foundation",
+ "time",
+]
+
+[[package]]
+name = "markup5ever"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
+dependencies = [
+ "log",
+ "phf 0.11.3",
+ "phf_codegen 0.11.3",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "match_token"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "muda"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.11.0",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "notify-rust"
+version = "4.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2"
+dependencies = [
+ "futures-lite",
+ "log",
+ "mac-notification-sys",
+ "serde",
+ "tauri-winrt-notification",
+ "zbus",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
+dependencies = [
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.5",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
+dependencies = [
+ "proc-macro-crate 3.4.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-text",
+ "objc2-core-video",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.11.0",
+ "dispatch2",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.11.0",
+ "dispatch2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-text"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-core-video"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-javascript-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
+dependencies = [
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-security"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-javascript-core",
+ "objc2-security",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_macros 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared 0.11.3",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher 1.0.2",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "plist"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.13.0",
+ "quick-xml 0.38.4",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+dependencies = [
+ "toml_edit 0.23.10+spec-1.0.0",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.11.0",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
+dependencies = [
+ "bitflags 2.11.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
+dependencies = [
+ "arrayvec",
+ "borsh",
+ "bytes",
+ "num-traits",
+ "rand 0.8.5",
+ "rkyv",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.11.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "selectors"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
+dependencies = [
+ "bitflags 1.3.2",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "phf 0.8.0",
+ "phf_codegen 0.8.0",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.13.0",
+ "schemars 0.9.0",
+ "schemars 1.2.1",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "siphasher"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
+dependencies = [
+ "bytemuck",
+ "js-sys",
+ "ndk",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "redox_syscall 0.5.18",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.5",
+ "hashlink",
+ "indexmap 2.13.0",
+ "log",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.18",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck 0.5.0",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn 2.0.117",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags 2.11.0",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand 0.8.5",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.18",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags 2.11.0",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand 0.8.5",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.18",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "thiserror 2.0.18",
+ "time",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "string_cache"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared 0.11.3",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.34.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2",
+ "core-foundation",
+ "core-graphics",
+ "crossbeam-channel",
+ "dispatch",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "once_cell",
+ "parking_lot",
+ "raw-window-handle",
+ "scopeguard",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "getrandom 0.3.4",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "toml 0.9.12+spec-1.1.0",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.117",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "toml 0.9.12+spec-1.1.0",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-log"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93"
+dependencies = [
+ "android_logger",
+ "byte-unit",
+ "fern",
+ "log",
+ "objc2",
+ "objc2-foundation",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "swift-rs",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+ "time",
+]
+
+[[package]]
+name = "tauri-plugin-notification"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
+dependencies = [
+ "log",
+ "notify-rust",
+ "rand 0.9.2",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "tauri-plugin-sql"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27454a5476feb6ea78d74d0f07085fc8a5e97565d7ae825373011fd27ec30303"
+dependencies = [
+ "futures-core",
+ "indexmap 2.13.0",
+ "log",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+ "time",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "tauri-plugin-store"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea"
+dependencies = [
+ "dunce",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dunce",
+ "glob",
+ "html5ever",
+ "http",
+ "infer",
+ "json-patch",
+ "kuchikiki",
+ "log",
+ "memchr",
+ "phf 0.11.3",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.18",
+ "toml 0.9.12+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "tauri-winrt-notification"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
+dependencies = [
+ "quick-xml 0.37.5",
+ "thiserror 2.0.18",
+ "windows",
+ "windows-version",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap 2.13.0",
+ "serde_core",
+ "serde_spanned 1.0.4",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.13.0",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.13.0",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.23.10+spec-1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
+dependencies = [
+ "indexmap 2.13.0",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.9+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
+dependencies = [
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+dependencies = [
+ "bitflags 2.11.0",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c"
+dependencies = [
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "winapi",
+]
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.13.0",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.0",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.0",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-implement",
+ "windows-interface",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
+dependencies = [
+ "thiserror 2.0.18",
+ "windows",
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "whoami"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
+dependencies = [
+ "libredox",
+ "wasite",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "windows-sys 0.59.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link 0.1.3",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "indexmap 2.13.0",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.0",
+ "indexmap 2.13.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.13.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "wry"
+version = "0.54.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a"
+dependencies = [
+ "base64 0.22.1",
+ "block2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "html5ever",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "kuchikiki",
+ "libc",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 0.7.14",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
+dependencies = [
+ "proc-macro-crate 3.4.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
+dependencies = [
+ "serde",
+ "winnow 0.7.14",
+ "zvariant",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zvariant"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 0.7.14",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
+dependencies = [
+ "proc-macro-crate 3.4.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.117",
+ "winnow 0.7.14",
+]
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
new file mode 100644
index 0000000..1b56654
--- /dev/null
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "app"
+version = "0.1.0"
+description = "A Tauri App"
+authors = ["you"]
+license = ""
+repository = ""
+edition = "2021"
+rust-version = "1.77.2"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+name = "app_lib"
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[build-dependencies]
+tauri-build = { version = "2.5.4", features = [] }
+
+[dependencies]
+serde_json = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+log = "0.4"
+tauri = { version = "2.10.0", features = [] }
+tauri-plugin-log = "2"
+tauri-plugin-store = "2.4.2"
+tauri-plugin-sql = { version = "2.3.2", features = ["sqlite"] }
+tauri-plugin-notification = "2.3.3"
diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs
new file mode 100644
index 0000000..795b9b7
--- /dev/null
+++ b/apps/desktop/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build()
+}
diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json
new file mode 100644
index 0000000..395d869
--- /dev/null
+++ b/apps/desktop/src-tauri/capabilities/default.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "enables the default permissions",
+ "windows": [
+ "main"
+ ],
+ "permissions": [
+ "core:default",
+ "store:default",
+ "sql:default",
+ "sql:allow-execute",
+ "notification:default"
+ ]
+}
diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png
new file mode 100644
index 0000000..8656077
Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128.png differ
diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png
new file mode 100644
index 0000000..fabcaa9
Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128@2x.png differ
diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png
new file mode 100644
index 0000000..72cbaee
Binary files /dev/null and b/apps/desktop/src-tauri/icons/32x32.png differ
diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png
new file mode 100644
index 0000000..f577606
Binary files /dev/null and b/apps/desktop/src-tauri/icons/64x64.png differ
diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png
new file mode 100644
index 0000000..eb6b297
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square107x107Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png
new file mode 100644
index 0000000..2d30fa0
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square142x142Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-tauri/icons/Square150x150Logo.png
new file mode 100644
index 0000000..96a526d
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square150x150Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png
new file mode 100644
index 0000000..8139868
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square284x284Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-tauri/icons/Square30x30Logo.png
new file mode 100644
index 0000000..cf2970c
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square30x30Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-tauri/icons/Square310x310Logo.png
new file mode 100644
index 0000000..d40e656
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square310x310Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png
new file mode 100644
index 0000000..25e9f3f
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square44x44Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-tauri/icons/Square71x71Logo.png
new file mode 100644
index 0000000..88a33e0
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square71x71Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png
new file mode 100644
index 0000000..d0225be
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square89x89Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-tauri/icons/StoreLogo.png
new file mode 100644
index 0000000..eb8bbf1
Binary files /dev/null and b/apps/desktop/src-tauri/icons/StoreLogo.png differ
diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns
new file mode 100644
index 0000000..5449576
Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.icns differ
diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico
new file mode 100644
index 0000000..e7888cc
Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.ico differ
diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png
new file mode 100644
index 0000000..77528c6
Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.png differ
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
new file mode 100644
index 0000000..e431cdf
--- /dev/null
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -0,0 +1,61 @@
+use std::{fs, path::PathBuf};
+use tauri::Manager;
+
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_store::Builder::new().build())
+ .plugin(tauri_plugin_sql::Builder::default().build())
+ .plugin(tauri_plugin_notification::init())
+ .setup(|app| {
+ migrate_legacy_app_data_dir(app.handle())?;
+
+ if cfg!(debug_assertions) {
+ app.handle().plugin(
+ tauri_plugin_log::Builder::default()
+ .level(log::LevelFilter::Info)
+ .build(),
+ )?;
+ }
+ Ok(())
+ })
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
+}
+
+fn migrate_legacy_app_data_dir(app: &tauri::AppHandle) -> tauri::Result<()> {
+ const DB_FILE_NAME: &str = "daily_check.db";
+ const LEGACY_IDENTIFIERS: [&str; 2] = ["com.mars112.dailycheck", "dailycheck"];
+
+ let current_data_dir: PathBuf = match app.path().app_data_dir() {
+ Ok(path) => path,
+ Err(_) => return Ok(()),
+ };
+
+ if fs::metadata(current_data_dir.join(DB_FILE_NAME)).is_ok() {
+ return Ok(());
+ }
+
+ let parent_dir = match current_data_dir.parent() {
+ Some(parent) => parent.to_path_buf(),
+ None => return Ok(()),
+ };
+
+ let legacy_db_path = match LEGACY_IDENTIFIERS
+ .iter()
+ .map(|identifier| parent_dir.join(identifier).join(DB_FILE_NAME))
+ .find(|path| fs::metadata(path).is_ok())
+ {
+ Some(path) => path,
+ None => return Ok(()),
+ };
+
+ if legacy_db_path == current_data_dir.join(DB_FILE_NAME) {
+ return Ok(());
+ }
+
+ fs::create_dir_all(¤t_data_dir)?;
+ fs::copy(&legacy_db_path, current_data_dir.join(DB_FILE_NAME))?;
+
+ Ok(())
+}
diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs
new file mode 100644
index 0000000..ad5fe83
--- /dev/null
+++ b/apps/desktop/src-tauri/src/main.rs
@@ -0,0 +1,6 @@
+// Prevents additional console window on Windows in release, DO NOT REMOVE!!
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+fn main() {
+ app_lib::run();
+}
diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json
new file mode 100644
index 0000000..1e56aa0
--- /dev/null
+++ b/apps/desktop/src-tauri/tauri.conf.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
+ "productName": "dailycheck",
+ "version": "0.1.0",
+ "identifier": "app.dailycheck",
+ "build": {
+ "frontendDist": "../dist",
+ "devUrl": "http://localhost:5173",
+ "beforeDevCommand": "bun run dev",
+ "beforeBuildCommand": "bun run build"
+ },
+ "app": {
+ "windows": [
+ {
+ "title": "dailycheck",
+ "width": 800,
+ "height": 600,
+ "resizable": true,
+ "fullscreen": false
+ }
+ ],
+ "security": {
+ "csp": null
+ }
+ },
+ "plugins": {
+ "sql": {
+ "preload": [
+ "sqlite:daily_check.db"
+ ]
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all",
+ "icon": [
+ "icons/32x32.png",
+ "icons/128x128.png",
+ "icons/128x128@2x.png",
+ "icons/icon.icns",
+ "icons/icon.ico"
+ ]
+ }
+}
diff --git a/apps/desktop/src/app/App.tsx b/apps/desktop/src/app/App.tsx
new file mode 100644
index 0000000..baf59d5
--- /dev/null
+++ b/apps/desktop/src/app/App.tsx
@@ -0,0 +1,155 @@
+import { useContext, useMemo } from 'react';
+
+import { HeaderTabs } from './layout/HeaderTabs';
+import { ErrorBanner } from './layout/ErrorBanner';
+
+import { TodayPage } from './pages/TodayPage';
+import { ManagePage } from './pages/ManagePage';
+import { SchedulePage } from './pages/SchedulePage';
+
+import { useAppModel } from './hooks/useAppModel';
+import { useAutoStopTick } from '../features/task/hooks/useAutoStopTick';
+
+import type { Task } from '../shared/types';
+
+import { ToastHost } from './ui/ToastHost';
+import { initNotifier } from './bootstrap/initNotifier';
+import { startOfWeekMonday, toYmd } from '../shared/utils/date';
+import { LocaleContext } from '../i18n/context';
+import { SettingsPage } from './pages/SettingsPage';
+
+// Toast
+// NOTE: Initializing outside App prevents re-init on every render.
+initNotifier();
+
+export default function App() {
+ // (role: app-wide timer auto-stop tick, type: () => void)
+ useAutoStopTick();
+
+ const m = useAppModel();
+ const { t } = useContext(LocaleContext);
+
+ if (!m.hydrated) {
+ return (
+
+ Loading data...
+
+ );
+ }
+
+ // (role: schedule view week start (Monday), type: string (YYYY-MM-DD))
+ const scheduleWeekStartYmd = useMemo(() => {
+ return toYmd(startOfWeekMonday(m.today));
+ }, [m.today]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ DailyCheck
+
+
+
+
+ {t('common.today')}
+
+
+ {m.todayYmd}{' '}
+ ({m.todayDow})
+
+
+
+
+
+
+
+
+
+ {m.errorMsg && (
+
+
+
+ )}
+
+
+
+ {m.tab === 'today' && (
+
+ m.toggleToday({ taskId: task.id, today: m.today })
+ }
+ onArchive={m.archiveTask}
+ onError={m.setError}
+ onStartTimer={m.handleStartTimer}
+ onStopTimer={m.handleStopTimer}
+ />
+ )}
+
+ {m.tab === 'manage' && (
+
+ m.toggleToday({ taskId: task.id, today: m.today })
+ }
+ onArchive={m.archiveTask}
+ onRestore={m.handleRestore}
+ onDelete={m.handleDelete}
+ onError={m.setError}
+ timeEntries={m.timeEntries}
+ nowIso={m.nowIso}
+ runningTaskIdToday={m.runningTaskIdToday}
+ onStartTimer={m.handleStartTimer}
+ onStopTimer={m.handleStopTimer}
+ />
+ )}
+
+ {m.tab === 'schedule' && (
+ m.setTab('manage')}
+ />
+ )}
+
+ {m.tab === 'settings' && }
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/app/bootstrap/initNotifier.ts b/apps/desktop/src/app/bootstrap/initNotifier.ts
new file mode 100644
index 0000000..19e99a5
--- /dev/null
+++ b/apps/desktop/src/app/bootstrap/initNotifier.ts
@@ -0,0 +1,7 @@
+import { setNotifier } from '../di/notifierDI';
+import { toastNotifier } from '../../infrastructure/notification/ToastNotifier';
+
+// (role: app bootstrap initializer, type: ()=>void)
+export function initNotifier(): void {
+ setNotifier(toastNotifier);
+}
diff --git a/apps/desktop/src/app/di/notifierDI.ts b/apps/desktop/src/app/di/notifierDI.ts
new file mode 100644
index 0000000..1c7b50a
--- /dev/null
+++ b/apps/desktop/src/app/di/notifierDI.ts
@@ -0,0 +1,20 @@
+import type { Notifier } from '../../domain/notifier';
+
+// (role: DI container for notifier, type: module state)
+let notifier: Notifier | null = null;
+
+// (role: set notifier dependency, type: (Notifier) => void)
+export function setNotifier(next: Notifier): void {
+ notifier = next;
+}
+
+// (role: get notifier dependency, type: () => Notifier)
+export function getNotifier(): Notifier {
+ if (!notifier) {
+ // 개발 중 실수 방지: bootstrap을 빼먹으면 즉시 알 수 있게
+ throw new Error(
+ 'Notifier is not initialized. Call initNotifier() in App bootstrap.',
+ );
+ }
+ return notifier;
+}
diff --git a/apps/desktop/src/app/hooks/useAppModel.ts b/apps/desktop/src/app/hooks/useAppModel.ts
new file mode 100644
index 0000000..ecbd47a
--- /dev/null
+++ b/apps/desktop/src/app/hooks/useAppModel.ts
@@ -0,0 +1,262 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useDailyCheckStore } from '../store/useDailyCheckStore';
+import { dayOfWeek, startOfWeekMonday, toYmd } from '../../shared/utils/date';
+import { calcTodayStats, calcWeekStats } from '../../domain/stats/stats';
+import { isDoneOn } from '../../domain/completion';
+import type { Task } from '../../shared/types';
+import type { Tab } from '../layout/HeaderTabs';
+import type { CreateTaskInput } from '../../features/task/components/TaskForm';
+import { getNotifier } from '../di/notifierDI';
+import { getDailyMemoText } from '../../domain/memo';
+import { isVisibleInWeek } from '../../domain/scheduleLimit';
+
+export function useAppModel() {
+ const {
+ hydrated,
+ tasks,
+ completions,
+ timeEntries,
+ taskDailyMemos,
+ errorMsg,
+ clearError,
+ createTask,
+ updateTaskMeta,
+ archiveTask,
+ restoreTask,
+ deleteTask,
+ toggleToday,
+ setDailyMemo,
+ startTimer,
+ stopTimer,
+ } = useDailyCheckStore();
+
+ const [tab, setTab] = useState('today');
+
+ // Toast
+ const notifier = getNotifier();
+
+ // Manage controls
+ const [showArchived, setShowArchived] = useState(false);
+ const [manageQuery, setManageQuery] = useState('');
+ const [manageCategory, setManageCategory] = useState<
+ 'all' | Task['category']
+ >('all');
+
+ // UI clock tick (30s). Used to keep "Today: Xm" increasing without per-item intervals.
+ // (role: ui clock iso, type: string)
+ const [nowIso, setNowIso] = useState(() => new Date().toISOString());
+
+ useEffect(() => {
+ const id = window.setInterval(() => {
+ setNowIso(new Date().toISOString());
+ }, 30000);
+
+ return () => window.clearInterval(id);
+ }, []);
+
+ // Derive "today" from ui clock so day changes (midnight) are reflected.
+ const today = useMemo(() => new Date(nowIso), [nowIso]);
+ const todayYmd = toYmd(today);
+ const todayDow = dayOfWeek(today);
+ const weekStartYmd = toYmd(startOfWeekMonday(today));
+
+ // Single running timer (today). Store enforces 1 running entry per day.
+ // (role: single running task id for today, type: string | null)
+ const runningTaskIdToday = useMemo(() => {
+ const running = timeEntries.find(
+ (e) => e.date === todayYmd && e.endedAt == null,
+ );
+ return running?.taskId ?? null;
+ }, [timeEntries, todayYmd]);
+
+ const weekStats = useMemo(
+ () => calcWeekStats(tasks, completions, weekStartYmd),
+ [tasks, completions, weekStartYmd],
+ );
+
+ const todayTasks = useMemo(() => {
+ const filtered = tasks.filter((t) => {
+ if (!t.isActive) return false;
+
+ const createdAtYmd = t.createdAt.slice(0, 10);
+ const startYmd = t.startYmd?.trim() || null;
+ const effectiveStartYmd =
+ startYmd && startYmd > createdAtYmd ? startYmd : createdAtYmd;
+ if (todayYmd < effectiveStartYmd) return false;
+
+ const doneToday = completions.some(
+ (c) => c.taskId === t.id && c.date === todayYmd,
+ );
+ // When done exists on this day, keep the row visible even if backlog is exhausted.
+ if (doneToday) return true;
+
+ if (!t.daysOfWeek.includes(todayDow)) return false;
+ return isVisibleInWeek(t, todayYmd, weekStartYmd, completions);
+ });
+ return [...filtered].sort((a, b) => {
+ const aDone = isDoneOn(completions, a.id, todayYmd);
+ const bDone = isDoneOn(completions, b.id, todayYmd);
+ if (aDone === bDone) return 0;
+ return aDone ? 1 : -1;
+ });
+ }, [tasks, completions, todayDow, todayYmd, weekStartYmd]);
+
+ const todayStats = useMemo(
+ () => calcTodayStats(todayTasks, completions, todayYmd, todayDow),
+ [todayTasks, completions, todayYmd, todayDow],
+ );
+
+ const manageTasks = useMemo(() => {
+ const base = showArchived
+ ? tasks.filter((t) => !t.isActive)
+ : tasks.filter((t) => t.isActive);
+
+ const byCategory =
+ manageCategory === 'all'
+ ? base
+ : base.filter((t) => t.category === manageCategory);
+
+ const q = manageQuery.trim().toLowerCase();
+ if (!q) return byCategory;
+
+ return byCategory.filter(
+ (t) =>
+ t.title.toLowerCase().includes(q) ||
+ t.description.toLowerCase().includes(q),
+ );
+ }, [tasks, showArchived, manageCategory, manageQuery]);
+
+ const setError = (msg: string) =>
+ useDailyCheckStore.setState({ errorMsg: msg });
+
+ // Handlers
+ const handleCreate = (input: CreateTaskInput) => {
+ createTask(input);
+ notifier.notify({
+ level: 'success',
+ message: `Task created: ${input.title}`,
+ });
+ };
+
+ const handleUpdateTaskMeta = (input: {
+ taskId: string;
+ title: string;
+ description: string;
+ startYmd: string | null;
+ autoArchiveAfter: number | null;
+ }) => {
+ updateTaskMeta(input);
+ notifier.notify({
+ level: 'success',
+ message: 'Task updated',
+ });
+ };
+
+ const handleSaveDailyMemo = (input: {
+ taskId: string;
+ date: string;
+ text: string;
+ }) => {
+ setDailyMemo(input);
+ notifier.notify({
+ level: 'info',
+ message: 'Memo saved',
+ });
+ };
+
+ const getMemoText = (taskId: string, date: string): string =>
+ getDailyMemoText(taskDailyMemos, taskId, date);
+
+ const handleRestore = (taskId: string) => {
+ restoreTask(taskId);
+ setShowArchived(false);
+ notifier.notify({
+ level: 'success',
+ message: `Task restored`,
+ });
+ };
+
+ const handleDelete = (taskId: string) => {
+ deleteTask(taskId);
+ notifier.notify({
+ level: 'success',
+ message: `Task deleted permanently`,
+ });
+ };
+
+ const handleResetManage = () => {
+ setManageQuery('');
+ setManageCategory('all');
+ setShowArchived(false);
+ };
+
+ // 실시간을 위해 today(useMemo) 대신 현재 날짜 받기
+ const handleStartTimer = (task: Task) => {
+ startTimer({ taskId: task.id, today: new Date() });
+
+ notifier.notify({
+ level: 'info',
+ message: `Timer started`,
+ });
+ };
+
+ const handleStopTimer = (task: Task) => {
+ stopTimer({ taskId: task.id, today: new Date() });
+ notifier.notify({
+ level: 'info',
+ message: `Timer stopped`,
+ });
+ };
+
+ return {
+ hydrated,
+ // raw
+ tasks,
+ completions,
+ timeEntries,
+ taskDailyMemos,
+ errorMsg,
+
+ // time
+ today,
+ todayYmd,
+ todayDow,
+ nowIso,
+ runningTaskIdToday,
+
+ // view state
+ tab,
+ setTab,
+
+ // manage state
+ showArchived,
+ setShowArchived,
+ manageQuery,
+ setManageQuery,
+ manageCategory,
+ setManageCategory,
+
+ // derived
+ weekStats,
+ todayStats,
+ todayTasks,
+ manageTasks,
+
+ // actions
+ clearError,
+ setError,
+ handleCreate,
+ handleUpdateTaskMeta,
+ toggleToday,
+ handleSaveDailyMemo,
+ getMemoText,
+ archiveTask,
+ handleRestore,
+ handleDelete,
+ handleResetManage,
+
+ // timer actions (UI용)
+ handleStartTimer,
+ handleStopTimer,
+ };
+}
diff --git a/apps/desktop/src/app/layout/ErrorBanner.tsx b/apps/desktop/src/app/layout/ErrorBanner.tsx
new file mode 100644
index 0000000..b17a452
--- /dev/null
+++ b/apps/desktop/src/app/layout/ErrorBanner.tsx
@@ -0,0 +1,22 @@
+import { useContext } from 'react';
+import { LocaleContext } from '../../i18n/context';
+
+export function ErrorBanner({
+ message,
+ onDismiss,
+}: {
+ message: string; // (role: error message, type: string)
+ onDismiss: () => void; // (role: dismiss handler, type: ()=>void)
+}) {
+ const { t } = useContext(LocaleContext);
+
+ return (
+
+ );
+}
diff --git a/apps/desktop/src/app/layout/HeaderTabs.tsx b/apps/desktop/src/app/layout/HeaderTabs.tsx
new file mode 100644
index 0000000..0fc6001
--- /dev/null
+++ b/apps/desktop/src/app/layout/HeaderTabs.tsx
@@ -0,0 +1,101 @@
+import { useContext } from 'react';
+import { LocaleContext } from '../../i18n/context';
+import {
+ CalendarCheck,
+ CalendarCog,
+ CalendarDays,
+ Settings,
+} from 'lucide-react';
+import clsx from 'clsx';
+
+export type Tab = 'today' | 'manage' | 'schedule' | 'settings';
+
+export function HeaderTabs({
+ tab,
+ onChange,
+}: {
+ tab: Tab; // (role: selected tab, type: Tab)
+ onChange: (tab: Tab) => void; // (role: tab change handler, type: (Tab)=>void)
+}) {
+ const { t } = useContext(LocaleContext);
+ const baseBtn =
+ 'rounded-full px-4 py-1.5 text-sm transition whitespace-nowrap';
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/app/pages/ManagePage.tsx b/apps/desktop/src/app/pages/ManagePage.tsx
new file mode 100644
index 0000000..333882e
--- /dev/null
+++ b/apps/desktop/src/app/pages/ManagePage.tsx
@@ -0,0 +1,218 @@
+import { useContext } from 'react';
+import { TaskForm } from '../../features/task/components/TaskForm';
+import { TaskList } from '../../features/task/components/TaskList';
+import type {
+ Completion,
+ DayOfWeek,
+ Task,
+ TimeEntry,
+} from '../../shared/types';
+import { LocaleContext } from '../../i18n/context';
+import { Package, PackageOpen, RotateCwIcon } from 'lucide-react';
+
+export function ManagePage(props: {
+ tasks: Task[]; // (role: visible tasks after filters, type: Task[])
+ completions: Completion[]; // (role: completion logs, type: Completion[])
+ timeEntries: TimeEntry[]; // (role: time tracking logs, type: TimeEntry[])
+ todayYmd: string; // (role: YYYY-MM-DD, type: string)
+ todayDow: DayOfWeek; // (role: day-of-week, type: DayOfWeek)
+
+ nowIso: string; // (role: ui clock iso, type: string)
+ runningTaskIdToday: string | null; // (role: single running task id, type: string | null)
+
+ manageQuery: string; // (role: search query, type: string)
+ setManageQuery: (v: string) => void; // (role: set query, type: (string)=>void)
+
+ manageCategory: 'all' | Task['category']; // (role: selected category filter, type: union)
+ setManageCategory: (v: 'all' | Task['category']) => void; // (role: set category, type: (union)=>void)
+
+ showArchived: boolean; // (role: show archived toggle, type: boolean)
+ setShowArchived: (v: boolean) => void; // (role: toggle archived view, type: (boolean)=>void)
+
+ onReset: () => void; // (role: reset manage controls, type: ()=>void)
+
+ onCreate: Parameters[0]['onCreate'];
+ onUpdateTaskMeta: (input: {
+ taskId: string;
+ title: string;
+ description: string;
+ startYmd: string | null;
+ autoArchiveAfter: number | null;
+ }) => void;
+ onToggleToday: (task: Task) => void; // (role: toggle completion, type: (Task)=>void)
+ onArchive: (taskId: string) => void; // (role: archive task, type: (string)=>void)
+ onRestore: (taskId: string) => void; // (role: restore task, type: (string)=>void)
+ onDelete: (taskId: string) => void; // (role: delete task, type: (string)=>void)
+ onStartTimer: (task: Task) => void; // (role: start timer, type: (Task)=>void)
+ onStopTimer: (task: Task) => void; // (role: stop timer, type: (Task)=>void)
+ onError: (msg: string) => void; // (role: error handler, type: (string)=>void)
+}) {
+ const { t } = useContext(LocaleContext);
+
+ const {
+ tasks,
+ completions,
+ timeEntries,
+ todayYmd,
+ todayDow,
+ nowIso,
+ runningTaskIdToday,
+ manageQuery,
+ setManageQuery,
+ manageCategory,
+ setManageCategory,
+ showArchived,
+ setShowArchived,
+ onReset,
+ onCreate,
+ onUpdateTaskMeta,
+ onToggleToday,
+ onArchive,
+ onRestore,
+ onDelete,
+ onStartTimer,
+ onStopTimer,
+ onError,
+ } = props;
+
+ return (
+
+
+
+
+
+
+
+ {t('task.manageTasks')}
+
+
+ {t('task.manageTasksDescription')}
+
+
+
+
+
+ {tasks.length === 0 && (
+
+ {t('task.noTasksScheduledManage')}
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/desktop/src/app/pages/SchedulePage.tsx b/apps/desktop/src/app/pages/SchedulePage.tsx
new file mode 100644
index 0000000..80e8aef
--- /dev/null
+++ b/apps/desktop/src/app/pages/SchedulePage.tsx
@@ -0,0 +1,263 @@
+import { useContext, useMemo, useState } from 'react';
+import clsx from 'clsx';
+import type { Completion, Task } from '../../shared/types';
+import { buildWeekSchedule, WEEK_ORDER } from '../../domain/scheduleView';
+import {
+ buildWeekDates,
+ startOfWeekMonday,
+ toYmd,
+} from '../../shared/utils/date';
+import { LocaleContext } from '../../i18n/context';
+
+// (role: display helper, type: (number)=>string)
+function formatDuration(
+ minutes: number,
+ t: (key: string, params?: Record) => string,
+): string {
+ const m = Math.max(0, Math.floor(minutes || 0));
+ const h = Math.floor(m / 60);
+ const r = m % 60;
+
+ if (h <= 0) return `${m}${t('time.minuteShort')}`;
+ if (r === 0) return `${h}${t('time.hourShort')}`;
+ return `${h}${t('time.hourShort')} ${r}${t('time.minuteShort')}`;
+}
+
+// (role: block height unit, type: number)
+const HOUR_PX = 44;
+
+// (role: minutes->hour blocks, type: (number)=>number)
+function toHourBlocks(minutes: number): number {
+ return Math.max(1, Math.ceil((minutes || 0) / 60));
+}
+
+function normalizeWeekStart(ymd: string): string {
+ return toYmd(startOfWeekMonday(new Date(`${ymd}T00:00:00`)));
+}
+
+function shiftWeekStart(weekStartYmd: string, deltaDays: number): string {
+ const d = new Date(`${weekStartYmd}T00:00:00`);
+ d.setDate(d.getDate() + deltaDays);
+ return normalizeWeekStart(toYmd(d));
+}
+
+export function SchedulePage(props: {
+ tasks: Task[]; // (role: all tasks, type: Task[])
+ completions: Completion[]; // (role: completion logs, type: Completion[])
+ weekStartYmd: string; // (role: current-week monday, type: string (YYYY-MM-DD))
+ getMemoText?: (taskId: string, date: string) => string;
+ onOpenTask?: (taskId: string) => void;
+}) {
+ const { t } = useContext(LocaleContext);
+ const { tasks, completions, weekStartYmd, getMemoText, onOpenTask } = props;
+
+ const currentWeekStartYmd = useMemo(
+ () => normalizeWeekStart(weekStartYmd),
+ [weekStartYmd],
+ );
+ const [displayWeekStartYmd, setDisplayWeekStartYmd] =
+ useState(currentWeekStartYmd);
+
+ const normalizedWeekStartYmd = useMemo(
+ () => normalizeWeekStart(displayWeekStartYmd),
+ [displayWeekStartYmd],
+ );
+ const canGoNext = normalizedWeekStartYmd < currentWeekStartYmd;
+
+ const week = buildWeekSchedule(tasks, completions, normalizedWeekStartYmd, {
+ includeArchived: false,
+ getMemoText,
+ });
+
+ const weekDates = useMemo(
+ () => buildWeekDates(normalizedWeekStartYmd),
+ [normalizedWeekStartYmd],
+ );
+ const weekEndYmd = weekDates[6] ?? normalizedWeekStartYmd;
+
+ return (
+
+
+
+
+ {t('common.schedule')}
+
+
+
+
+
+
+
+
+ {t('schedule.weekRange', {
+ start: normalizedWeekStartYmd,
+ end: weekEndYmd,
+ })}
+
+
+ {t('note.scheduleDescription')}
+
+
+
+
+ {WEEK_ORDER.map((dow, dowIndex) => {
+ const items = week[dow];
+ const columnDateYmd = weekDates[dowIndex] ?? normalizedWeekStartYmd;
+
+ const totalMinutes = items.reduce(
+ (acc, it) => acc + (it.durationMinutes || 0),
+ 0,
+ );
+
+ const doneCount = items.reduce((acc, it) => {
+ const doneThatDay = completions.some(
+ (c) => c.taskId === it.taskId && c.date === columnDateYmd,
+ );
+ return acc + (doneThatDay ? 1 : 0);
+ }, 0);
+
+ return (
+
+
+
+
+ {t(`time.day.${dow}`)}
+
+
+ {columnDateYmd.slice(5)}
+
+
+
+
+
+ {formatDuration(totalMinutes, t)}
+
+
+ ({doneCount}/{items.length})
+
+
+
+
+ {items.length === 0 ? (
+
+ {t('task.noTasksInSchedule')}
+
+ ) : (
+
+ {items.map((it) => {
+ const blocks = toHourBlocks(it.durationMinutes);
+ const heightPx = blocks * HOUR_PX;
+
+ const doneThatDay = completions.some(
+ (c) => c.taskId === it.taskId && c.date === columnDateYmd,
+ );
+
+ const blockStyle = {
+ ['--h' as string]: `${heightPx}px`,
+ } as React.CSSProperties;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/desktop/src/app/pages/SettingsPage.tsx b/apps/desktop/src/app/pages/SettingsPage.tsx
new file mode 100644
index 0000000..4e3d176
--- /dev/null
+++ b/apps/desktop/src/app/pages/SettingsPage.tsx
@@ -0,0 +1,113 @@
+import { useContext, useEffect, useState } from 'react';
+import type { Locale } from '../../i18n';
+import { LocaleContext } from '../../i18n/context';
+import { getSetting, setSetting } from '../../infrastructure/tauri/store';
+import { requestNotifyPermission } from '../../infrastructure/notification';
+
+const TIMER_DONE_NOTIFY_KEY = 'settings.notifications.timerDone';
+
+export function SettingsPage() {
+ const { locale, setLocale, t } = useContext(LocaleContext);
+ const [timerDoneEnabled, setTimerDoneEnabled] = useState(true);
+ const [permissionHint, setPermissionHint] = useState('');
+
+ useEffect(() => {
+ let active = true;
+
+ void (async () => {
+ const saved = await getSetting(TIMER_DONE_NOTIFY_KEY, true);
+ if (!active) return;
+ setTimerDoneEnabled(Boolean(saved));
+ })();
+
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ // (role: change handler, type: (e: React.ChangeEvent)=>void)
+ const onChangeLocale = (e: React.ChangeEvent) => {
+ setLocale(e.target.value as Locale);
+ };
+
+ const onToggleTimerDoneNotify = async (
+ e: React.ChangeEvent,
+ ) => {
+ const next = e.target.checked;
+
+ if (!next) {
+ setPermissionHint('');
+ setTimerDoneEnabled(false);
+ await setSetting(TIMER_DONE_NOTIFY_KEY, false);
+ return;
+ }
+
+ const permission = await requestNotifyPermission();
+ if (permission === 'granted') {
+ setPermissionHint('');
+ setTimerDoneEnabled(true);
+ await setSetting(TIMER_DONE_NOTIFY_KEY, true);
+ return;
+ }
+
+ setTimerDoneEnabled(false);
+ await setSetting(TIMER_DONE_NOTIFY_KEY, false);
+ setPermissionHint(t('settings.notifications.timerDone.hintDenied'));
+ };
+
+ return (
+
+
+
+
+
+ {t('settings.language.title')}
+
+
+ {t('settings.language.desc')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('settings.notifications.timerDone.title')}
+
+
+ {t('settings.notifications.timerDone.desc')}
+
+ {permissionHint && (
+
{permissionHint}
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/app/pages/TodayPage.tsx b/apps/desktop/src/app/pages/TodayPage.tsx
new file mode 100644
index 0000000..3952d5d
--- /dev/null
+++ b/apps/desktop/src/app/pages/TodayPage.tsx
@@ -0,0 +1,263 @@
+import { useContext } from 'react';
+import { TaskList } from '../../features/task/components/TaskList';
+import { PeriodStatsPanel } from '../../features/statistics/components/PeriodStatsPanel';
+import type {
+ Task,
+ Completion,
+ DayOfWeek,
+ TimeEntry,
+} from '../../shared/types';
+import type { TodayStats } from '../../domain/stats/stats';
+import { diffMinutes } from '../../shared/utils/date';
+import { LocaleContext } from '../../i18n/context';
+
+// (role: clamp helper, type: (number, number, number)=>number)
+function clamp(v: number, min: number, max: number): number {
+ return Math.min(max, Math.max(min, v));
+}
+
+function ProgressBar(props: {
+ value: number; // (role: progress percent, type: number)
+}) {
+ const pct = clamp(Number.isFinite(props.value) ? props.value : 0, 0, 100);
+
+ return (
+
+ );
+}
+
+function formatMinutes(
+ m: number,
+ t: (key: string, params?: Record) => string,
+): string {
+ const mm = Math.max(0, Math.floor(m || 0));
+ const h = Math.floor(mm / 60);
+ const r = mm % 60;
+ if (h <= 0) return `${r}${t('time.minuteShort')}`;
+ return `${h}${t('time.hourShort')} ${r}${t('time.minuteShort')}`;
+}
+
+export function TodayPage(props: {
+ todayYmd: string; // (role: YYYY-MM-DD, type: string)
+ todayDow: DayOfWeek; // (role: day-of-week, type: DayOfWeek)
+
+ tasks: Task[]; // (role: all tasks, type: Task[])
+ todayTasks: Task[]; // (role: tasks scheduled today, type: Task[])
+
+ todayStats: TodayStats; // (role: today stats, type: TodayStats)
+
+ completions: Completion[]; // (role: completions, type: Completion[])
+ timeEntries: TimeEntry[]; // (role: time tracking logs, type: TimeEntry[])
+
+ nowIso: string; // (role: ui clock iso, type: string)
+ runningTaskIdToday: string | null; // (role: single running task id, type: string | null)
+
+ getMemoText: (taskId: string, date: string) => string;
+ onSaveMemo: (input: { taskId: string; date: string; text: string }) => void;
+
+ onToggleToday: (task: Task) => void; // (role: toggle completion, type: (Task)=>void)
+ onArchive: (taskId: string) => void; // (role: archive task, type: (string)=>void)
+ onStartTimer: (task: Task) => void; // (role: start timer, type: (Task)=>void)
+ onStopTimer: (task: Task) => void; // (role: stop timer, type: (Task)=>void)
+ onError: (msg: string) => void; // (role: error handler, type: (string)=>void)
+}) {
+ const { t } = useContext(LocaleContext);
+
+ const {
+ todayYmd,
+ todayDow,
+ tasks,
+ todayTasks,
+ todayStats,
+
+ completions,
+ timeEntries,
+ nowIso,
+ runningTaskIdToday,
+ getMemoText,
+ onSaveMemo,
+ onToggleToday,
+ onArchive,
+ onStartTimer,
+ onStopTimer,
+ onError,
+ } = props;
+
+ const plannedMinutesToday = todayTasks.reduce(
+ (acc, t) => acc + Math.max(0, t.durationMinutes || 0),
+ 0,
+ );
+
+ const todayTaskIdSet = new Set(todayTasks.map((t) => t.id));
+
+ const doneTaskIdSet = new Set(
+ (completions ?? [])
+ .filter((c) => c.date === todayYmd && todayTaskIdSet.has(c.taskId))
+ .map((c) => c.taskId),
+ );
+
+ const measuredByTaskId = new Map();
+
+ (timeEntries ?? []).forEach((e) => {
+ if (e.date !== todayYmd) return;
+ if (!todayTaskIdSet.has(e.taskId)) return;
+
+ const minutes =
+ e.endedAt == null ? diffMinutes(e.startedAt, nowIso) : e.minutes || 0;
+
+ measuredByTaskId.set(
+ e.taskId,
+ (measuredByTaskId.get(e.taskId) || 0) + minutes,
+ );
+ });
+
+ const spentMinutesToday = todayTasks.reduce((acc, t) => {
+ const planned = Math.max(0, t.durationMinutes || 0);
+ if (doneTaskIdSet.has(t.id)) return acc + planned;
+ return acc + (measuredByTaskId.get(t.id) || 0);
+ }, 0);
+
+ const timeProgressPct =
+ plannedMinutesToday <= 0
+ ? 0
+ : clamp((spentMinutesToday / plannedMinutesToday) * 100, 0, 100);
+
+ return (
+
+
+
+
+
+
+
+
+ {t('common.time')}
+
+
+
+
+ {formatMinutes(spentMinutesToday, t)}
+
+
+ / {formatMinutes(plannedMinutesToday, t)}
+
+
+
+
+
+ {timeProgressPct.toFixed(0)}%
+
+
+
+
+
+
+ {t('time.basedOnTodayPlannedMinutes')}
+
+
+
+
+
+
+ {t('task.todayTasks')}
+
+
+ {t('task.todayTasksDescription')}
+
+
+
+
+
+ {todayTasks.length === 0 && (
+
+ {t('task.noTasksScheduledToday')}
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/desktop/src/app/store/useDailyCheckStore.ts b/apps/desktop/src/app/store/useDailyCheckStore.ts
new file mode 100644
index 0000000..f52b853
--- /dev/null
+++ b/apps/desktop/src/app/store/useDailyCheckStore.ts
@@ -0,0 +1,529 @@
+import { create } from 'zustand';
+import type {
+ Category,
+ Completion,
+ DayOfWeek,
+ Task,
+ TaskDailyMemo,
+ TimeEntry,
+} from '../../shared/types';
+import { loadAppData, replaceAllAppData } from '../../infrastructure/storage';
+import { diffMinutes, toYmd } from '../../shared/utils/date';
+import {
+ shouldAutoArchive,
+ toggleCompletion as toggleCompletionDomain,
+} from '../../domain/completion';
+import { createTaskEntity } from '../../domain/task/taskFactory';
+import { getNotifier } from '../di/notifierDI';
+import { upsertDailyMemo } from '../../domain/memo';
+
+export type Filter = 'all' | Category;
+
+type PersistedCollections = Pick<
+ DailyCheckState,
+ 'tasks' | 'completions' | 'timeEntries' | 'taskDailyMemos'
+>;
+
+interface DailyCheckState {
+ hydrated: boolean;
+ tasks: Task[];
+ completions: Completion[];
+ timeEntries: TimeEntry[];
+ taskDailyMemos: TaskDailyMemo[];
+ filter: Filter;
+ errorMsg: string;
+
+ hydrate: () => Promise;
+ setFilter: (filter: Filter) => void;
+ clearError: () => void;
+
+ createTask: (input: {
+ title: string;
+ description: string;
+ category: Category;
+ durationMinutes: number;
+ startYmd?: string | null;
+ autoArchiveAfter?: number | null;
+ customDays?: DayOfWeek[];
+ }) => void;
+
+ updateTaskMeta: (input: {
+ taskId: string;
+ title: string;
+ description: string;
+ startYmd?: string | null;
+ autoArchiveAfter?: number | null;
+ }) => void;
+
+ archiveTask: (taskId: string) => void;
+ restoreTask: (taskId: string) => void;
+ deleteTask: (taskId: string) => void;
+ toggleToday: (input: { taskId: string; today: Date }) => void;
+ setDailyMemo: (input: { taskId: string; date: string; text: string }) => void;
+ startTimer: (input: { taskId: string; today: Date }) => void;
+ stopTimer: (input: { taskId: string; today: Date }) => void;
+ autoStopIfReached: (input: { today: Date }) => string[];
+}
+
+function uid(): string {
+ return `${Date.now()}_${Math.random().toString(16).slice(2)}`;
+}
+
+function formatError(error: unknown): string {
+ if (error instanceof Error) {
+ return error.stack || error.message;
+ }
+
+ if (typeof error === 'string') {
+ return error;
+ }
+
+ try {
+ return JSON.stringify(error);
+ } catch {
+ return 'Unknown error';
+ }
+}
+
+function persistCollections(
+ next: PersistedCollections,
+ failureMessage: string,
+): void {
+ void replaceAllAppData(next).catch((error) => {
+ console.error(failureMessage, error);
+ useDailyCheckStore.setState({
+ errorMsg: `${failureMessage} ${formatError(error)}`,
+ });
+ });
+}
+
+export const useDailyCheckStore = create((set, get) => ({
+ hydrated: false,
+ tasks: [],
+ completions: [],
+ timeEntries: [],
+ taskDailyMemos: [],
+ filter: 'all',
+ errorMsg: '',
+
+ hydrate: async () => {
+ try {
+ const data = await loadAppData();
+ set({
+ ...data,
+ hydrated: true,
+ errorMsg: '',
+ });
+ } catch (error) {
+ console.error('Failed to hydrate app data', error);
+ set({
+ hydrated: true,
+ errorMsg: `Failed to load app data. ${formatError(error)}`,
+ });
+ }
+ },
+
+ setFilter: (filter) => set({ filter }),
+ clearError: () => set({ errorMsg: '' }),
+
+ createTask: ({
+ title,
+ description,
+ category,
+ durationMinutes,
+ startYmd,
+ autoArchiveAfter,
+ customDays,
+ }) => {
+ const t = title.trim();
+ if (!t) {
+ set({ errorMsg: 'Title is required.' });
+ return;
+ }
+
+ const createdAtYmd = toYmd(new Date());
+ const normalizedStartYmd =
+ startYmd == null || String(startYmd).trim() === ''
+ ? null
+ : String(startYmd).trim();
+ if (normalizedStartYmd && normalizedStartYmd < createdAtYmd) {
+ set({ errorMsg: 'Start date cannot be earlier than created date.' });
+ return;
+ }
+
+ try {
+ const task = createTaskEntity({
+ id: uid(),
+ title: t,
+ description,
+ category,
+ customDays,
+ durationMinutes,
+ startYmd: normalizedStartYmd,
+ autoArchiveAfter,
+ nowIso: new Date().toISOString(),
+ });
+
+ const nextTasks = [task, ...get().tasks];
+ const next = {
+ tasks: nextTasks,
+ completions: get().completions,
+ timeEntries: get().timeEntries,
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({ tasks: nextTasks, errorMsg: '' });
+ persistCollections(next, 'Failed to save task.');
+ } catch (e) {
+ set({
+ errorMsg: e instanceof Error ? e.message : 'Failed to create task.',
+ });
+ }
+ },
+
+ updateTaskMeta: ({
+ taskId,
+ title,
+ description,
+ startYmd,
+ autoArchiveAfter,
+ }) => {
+ const normalizedTitle = title.trim();
+ if (!normalizedTitle) {
+ set({ errorMsg: 'Title is required.' });
+ return;
+ }
+
+ const numericThreshold =
+ autoArchiveAfter == null ? null : Number(autoArchiveAfter);
+ const normalizedThreshold =
+ numericThreshold == null ||
+ !Number.isInteger(numericThreshold) ||
+ numericThreshold < 1
+ ? null
+ : numericThreshold;
+
+ const normalizedStartYmdRaw =
+ startYmd == null ? null : String(startYmd).trim();
+ const normalizedStartYmd =
+ normalizedStartYmdRaw == null || normalizedStartYmdRaw === ''
+ ? null
+ : /^\d{4}-\d{2}-\d{2}$/.test(normalizedStartYmdRaw)
+ ? normalizedStartYmdRaw
+ : null;
+ const targetTask = get().tasks.find((task) => task.id === taskId);
+ if (!targetTask) {
+ set({ errorMsg: 'Task not found.' });
+ return;
+ }
+
+ const createdAtYmd = targetTask.createdAt.slice(0, 10);
+ if (normalizedStartYmd && normalizedStartYmd < createdAtYmd) {
+ set({ errorMsg: 'Start date cannot be earlier than created date.' });
+ return;
+ }
+
+ const nextTasks = get().tasks.map((task) =>
+ task.id === taskId
+ ? {
+ ...task,
+ title: normalizedTitle,
+ description: description.trim(),
+ startYmd: normalizedStartYmd,
+ autoArchiveAfter: normalizedThreshold,
+ }
+ : task,
+ );
+
+ const next = {
+ tasks: nextTasks,
+ completions: get().completions,
+ timeEntries: get().timeEntries,
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({ tasks: nextTasks, errorMsg: '' });
+ persistCollections(next, 'Failed to update task.');
+ },
+
+ archiveTask: (taskId) => {
+ const nextTasks = get().tasks.map((t) =>
+ t.id === taskId ? { ...t, isActive: false } : t,
+ );
+ const next = {
+ tasks: nextTasks,
+ completions: get().completions,
+ timeEntries: get().timeEntries,
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({ tasks: nextTasks, errorMsg: '' });
+ persistCollections(next, 'Failed to archive task.');
+ },
+
+ restoreTask: (taskId) => {
+ const nextTasks = get().tasks.map((t) =>
+ t.id === taskId ? { ...t, isActive: true } : t,
+ );
+ const next = {
+ tasks: nextTasks,
+ completions: get().completions,
+ timeEntries: get().timeEntries,
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({ tasks: nextTasks, errorMsg: '' });
+ persistCollections(next, 'Failed to restore task.');
+ },
+
+ deleteTask: (taskId) => {
+ const nextTasks = get().tasks.filter((t) => t.id !== taskId);
+ const nextCompletions = get().completions.filter(
+ (c) => c.taskId !== taskId,
+ );
+ const nextTimeEntries = get().timeEntries.filter(
+ (e) => e.taskId !== taskId,
+ );
+ const nextMemos = get().taskDailyMemos.filter((m) => m.taskId !== taskId);
+
+ const next = {
+ tasks: nextTasks,
+ completions: nextCompletions,
+ timeEntries: nextTimeEntries,
+ taskDailyMemos: nextMemos,
+ };
+
+ set({
+ ...next,
+ errorMsg: '',
+ });
+ persistCollections(next, 'Failed to delete task.');
+ },
+
+ toggleToday: ({ taskId, today }) => {
+ const date = toYmd(today);
+
+ const prevCompletions = get().completions;
+ const nextCompletions = toggleCompletionDomain(
+ prevCompletions,
+ taskId,
+ date,
+ );
+
+ const wasDone = prevCompletions.some(
+ (c) => c.taskId === taskId && c.date === date,
+ );
+
+ let nextTimeEntries = get().timeEntries;
+ let nextTasks = get().tasks;
+
+ if (wasDone) {
+ nextTimeEntries = nextTimeEntries.filter(
+ (e) => !(e.taskId === taskId && e.date === date),
+ );
+ } else {
+ const toggledTask = nextTasks.find((t) => t.id === taskId);
+ if (
+ toggledTask &&
+ toggledTask.isActive &&
+ shouldAutoArchive(toggledTask, nextCompletions)
+ ) {
+ nextTasks = nextTasks.map((task) =>
+ task.id === taskId ? { ...task, isActive: false } : task,
+ );
+
+ const notifier = getNotifier();
+ notifier.notify({
+ level: 'info',
+ message: `Auto-archived: ${toggledTask.title}`,
+ });
+ }
+ }
+
+ const next = {
+ tasks: nextTasks,
+ completions: nextCompletions,
+ timeEntries: nextTimeEntries,
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({
+ tasks: nextTasks,
+ completions: nextCompletions,
+ timeEntries: nextTimeEntries,
+ errorMsg: '',
+ });
+ persistCollections(next, 'Failed to update completion.');
+ },
+
+ setDailyMemo: ({ taskId, date, text }) => {
+ const nextMemos = upsertDailyMemo(get().taskDailyMemos, {
+ taskId,
+ date,
+ text,
+ updatedAt: new Date().toISOString(),
+ });
+
+ const next = {
+ tasks: get().tasks,
+ completions: get().completions,
+ timeEntries: get().timeEntries,
+ taskDailyMemos: nextMemos,
+ };
+
+ set({ taskDailyMemos: nextMemos, errorMsg: '' });
+ persistCollections(next, 'Failed to save memo.');
+ },
+
+ startTimer: ({ taskId, today }) => {
+ const date = toYmd(today);
+ const nowIso = new Date().toISOString();
+
+ const entries = get().timeEntries;
+ const alreadyRunningSameTask = entries.some(
+ (e) => e.taskId === taskId && e.date === date && e.endedAt == null,
+ );
+ if (alreadyRunningSameTask) {
+ set({ errorMsg: 'Timer is already running for this task today.' });
+ return;
+ }
+
+ const nextTimeEntries = entries.map((e) => {
+ if (e.date !== date) return e;
+ if (e.endedAt != null) return e;
+
+ const minutes = diffMinutes(e.startedAt, nowIso);
+ return { ...e, endedAt: nowIso, minutes };
+ });
+
+ const entry: TimeEntry = {
+ id: uid(),
+ taskId,
+ date,
+ startedAt: nowIso,
+ endedAt: null,
+ minutes: 0,
+ };
+
+ const next = {
+ tasks: get().tasks,
+ completions: get().completions,
+ timeEntries: [entry, ...nextTimeEntries],
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({ timeEntries: next.timeEntries, errorMsg: '' });
+ persistCollections(next, 'Failed to start timer.');
+ },
+
+ stopTimer: ({ taskId, today }) => {
+ const date = toYmd(today);
+
+ const idx = get().timeEntries.findIndex(
+ (e) => e.taskId === taskId && e.date === date && e.endedAt == null,
+ );
+ if (idx === -1) {
+ set({ errorMsg: 'No running timer for this task today.' });
+ return;
+ }
+
+ const nowIso = new Date().toISOString();
+ const cur = get().timeEntries[idx];
+ const minutes = diffMinutes(cur.startedAt, nowIso);
+
+ const updated: TimeEntry = { ...cur, endedAt: nowIso, minutes };
+ const nextTimeEntries = [...get().timeEntries];
+ nextTimeEntries[idx] = updated;
+
+ const next = {
+ tasks: get().tasks,
+ completions: get().completions,
+ timeEntries: nextTimeEntries,
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({ timeEntries: nextTimeEntries, errorMsg: '' });
+ persistCollections(next, 'Failed to stop timer.');
+ },
+
+ autoStopIfReached: ({ today }) => {
+ if (!get().hydrated) return [];
+
+ const date = toYmd(today);
+ const nowIso = new Date().toISOString();
+
+ const entries = get().timeEntries;
+ const tasks = get().tasks;
+
+ const nextEntries = [...entries];
+ const nextCompletions = [...get().completions];
+ const finishedTaskTitles: string[] = [];
+ let changed = false;
+
+ for (let i = 0; i < nextEntries.length; i += 1) {
+ const e = nextEntries[i];
+
+ if (e.date !== date) continue;
+ if (e.endedAt != null) continue;
+
+ const task = tasks.find((t) => t.id === e.taskId);
+ if (!task) continue;
+
+ const doneMinutes = nextEntries
+ .filter(
+ (x) =>
+ x.taskId === e.taskId &&
+ x.date === date &&
+ x.endedAt != null &&
+ Number.isFinite(x.minutes),
+ )
+ .reduce((acc, x) => acc + (x.minutes || 0), 0);
+
+ const runningMinutes = diffMinutes(e.startedAt, nowIso);
+ const total = doneMinutes + runningMinutes;
+
+ if (total >= task.durationMinutes) {
+ nextEntries[i] = { ...e, endedAt: nowIso, minutes: runningMinutes };
+ finishedTaskTitles.push(task.title);
+ changed = true;
+
+ const notifier = getNotifier();
+ notifier.notify({
+ level: 'info',
+ message: `Auto-stopped: ${task.title} (+${runningMinutes}m)`,
+ });
+
+ const alreadyDone = nextCompletions.some(
+ (c) => c.taskId === e.taskId && c.date === date,
+ );
+
+ if (!alreadyDone) {
+ nextCompletions.push({ taskId: e.taskId, date });
+
+ notifier.notify({
+ level: 'success',
+ message: `Auto-completed: ${task.title}`,
+ });
+ }
+ }
+ }
+
+ if (!changed) return [];
+
+ const next = {
+ tasks: get().tasks,
+ completions: nextCompletions,
+ timeEntries: nextEntries,
+ taskDailyMemos: get().taskDailyMemos,
+ };
+
+ set({
+ timeEntries: nextEntries,
+ completions: nextCompletions,
+ errorMsg: '',
+ });
+ persistCollections(next, 'Failed to auto-stop timer.');
+
+ return finishedTaskTitles;
+ },
+}));
diff --git a/apps/desktop/src/app/ui/ToastHost.tsx b/apps/desktop/src/app/ui/ToastHost.tsx
new file mode 100644
index 0000000..905958e
--- /dev/null
+++ b/apps/desktop/src/app/ui/ToastHost.tsx
@@ -0,0 +1,70 @@
+import { useEffect, useState } from 'react';
+import type { NotifyLevel } from '../../domain/notifier';
+import { subscribeToast } from '../../infrastructure/notification/ToastBus';
+
+// (role: toast view model, type: interface)
+interface ToastVM {
+ id: string; // (role: unique id, type: string)
+ level: NotifyLevel; // (role: level, type: NotifyLevel)
+ message: string; // (role: message, type: string)
+}
+
+// (role: id generator, type: () => string)
+function uid(): string {
+ return `${Date.now()}_${Math.random().toString(16).slice(2)}`;
+}
+
+export function ToastHost(props: {
+ durationMs?: number; // (role: auto hide, type: number | undefined)
+}) {
+ const durationMs = props.durationMs ?? 2000;
+
+ const [toast, setToast] = useState(null);
+
+ useEffect(() => {
+ const unsub = subscribeToast((p) => {
+ const item: ToastVM = { id: uid(), level: p.level, message: p.message };
+ setToast(item);
+
+ window.setTimeout(() => {
+ setToast((cur) => (cur?.id === item.id ? null : cur));
+ }, durationMs);
+ });
+
+ return unsub;
+ }, [durationMs]);
+
+ if (!toast) return null;
+
+ // level별 스타일은 최소로만(원하면 더 세분화 가능)
+ const border =
+ toast.level === 'error'
+ ? 'border-red-400/30'
+ : toast.level === 'warning'
+ ? 'border-amber-400/30'
+ : toast.level === 'success'
+ ? 'border-emerald-400/30'
+ : 'border-zinc-800';
+
+ const bg =
+ toast.level === 'error'
+ ? 'bg-red-400/10'
+ : toast.level === 'warning'
+ ? 'bg-amber-400/10'
+ : toast.level === 'success'
+ ? 'bg-emerald-400/10'
+ : 'bg-zinc-950/90';
+
+ return (
+
+ );
+}
diff --git a/apps/desktop/src/domain/completion/completion.ts b/apps/desktop/src/domain/completion/completion.ts
new file mode 100644
index 0000000..f224462
--- /dev/null
+++ b/apps/desktop/src/domain/completion/completion.ts
@@ -0,0 +1,39 @@
+import type { Completion, Task } from '../../shared/types';
+
+// (role: toggle completion for (taskId, date), type: (Completion[], string, string) => Completion[])
+export function toggleCompletion(
+ completions: Completion[],
+ taskId: string,
+ date: string,
+): Completion[] {
+ const exists = completions.some(
+ (c) => c.taskId === taskId && c.date === date,
+ );
+ if (exists) {
+ return completions.filter((c) => !(c.taskId === taskId && c.date === date));
+ }
+ return [...completions, { taskId, date }];
+}
+
+// (role: check if task completed on a date, type: (Completion[], string, string) => boolean)
+export function isDoneOn(
+ completions: Completion[],
+ taskId: string,
+ date: string,
+): boolean {
+ return completions.some((c) => c.taskId === taskId && c.date === date);
+}
+
+// (role: get completion count per task across all dates, type: (Completion[], string)=>number)
+export function getCompletionCountForTask(
+ completions: Completion[],
+ taskId: string,
+): number {
+ return completions.filter((c) => c.taskId === taskId).length;
+}
+
+// (role: check auto archive condition, type: (Task, Completion[])=>boolean)
+export function shouldAutoArchive(task: Task, completions: Completion[]): boolean {
+ if (task.autoArchiveAfter == null) return false;
+ return getCompletionCountForTask(completions, task.id) >= task.autoArchiveAfter;
+}
diff --git a/apps/desktop/src/domain/completion/index.ts b/apps/desktop/src/domain/completion/index.ts
new file mode 100644
index 0000000..a1863c0
--- /dev/null
+++ b/apps/desktop/src/domain/completion/index.ts
@@ -0,0 +1 @@
+export * from './completion';
diff --git a/apps/desktop/src/domain/memo/index.ts b/apps/desktop/src/domain/memo/index.ts
new file mode 100644
index 0000000..ba556f3
--- /dev/null
+++ b/apps/desktop/src/domain/memo/index.ts
@@ -0,0 +1 @@
+export * from './memo';
diff --git a/apps/desktop/src/domain/memo/memo.ts b/apps/desktop/src/domain/memo/memo.ts
new file mode 100644
index 0000000..859132c
--- /dev/null
+++ b/apps/desktop/src/domain/memo/memo.ts
@@ -0,0 +1,49 @@
+import type { TaskDailyMemo } from '../../shared/types';
+
+// (role: upsert per-task per-day memo, type: (TaskDailyMemo[], input)=>TaskDailyMemo[])
+export function upsertDailyMemo(
+ memos: TaskDailyMemo[],
+ input: {
+ taskId: string;
+ date: string;
+ text: string;
+ updatedAt: string;
+ },
+): TaskDailyMemo[] {
+ const text = input.text.trim();
+
+ // Empty text deletes memo entry for the day.
+ if (!text) {
+ return memos.filter(
+ (m) => !(m.taskId === input.taskId && m.date === input.date),
+ );
+ }
+
+ const index = memos.findIndex(
+ (m) => m.taskId === input.taskId && m.date === input.date,
+ );
+
+ const next: TaskDailyMemo = {
+ id: `${input.taskId}_${input.date}`,
+ taskId: input.taskId,
+ date: input.date,
+ text,
+ updatedAt: input.updatedAt,
+ };
+
+ if (index === -1) return [next, ...memos];
+
+ const copied = [...memos];
+ copied[index] = next;
+ return copied;
+}
+
+// (role: read memo text by task/date, type: (TaskDailyMemo[], string, string)=>string)
+export function getDailyMemoText(
+ memos: TaskDailyMemo[],
+ taskId: string,
+ date: string,
+): string {
+ const found = memos.find((m) => m.taskId === taskId && m.date === date);
+ return found?.text ?? '';
+}
diff --git a/apps/desktop/src/domain/notifier.ts b/apps/desktop/src/domain/notifier.ts
new file mode 100644
index 0000000..5b8f1c2
--- /dev/null
+++ b/apps/desktop/src/domain/notifier.ts
@@ -0,0 +1,11 @@
+// (role: notification level, type: union)
+export type NotifyLevel = 'info' | 'success' | 'warning' | 'error';
+
+// (role: notifier contract, type: interface)
+export interface Notifier {
+ notify: (input: {
+ level: NotifyLevel; // (role: severity level, type: NotifyLevel)
+ message: string; // (role: user-facing message, type: string)
+ meta?: Record; // (role: optional metadata, type: Record | undefined)
+ }) => void;
+}
diff --git a/apps/desktop/src/domain/schedule/index.ts b/apps/desktop/src/domain/schedule/index.ts
new file mode 100644
index 0000000..741e6ab
--- /dev/null
+++ b/apps/desktop/src/domain/schedule/index.ts
@@ -0,0 +1 @@
+export * from './schedule';
diff --git a/apps/desktop/src/domain/schedule/schedule.ts b/apps/desktop/src/domain/schedule/schedule.ts
new file mode 100644
index 0000000..b1f364e
--- /dev/null
+++ b/apps/desktop/src/domain/schedule/schedule.ts
@@ -0,0 +1,25 @@
+import type { Category, DayOfWeek, Task } from '../../shared/types';
+
+export const ALL_DAYS: DayOfWeek[] = [
+ 'Mon',
+ 'Tue',
+ 'Wed',
+ 'Thu',
+ 'Fri',
+ 'Sat',
+ 'Sun',
+];
+
+export const FIXED_DAYS: Record<
+ Exclude,
+ readonly DayOfWeek[]
+> = {
+ weekday: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
+ weekend: ['Sat', 'Sun'],
+ daily: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+};
+
+// (role: check if task scheduled on given day, type: (Task, DayOfWeek) => boolean)
+export function isScheduledOn(task: Task, dow: DayOfWeek): boolean {
+ return task.daysOfWeek.includes(dow);
+}
diff --git a/apps/desktop/src/domain/schedule/scheduleLimit.ts b/apps/desktop/src/domain/schedule/scheduleLimit.ts
new file mode 100644
index 0000000..51bdd73
--- /dev/null
+++ b/apps/desktop/src/domain/schedule/scheduleLimit.ts
@@ -0,0 +1,118 @@
+import { buildWeekDates, dayOfWeek, toYmd } from '../../shared/utils/date';
+import { isScheduledOn } from './schedule';
+import type { Completion, Task } from '../../shared/types';
+
+// (role: resolve backlog limit, type: (Task)=>number|null)
+function toBacklogLimit(task: Task): number | null {
+ // repeatCount is primary schedule backlog limit.
+ // Keep temporary autoArchiveAfter fallback only for legacy data that used it as the only limit.
+ const raw = task.repeatCount ?? task.autoArchiveAfter ?? null;
+ if (raw == null) return null;
+
+ const parsed = Math.floor(Number(raw));
+ if (!Number.isFinite(parsed) || parsed < 1) return null;
+ return parsed;
+}
+
+// (role: derive effective schedule start ymd, type: (Task)=>string)
+function effectiveStartYmd(task: Task): string {
+ const createdAtYmd = toYmd(new Date(task.createdAt));
+ const userStartYmd = (task.startYmd ?? '').trim();
+
+ if (!userStartYmd) return createdAtYmd;
+ return userStartYmd < createdAtYmd ? createdAtYmd : userStartYmd;
+}
+
+// (role: unique done dates for a task, type: (Completion[], string)=>Set)
+function doneDateSetAll(
+ completions: Completion[],
+ taskId: string,
+): Set {
+ return new Set(
+ (completions ?? []).filter((c) => c.taskId === taskId).map((c) => c.date),
+ );
+}
+
+// (role: done dates within a week, type: (Completion[], string, string, string)=>Set)
+function doneDateSetInWeek(
+ completions: Completion[],
+ taskId: string,
+ weekStartYmd: string,
+ weekEndYmd: string,
+): Set {
+ return new Set(
+ (completions ?? [])
+ .filter(
+ (c) =>
+ c.taskId === taskId && c.date >= weekStartYmd && c.date <= weekEndYmd,
+ )
+ .map((c) => c.date),
+ );
+}
+
+// (role: pick visible weekly slots for a task, type: (Task, string, Completion[])=>string[])
+export function pickWeeklySlots(
+ task: Task,
+ weekStartYmd: string,
+ completions: Completion[],
+): string[] {
+ const weekDates = buildWeekDates(weekStartYmd);
+ const weekEndYmd = weekDates[weekDates.length - 1];
+
+ const cutoffYmd = effectiveStartYmd(task);
+
+ // (role: scheduled dates within the week, type: string[])
+ const scheduledDates = weekDates.filter((ymd) => {
+ if (ymd < cutoffYmd) return false;
+ const date = new Date(`${ymd}T00:00:00`);
+ return isScheduledOn(task, dayOfWeek(date));
+ });
+ const doneThisWeek = doneDateSetInWeek(
+ completions,
+ task.id,
+ weekStartYmd,
+ weekEndYmd,
+ );
+ // Done slots are based on weekDates (not scheduledDates), so completed history never disappears.
+ const doneDatesThisWeek = weekDates.filter(
+ (d) => d >= cutoffYmd && doneThisWeek.has(d),
+ );
+
+ const backlogLimit = toBacklogLimit(task);
+
+ // Unlimited schedule: show scheduled slots + any done slots in this week.
+ if (backlogLimit == null) {
+ const visible = new Set([...doneDatesThisWeek, ...scheduledDates]);
+ return weekDates.filter((d) => d >= cutoffYmd && visible.has(d));
+ }
+
+ // Backlog model: repeatCount is lifetime total target.
+ const doneAll = doneDateSetAll(completions, task.id);
+ const doneTotal = doneAll.size;
+ const remainingTotal = Math.max(0, backlogLimit - doneTotal);
+
+ // Backlog exhausted: keep only done slots in displayed week; no planned slots.
+ if (remainingTotal === 0) {
+ return doneDatesThisWeek;
+ }
+
+ // Planned slots this week are capped by remaining lifetime backlog.
+ const remainingToShowThisWeek = remainingTotal;
+ const queuedDates = scheduledDates
+ .filter((d) => !doneThisWeek.has(d))
+ .slice(0, remainingToShowThisWeek);
+
+ const visible = new Set([...doneDatesThisWeek, ...queuedDates]);
+ // Preserve week order and effective start cutoff.
+ return weekDates.filter((d) => d >= cutoffYmd && visible.has(d));
+}
+
+// (role: check if task is visible in week on date, type: (Task, string, string, Completion[])=>boolean)
+export function isVisibleInWeek(
+ task: Task,
+ dateYmd: string,
+ weekStartYmd: string,
+ completions: Completion[],
+): boolean {
+ return pickWeeklySlots(task, weekStartYmd, completions).includes(dateYmd);
+}
diff --git a/apps/desktop/src/domain/schedule/scheduleView.ts b/apps/desktop/src/domain/schedule/scheduleView.ts
new file mode 100644
index 0000000..5b9e8ba
--- /dev/null
+++ b/apps/desktop/src/domain/schedule/scheduleView.ts
@@ -0,0 +1,109 @@
+import type { Completion, DayOfWeek, Task } from '../../shared/types';
+import { buildWeekDates, dayOfWeek, startOfWeekMonday, toYmd } from '../../shared/utils/date';
+import { isVisibleInWeek } from './scheduleLimit';
+
+// (role: day schedule item, type: interface)
+export interface DayScheduleItem {
+ taskId: string; // (role: task id, type: string)
+ title: string; // (role: task title, type: string)
+ description?: string; // (role: task description, type: string | undefined)
+ memoText?: string; // (role: daily memo text, type: string | undefined)
+ durationMinutes: number; // (role: planned minutes, type: number)
+ category: Task['category']; // (role: category, type: Task['category'])
+ isActive: boolean; // (role: active flag, type: boolean)
+}
+
+// (role: schedule by day-of-week, type: Record)
+export type WeekSchedule = Record;
+
+// (role: fixed day order, type: DayOfWeek[])
+export const WEEK_ORDER: DayOfWeek[] = [
+ 'Mon',
+ 'Tue',
+ 'Wed',
+ 'Thu',
+ 'Fri',
+ 'Sat',
+ 'Sun',
+];
+
+// (role: day label map, type: Record)
+export const WEEK_LABEL_KO: Record = {
+ Mon: '월',
+ Tue: '화',
+ Wed: '수',
+ Thu: '목',
+ Fri: '금',
+ Sat: '토',
+ Sun: '일',
+};
+
+// (role: build week schedule view model, type: (Task[], boolean)=>WeekSchedule)
+export function buildWeekSchedule(
+ tasks: Task[],
+ completions: Completion[],
+ weekStartYmd: string,
+ options?: {
+ includeArchived?: boolean; // (role: include inactive tasks, type: boolean | undefined)
+ getMemoText?: (taskId: string, dateYmd: string) => string | undefined;
+ },
+): WeekSchedule {
+ const includeArchived = options?.includeArchived ?? false;
+ const getMemoText = options?.getMemoText;
+ const normalizedWeekStartYmd = toYmd(
+ startOfWeekMonday(new Date(`${weekStartYmd}T00:00:00`)),
+ );
+ const weekDates = buildWeekDates(normalizedWeekStartYmd);
+
+ // 초기화
+ const base: WeekSchedule = {
+ Mon: [],
+ Tue: [],
+ Wed: [],
+ Thu: [],
+ Fri: [],
+ Sat: [],
+ Sun: [],
+ };
+
+ const filtered = includeArchived ? tasks : tasks.filter((t) => t.isActive);
+
+ for (const t of filtered) {
+ for (const ymd of weekDates) {
+ if (!isVisibleInWeek(t, ymd, normalizedWeekStartYmd, completions))
+ continue;
+
+ // 완료 여부 먼저 계산 (완료면 무조건 표시해야 하므로)
+ const doneThatDay = (completions ?? []).some(
+ (c) => c.taskId === t.id && c.date === ymd,
+ );
+
+ const dow = dayOfWeek(new Date(`${ymd}T00:00:00`));
+
+ // 완료가 아니면, 기존 룰대로 "그 요일에 스케줄된 task만"
+ if (!doneThatDay && !t.daysOfWeek.includes(dow)) continue;
+
+ base[dow].push({
+ taskId: t.id,
+ title: t.title,
+ description: t.description,
+ memoText: getMemoText?.(t.id, ymd) ?? undefined,
+ durationMinutes: t.durationMinutes,
+ category: t.category,
+ isActive: t.isActive,
+ });
+ }
+ }
+
+ // 보기 좋게 정렬: (1) active 먼저, (2) category, (3) title
+ for (const dow of WEEK_ORDER) {
+ base[dow] = [...base[dow]].sort((a, b) => {
+ if (a.isActive !== b.isActive) return a.isActive ? -1 : 1;
+ if (a.category !== b.category)
+ return String(a.category).localeCompare(String(b.category));
+ return a.title.localeCompare(b.title);
+ });
+ }
+
+ return base;
+}
diff --git a/apps/desktop/src/domain/scheduleLimit.ts b/apps/desktop/src/domain/scheduleLimit.ts
new file mode 100644
index 0000000..8e45fdb
--- /dev/null
+++ b/apps/desktop/src/domain/scheduleLimit.ts
@@ -0,0 +1 @@
+export * from './schedule/scheduleLimit';
diff --git a/apps/desktop/src/domain/scheduleView.ts b/apps/desktop/src/domain/scheduleView.ts
new file mode 100644
index 0000000..5263b85
--- /dev/null
+++ b/apps/desktop/src/domain/scheduleView.ts
@@ -0,0 +1 @@
+export * from './schedule/scheduleView';
diff --git a/apps/desktop/src/domain/stats/stats.ts b/apps/desktop/src/domain/stats/stats.ts
new file mode 100644
index 0000000..dc49ae2
--- /dev/null
+++ b/apps/desktop/src/domain/stats/stats.ts
@@ -0,0 +1,71 @@
+import type { Completion, DayOfWeek, Task } from '../../shared/types';
+import { buildWeekDates } from '../../shared/utils/date';
+import { isDoneOn } from '../completion';
+
+export interface WeekStats {
+ weekStart: string; // (role: week start YYYY-MM-DD, type: string)
+ totalRate: number; // (role: % 0..100, type: number)
+ weekdayRate: number;
+ weekendRate: number;
+ dailyRate: number;
+ customRate: number;
+}
+
+function completedInWeek(
+ task: Task, // (role: task, type: Task)
+ completions: Completion[], // (role: completion logs, type: Completion[])
+ weekDates: string[], // (role: YYYY-MM-DD[] in week, type: string[])
+): boolean {
+ const dateSet = new Set(weekDates);
+ return completions.some((c) => c.taskId === task.id && dateSet.has(c.date));
+}
+
+export function calcWeekStats(
+ tasks: Task[], // (role: all tasks, type: Task[])
+ completions: Completion[], // (role: completion logs, type: Completion[])
+ weekStart: string, // (role: week start YYYY-MM-DD, type: string)
+): WeekStats {
+ const weekDates = buildWeekDates(weekStart);
+ const active = tasks.filter((t) => t.isActive);
+
+ const rateFor = (subset: Task[]) => {
+ if (subset.length === 0) return 0;
+ const done = subset.filter((t) =>
+ completedInWeek(t, completions, weekDates),
+ ).length;
+ return (done * 100) / subset.length;
+ };
+
+ return {
+ weekStart,
+ totalRate: rateFor(active),
+ weekdayRate: rateFor(active.filter((t) => t.category === 'weekday')),
+ weekendRate: rateFor(active.filter((t) => t.category === 'weekend')),
+ dailyRate: rateFor(active.filter((t) => t.category === 'daily')),
+ customRate: rateFor(active.filter((t) => t.category === 'custom')),
+ };
+}
+
+export interface TodayStats {
+ scheduledCount: number; // (role: tasks scheduled today, type: number)
+ doneCount: number; // (role: tasks completed today, type: number)
+ rate: number; // (role: completion rate 0..100, type: number)
+}
+
+export function calcTodayStats(
+ tasks: Task[], // (role: all tasks, type: Task[])
+ completions: Completion[], // (role: completion logs, type: Completion[])
+ todayYmd: string, // (role: YYYY-MM-DD, type: string)
+ todayDow: DayOfWeek, // (role: day-of-week, type: DayOfWeek)
+): TodayStats {
+ const scheduled = tasks.filter(
+ (t) => t.isActive && t.daysOfWeek.includes(todayDow),
+ );
+ const done = scheduled.filter((t) => isDoneOn(completions, t.id, todayYmd));
+
+ const scheduledCount = scheduled.length;
+ const doneCount = done.length;
+ const rate = scheduledCount === 0 ? 0 : (doneCount * 100) / scheduledCount;
+
+ return { scheduledCount, doneCount, rate };
+}
diff --git a/apps/desktop/src/domain/task/taskFactory.ts b/apps/desktop/src/domain/task/taskFactory.ts
new file mode 100644
index 0000000..ca2592a
--- /dev/null
+++ b/apps/desktop/src/domain/task/taskFactory.ts
@@ -0,0 +1,76 @@
+import type { Category, DayOfWeek, Task } from '../../shared/types';
+import { FIXED_DAYS } from '../schedule';
+
+export function createTaskEntity(args: {
+ id: string; // (role: task id, type: string)
+ title: string; // (role: title, type: string)
+ description?: string; // (role: persistent description, type: string | undefined)
+ category: Category; // (role: schedule category, type: Category)
+ customDays?: DayOfWeek[]; // (role: custom days, type: DayOfWeek[] | undefined)
+ durationMinutes: number; // (role: planned minutes, type: number)
+ startYmd?: string | null; // (role: first eligible date YYYY-MM-DD, type: string | null | undefined)
+ autoArchiveAfter?: number | null; // (role: auto archive threshold, type: number | null | undefined)
+ repeatCount?: number | null; // (role: weekly max occurrences, type: number | null | undefined)
+ nowIso: string; // (role: created timestamp, type: ISO string)
+}): Task {
+ const title = args.title.trim();
+ if (!title) throw new Error('Title is required.');
+
+ const description = (args.description ?? '').trim();
+
+ const durationMinutes = Math.max(1, Math.floor(args.durationMinutes || 0));
+ if (!Number.isFinite(durationMinutes) || durationMinutes <= 0) {
+ throw new Error('Duration must be a positive number (minutes).');
+ }
+
+ const autoArchiveAfterRaw = args.autoArchiveAfter;
+ const autoArchiveAfterNum =
+ autoArchiveAfterRaw == null ? null : Number(autoArchiveAfterRaw);
+ const autoArchiveAfter =
+ autoArchiveAfterNum == null ||
+ !Number.isInteger(autoArchiveAfterNum) ||
+ autoArchiveAfterNum < 1
+ ? null
+ : autoArchiveAfterNum;
+
+ const startYmdRaw = args.startYmd == null ? null : String(args.startYmd);
+ const startYmd =
+ startYmdRaw == null || startYmdRaw.trim() === ''
+ ? null
+ : /^\d{4}-\d{2}-\d{2}$/.test(startYmdRaw.trim())
+ ? startYmdRaw.trim()
+ : null;
+
+ const repeatCountRaw = args.repeatCount;
+ const repeatCountNum = repeatCountRaw == null ? null : Number(repeatCountRaw);
+ const repeatCount =
+ repeatCountNum == null ||
+ !Number.isInteger(repeatCountNum) ||
+ repeatCountNum < 1
+ ? null
+ : repeatCountNum;
+
+ let daysOfWeek: readonly DayOfWeek[];
+
+ if (args.category === 'custom') {
+ const days = (args.customDays ?? []).filter(Boolean);
+ if (days.length === 0) throw new Error('Pick at least one day for custom.');
+ daysOfWeek = [...new Set(days)];
+ } else {
+ daysOfWeek = FIXED_DAYS[args.category];
+ }
+
+ return {
+ id: args.id,
+ title,
+ description,
+ category: args.category,
+ daysOfWeek,
+ durationMinutes,
+ startYmd,
+ autoArchiveAfter,
+ repeatCount,
+ isActive: true,
+ createdAt: args.nowIso,
+ };
+}
diff --git a/apps/desktop/src/features/statistics/components/PeriodStatsPanel.tsx b/apps/desktop/src/features/statistics/components/PeriodStatsPanel.tsx
new file mode 100644
index 0000000..b12ac91
--- /dev/null
+++ b/apps/desktop/src/features/statistics/components/PeriodStatsPanel.tsx
@@ -0,0 +1,187 @@
+import { useContext } from 'react';
+import type { Completion, Task } from '../types';
+import { dayOfWeek } from '../date';
+import { isScheduledOn } from '../../../domain/schedule';
+import { isDoneOn } from '../../../domain/completion';
+import { LocaleContext } from '../../../i18n/context';
+
+// (role: clamp helper, type: (number, number, number)=>number)
+function clamp(v: number, min: number, max: number): number {
+ return Math.min(max, Math.max(min, v));
+}
+
+// (role: date add helper, type: (string, number)=>string)
+function addDaysYmd(ymd: string, days: number): string {
+ const d = new Date(`${ymd}T00:00:00`);
+ d.setDate(d.getDate() + days);
+ const yyyy = d.getFullYear();
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
+ const dd = String(d.getDate()).padStart(2, '0');
+ return `${yyyy}-${mm}-${dd}`;
+}
+
+// (role: start of week monday (ymd), type: (string)=>string)
+function startOfWeekMondayYmd(todayYmd: string): string {
+ const d = new Date(`${todayYmd}T00:00:00`);
+ const jsDow = d.getDay(); // 0 Sun .. 6 Sat
+ const offset = (jsDow + 6) % 7; // Mon=0, Tue=1, ... Sun=6
+ return addDaysYmd(todayYmd, -offset);
+}
+
+// (role: start of month (ymd), type: (string)=>string)
+function startOfMonthYmd(todayYmd: string): string {
+ const d = new Date(`${todayYmd}T00:00:00`);
+ const yyyy = d.getFullYear();
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
+ return `${yyyy}-${mm}-01`;
+}
+
+// (role: min ymd from completions, type: (Completion[], string)=>string)
+function earliestCompletionYmd(
+ completions: Completion[],
+ fallbackYmd: string,
+): string {
+ if (!completions || completions.length === 0) return fallbackYmd;
+ // YYYY-MM-DD lexical order works
+ return completions.reduce(
+ (min, c) => (c.date < min ? c.date : min),
+ completions[0].date,
+ );
+}
+
+// (role: range stats, type: (Task[], Completion[], string, string)=>{scheduledCount:number, doneCount:number, rate:number})
+function calcRangeCompletion(
+ tasks: Task[],
+ completions: Completion[],
+ startYmd: string,
+ endYmd: string,
+) {
+ const activeTasks = tasks.filter((t) => t.isActive);
+
+ let scheduledCount = 0;
+ let doneCount = 0;
+
+ for (let cur = startYmd; cur <= endYmd; cur = addDaysYmd(cur, 1)) {
+ const dow = dayOfWeek(new Date(`${cur}T00:00:00`));
+
+ for (const t of activeTasks) {
+ if (!isScheduledOn(t, dow)) continue;
+
+ scheduledCount += 1;
+
+ if (isDoneOn(completions, t.id, cur)) {
+ doneCount += 1;
+ }
+ }
+ }
+
+ const rate = scheduledCount === 0 ? 0 : (doneCount / scheduledCount) * 100;
+
+ return { scheduledCount, doneCount, rate };
+}
+
+function ProgressBar(props: {
+ value: number; // (role: progress percent, type: number)
+}) {
+ const pct = clamp(Number.isFinite(props.value) ? props.value : 0, 0, 100);
+
+ return (
+
+ );
+}
+
+function StatCard(props: {
+ label: string; // (role: title, type: string)
+ range: string; // (role: date range label, type: string)
+ doneCount: number; // (role: done, type: number)
+ scheduledCount: number; // (role: scheduled, type: number)
+ rate: number; // (role: percent, type: number)
+}) {
+ const { label, range, doneCount, scheduledCount, rate } = props;
+
+ return (
+
+
+
{label}
+
+ {rate.toFixed(1)}%
+
+
+
+
+
{range}
+
+ ({doneCount}/{scheduledCount})
+
+
+
+
+
+ );
+}
+
+export function PeriodStatsPanel(props: {
+ tasks: Task[]; // (role: all tasks, type: Task[])
+ completions: Completion[]; // (role: completion logs, type: Completion[])
+ todayYmd: string; // (role: today ymd, type: string)
+}) {
+ const { t } = useContext(LocaleContext);
+ const { tasks, completions, todayYmd } = props;
+
+ const weekStart = startOfWeekMondayYmd(todayYmd);
+ const weekEnd = addDaysYmd(weekStart, 6);
+
+ const monthStart = startOfMonthYmd(todayYmd);
+ const monthEnd = todayYmd; // month-to-date
+
+ // Current MVP definition:
+ // "All time" means from earliest completion date (not from first task create date).
+ const allStart = earliestCompletionYmd(completions, todayYmd);
+ const allEnd = todayYmd;
+
+ const week = calcRangeCompletion(tasks, completions, weekStart, weekEnd);
+ const month = calcRangeCompletion(tasks, completions, monthStart, monthEnd);
+ const all = calcRangeCompletion(tasks, completions, allStart, allEnd);
+
+ return (
+
+
+
+ {t('common.completion')}
+
+
+ {t('period.basedOnScheduledVsChecked')}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/features/statistics/components/StatsPanel.tsx b/apps/desktop/src/features/statistics/components/StatsPanel.tsx
new file mode 100644
index 0000000..31ff2f1
--- /dev/null
+++ b/apps/desktop/src/features/statistics/components/StatsPanel.tsx
@@ -0,0 +1,59 @@
+import { useContext } from 'react';
+import type { WeekStats } from '../../../domain/stats/stats';
+import { LocaleContext } from '../../../i18n/context';
+
+interface StatsPanelProps {
+ stats: WeekStats;
+}
+
+function StatRow({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+export function StatsPanel({ stats }: StatsPanelProps) {
+ const { t } = useContext(LocaleContext);
+
+ return (
+
+
+
+ {t('stats.weeklyStats')}
+
+
+ {t('stats.weekStart')}:{' '}
+ {stats.weekStart}
+
+
+
+
+
+
+
+
+
+
+
+
{t('stats.mvpRule')}
+
+ );
+}
diff --git a/apps/desktop/src/features/statistics/date.ts b/apps/desktop/src/features/statistics/date.ts
new file mode 100644
index 0000000..6253977
--- /dev/null
+++ b/apps/desktop/src/features/statistics/date.ts
@@ -0,0 +1 @@
+export * from '../../shared/utils/date';
diff --git a/apps/desktop/src/features/statistics/types.ts b/apps/desktop/src/features/statistics/types.ts
new file mode 100644
index 0000000..aaa6c2d
--- /dev/null
+++ b/apps/desktop/src/features/statistics/types.ts
@@ -0,0 +1 @@
+export type * from '../../shared/types';
diff --git a/apps/desktop/src/features/task/components/TaskForm.tsx b/apps/desktop/src/features/task/components/TaskForm.tsx
new file mode 100644
index 0000000..af04a9e
--- /dev/null
+++ b/apps/desktop/src/features/task/components/TaskForm.tsx
@@ -0,0 +1,344 @@
+import { useContext, useMemo } from 'react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+
+import type { Category, DayOfWeek } from '../types';
+import { ALL_DAYS } from '../../../domain/schedule';
+import { LocaleContext } from '../../../i18n/context';
+import { toYmd } from '../date';
+
+// (role: zod enum sources, type: readonly arrays)
+const CATEGORY_VALUES = [
+ 'weekday',
+ 'weekend',
+ 'daily',
+ 'custom',
+] as const satisfies readonly Category[];
+const DAY_VALUES = ALL_DAYS as readonly DayOfWeek[];
+
+// (role: preprocess numeric inputs safely, type: (unknown)=>unknown)
+const toNumber = (v: unknown) => {
+ if (typeof v === 'number') return v;
+ if (typeof v === 'string') return v.trim() === '' ? NaN : Number(v);
+ return NaN;
+};
+
+const toNullableThreshold = (v: unknown) => {
+ if (v == null) return null;
+ if (typeof v === 'string') return v.trim() === '' ? null : Number(v);
+ if (typeof v === 'number') return v;
+ return null;
+};
+
+const toNullableYmd = (v: unknown) => {
+ if (v == null) return null;
+ if (typeof v !== 'string') return null;
+ const ymd = v.trim();
+ if (ymd === '') return null;
+ return /^\d{4}-\d{2}-\d{2}$/.test(ymd) ? ymd : null;
+};
+
+function createTaskInputSchema(
+ t: (key: string, params?: Record) => string,
+ createdAtYmd: string,
+) {
+ return z
+ .object({
+ title: z
+ .string()
+ .trim()
+ .min(1, t('task.validation.titleRequired'))
+ .max(80, t('task.validation.titleTooLong')),
+
+ description: z.string().max(2000).default(''),
+
+ category: z.enum(CATEGORY_VALUES),
+
+ durationMinutes: z.preprocess(
+ toNumber,
+ z
+ .number()
+ .int()
+ .min(1, t('task.validation.durationMin'))
+ .max(600, t('task.validation.durationTooLarge')),
+ ),
+
+ startYmd: z.preprocess(
+ toNullableYmd,
+ z
+ .string()
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
+ .nullable(),
+ ),
+
+ autoArchiveAfter: z.preprocess(
+ toNullableThreshold,
+ z.number().int().min(1).nullable(),
+ ),
+
+ customDays: z.array(z.enum(DAY_VALUES)).optional(),
+ })
+ .superRefine((val, ctx) => {
+ if (val.startYmd && val.startYmd < createdAtYmd) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['startYmd'],
+ message: t('task.validation.startDateBeforeCreatedAt'),
+ });
+ }
+
+ if (val.category === 'custom') {
+ const days = val.customDays ?? [];
+ if (days.length === 0) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['customDays'],
+ message: t('task.validation.pickOneDay'),
+ });
+ }
+ }
+ });
+}
+
+// (role: input type inferred from schema, type: type)
+type CreateTaskSchema = ReturnType;
+type CreateTaskFormValues = z.input;
+export type CreateTaskInput = z.output;
+
+export interface TaskFormProps {
+ onCreate: (input: CreateTaskInput) => void; // (role: create handler, type: (CreateTaskInput)=>void)
+}
+
+export function TaskForm({ onCreate }: TaskFormProps) {
+ const { t } = useContext(LocaleContext);
+ const createdAtYmd = useMemo(() => toYmd(new Date()), []);
+ const schema = useMemo(
+ () => createTaskInputSchema(t, createdAtYmd),
+ [t, createdAtYmd],
+ );
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ setValue,
+ reset,
+ formState: { errors, isSubmitting, isValid },
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ title: '',
+ description: '',
+ category: 'custom',
+ durationMinutes: 30,
+ startYmd: null,
+ autoArchiveAfter: null,
+ customDays: [],
+ },
+ mode: 'onChange',
+ });
+
+ const category = watch('category');
+ const customDays = watch('customDays') ?? [];
+
+ const canSubmit = useMemo(() => {
+ if (!isValid) return false;
+ if (category === 'custom' && customDays.length === 0) return false;
+ return true;
+ }, [isValid, category, customDays.length]);
+
+ const toggleDay = (d: DayOfWeek) => {
+ const next = customDays.includes(d)
+ ? customDays.filter((x) => x !== d)
+ : [...customDays, d];
+
+ setValue('customDays', next, { shouldValidate: true, shouldDirty: true });
+ };
+
+ const submit = handleSubmit((data) => {
+ const payload: CreateTaskInput =
+ data.category === 'custom' ? data : { ...data, customDays: undefined };
+
+ onCreate(payload);
+
+ reset({
+ title: '',
+ description: '',
+ category: 'weekday',
+ durationMinutes: 30,
+ startYmd: null,
+ autoArchiveAfter: null,
+ customDays: ['Mon'],
+ });
+ });
+
+ return (
+
+
+
+ {t('task.createTask')}
+
+
{t('task.createTaskHelp')}
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/features/task/components/TaskList.tsx b/apps/desktop/src/features/task/components/TaskList.tsx
new file mode 100644
index 0000000..9c3a021
--- /dev/null
+++ b/apps/desktop/src/features/task/components/TaskList.tsx
@@ -0,0 +1,77 @@
+import { useContext } from 'react';
+import type { Completion, DayOfWeek, Task, TimeEntry } from '../types';
+import { LocaleContext } from '../../../i18n/context';
+import { TaskListItem } from './TaskListItem';
+
+interface TaskListProps {
+ tasks: Task[];
+ completions: Completion[];
+ timeEntries: TimeEntry[];
+ todayYmd: string;
+ todayDow: DayOfWeek;
+
+ nowIso: string; // (role: ui clock iso, type: string)
+ runningTaskIdToday: string | null; // (role: single running task id, type: string | null)
+
+ getMemoText?: (taskId: string, date: string) => string;
+
+ variant: 'today' | 'manage';
+ onToggleToday: (task: Task) => void;
+ onArchive: (taskId: string) => void;
+ onRestore?: (taskId: string) => void;
+ onDelete?: (taskId: string) => void;
+ onSaveMemo?: (input: { taskId: string; date: string; text: string }) => void;
+ onUpdateTaskMeta?: (input: {
+ taskId: string;
+ title: string;
+ description: string;
+ startYmd: string | null;
+ autoArchiveAfter: number | null;
+ }) => void;
+
+ onStartTimer: (task: Task) => void;
+ onStopTimer: (task: Task) => void;
+
+ onError: (msg: string) => void;
+}
+
+export function TaskList(props: TaskListProps) {
+ const { t } = useContext(LocaleContext);
+ const { tasks } = props;
+
+ if (tasks.length === 0) {
+ return (
+
+ {t('task.noTasks')}
+
+ );
+ }
+
+ return (
+
+ {tasks.map((t) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/desktop/src/features/task/components/TaskListItem.tsx b/apps/desktop/src/features/task/components/TaskListItem.tsx
new file mode 100644
index 0000000..b05dc80
--- /dev/null
+++ b/apps/desktop/src/features/task/components/TaskListItem.tsx
@@ -0,0 +1,498 @@
+import { useContext, useState } from 'react';
+import type { Completion, DayOfWeek, Task, TimeEntry } from '../types';
+import { isDoneOn } from '../../../domain/completion';
+import { isScheduledOn } from '../../../domain/schedule';
+import { diffMinutes } from '../date';
+import { Archive, Check, Pause, Pencil, Play, StickyNote } from 'lucide-react';
+import clsx from 'clsx';
+import { LocaleContext } from '../../../i18n/context';
+
+interface TaskListItemProps {
+ task: Task; // (role: task item, type: Task)
+ completions: Completion[]; // (role: completion logs, type: Completion[])
+ timeEntries: TimeEntry[]; // (role: time tracking logs, type: TimeEntry[])
+ todayYmd: string; // (role: YYYY-MM-DD, type: string)
+ todayDow: DayOfWeek; // (role: day-of-week, type: DayOfWeek)
+
+ nowIso: string; // (role: ui clock iso, type: string)
+ runningTaskIdToday: string | null; // (role: single running task id, type: string | null)
+
+ variant: 'today' | 'manage'; // (role: UI behavior switch, type: union)
+ memoText?: string; // (role: today memo text, type: string | undefined)
+
+ onToggleToday: (task: Task) => void; // (role: toggle today's completion, type: (Task)=>void)
+ onArchive: (taskId: string) => void; // (role: archive task, type: (string)=>void)
+ onRestore?: (taskId: string) => void; // (role: restore handler, type: ((string)=>void) | undefined)
+ onDelete?: (taskId: string) => void; // (role: hard delete handler, type: ((string)=>void) | undefined)
+ onStartTimer: (task: Task) => void; // (role: start timer, type: (Task)=>void)
+ onStopTimer: (task: Task) => void; // (role: stop timer, type: (Task)=>void)
+ onSaveMemo?: (input: { taskId: string; date: string; text: string }) => void; // (role: save daily memo, type: ((input)=>void) | undefined)
+ onUpdateTaskMeta?: (input: {
+ taskId: string;
+ title: string;
+ description: string;
+ startYmd: string | null;
+ autoArchiveAfter: number | null;
+ }) => void; // (role: update task fields, type: ((input)=>void) | undefined)
+ onError: (msg: string) => void; // (role: set error message, type: (string)=>void)
+}
+
+export function TaskListItem(props: TaskListItemProps) {
+ const { t: tr } = useContext(LocaleContext);
+
+ const {
+ task,
+ completions,
+ timeEntries,
+ todayYmd,
+ todayDow,
+ nowIso,
+ runningTaskIdToday,
+ variant,
+ memoText,
+ onToggleToday,
+ onArchive,
+ onRestore,
+ onDelete,
+ onStartTimer,
+ onStopTimer,
+ onSaveMemo,
+ onUpdateTaskMeta,
+ onError,
+ } = props;
+
+ const scheduledToday = isScheduledOn(task, todayDow);
+ const doneToday = isDoneOn(completions, task.id, todayYmd);
+
+ // (role: safe description string, type: string)
+ const description = (task.description ?? '').trim();
+
+ // (role: total completion count for this task, type: number)
+ const doneCountTotal = (completions ?? []).filter(
+ (c) => c.taskId === task.id,
+ ).length;
+
+ // (role: auto-archive progress "done/threshold", type: string | null)
+ const autoArchiveProgressLabel =
+ task.autoArchiveAfter == null
+ ? null
+ : `(${Math.min(doneCountTotal, task.autoArchiveAfter)}/${task.autoArchiveAfter})`;
+
+ const safeTimeEntries = timeEntries ?? [];
+ const todayEntries = safeTimeEntries.filter(
+ (e) => e.taskId === task.id && e.date === todayYmd,
+ );
+
+ // Store policy: only ONE running entry per day.
+ const running = runningTaskIdToday === task.id;
+
+ const totalMinutesToday = todayEntries.reduce((acc, e) => {
+ if (e.endedAt == null) return acc + diffMinutes(e.startedAt, nowIso);
+ return acc + (e.minutes || 0);
+ }, 0);
+
+ const plannedMinutes = Math.max(0, task.durationMinutes || 0);
+
+ // progress (0~1). if done => 1 (UX)
+ const progress01 = doneToday
+ ? 1
+ : plannedMinutes === 0
+ ? 0
+ : Math.min(totalMinutesToday / plannedMinutes, 1);
+
+ const progressPct = Math.round(progress01 * 100);
+ const daysLabel = task.daysOfWeek.map((d) => tr(`time.day.${d}`)).join(', ');
+ const categoryLabel = tr(`common.${task.category}`);
+
+ const [memoOpen, setMemoOpen] = useState(false);
+ const [memoDraft, setMemoDraft] = useState(memoText ?? '');
+
+ // (role: toggle memo editor, type: ()=>void)
+ const onToggleMemo = () => {
+ setMemoOpen((prev) => {
+ const next = !prev;
+
+ // "열리는 순간"에만 초기값 주입 (effect 필요 없음)
+ if (next) {
+ setMemoDraft(memoText ?? '');
+ }
+
+ return next;
+ });
+ };
+
+ const [editOpen, setEditOpen] = useState(false);
+ const [editTitle, setEditTitle] = useState(task.title);
+ const [editDescription, setEditDescription] = useState(
+ task.description ?? '',
+ );
+ const [editAutoArchiveAfter, setEditAutoArchiveAfter] = useState(
+ task.autoArchiveAfter == null ? '' : String(task.autoArchiveAfter),
+ );
+ const [editStartYmd, setEditStartYmd] = useState(task.startYmd ?? '');
+ const [editStartYmdError, setEditStartYmdError] = useState(
+ null,
+ );
+
+ // (role: open edit panel and initialize drafts, type: ()=>void)
+ const openEdit = () => {
+ setEditTitle(task.title);
+ setEditDescription(task.description ?? '');
+ setEditAutoArchiveAfter(
+ task.autoArchiveAfter == null ? '' : String(task.autoArchiveAfter),
+ );
+ setEditStartYmd(task.startYmd ?? '');
+ setEditStartYmdError(null);
+ setEditOpen(true);
+ };
+
+ // (role: close edit panel, type: ()=>void)
+ const closeEdit = () => setEditOpen(false);
+
+ const saveMemo = () => {
+ if (!onSaveMemo) return;
+ onSaveMemo({
+ taskId: task.id,
+ date: todayYmd,
+ text: memoDraft,
+ });
+ };
+
+ const saveTaskMeta = () => {
+ if (!onUpdateTaskMeta) return;
+
+ const normalized = editAutoArchiveAfter.trim();
+ const threshold = normalized === '' ? null : Number(normalized);
+
+ if (threshold != null && (!Number.isInteger(threshold) || threshold < 1)) {
+ onError(tr('task.autoArchiveAfterHint'));
+ return;
+ }
+
+ const createdAtYmd = task.createdAt.slice(0, 10);
+ const normalizedStartYmd =
+ editStartYmd.trim() === '' ? null : editStartYmd.trim();
+ if (normalizedStartYmd && normalizedStartYmd < createdAtYmd) {
+ setEditStartYmdError(tr('task.validation.startDateBeforeCreatedAt'));
+ return;
+ }
+
+ setEditStartYmdError(null);
+ onUpdateTaskMeta({
+ taskId: task.id,
+ title: editTitle,
+ description: editDescription, // always string
+ startYmd: normalizedStartYmd,
+ autoArchiveAfter: threshold,
+ });
+
+ // UX: 저장 후 닫고 싶으면 이 줄을 켜도 됨.
+ // closeEdit();
+ };
+
+ return (
+
+
+
+
+
+ {task.title}
+
+
+
+ {categoryLabel}
+
+
+ {!task.isActive && (
+
+ {tr('common.archived')}
+
+ )}
+
+
+ {description && (
+
{description}
+ )}
+
+
+
+ {tr('task.days')}: {daysLabel} ·
+ {' '}
+ {tr('task.plan')}: {task.durationMinutes}
+ {tr('time.minuteShort')}{' '}
+
+ · {tr('task.todaySpent')}:{' '}
+ {doneToday ? task.durationMinutes : totalMinutesToday}
+ {tr('time.minuteShort')}
+
+ {task.autoArchiveAfter != null && (
+
+ · {tr('task.autoArchiveAfter')}: {autoArchiveProgressLabel}
+
+ )}
+ {running && (
+
+ {tr('common.running')}
+
+ )}
+ {!scheduledToday && (
+
+ {tr('empty.notScheduledToday')}
+
+ )}
+
+
+ {progressPct}%
+
+
+
+
+
+ {variant == 'today' && (
+
+ )}
+
+
+
+ {variant === 'today' && (
+
+ )}
+
+ {variant === 'manage' && (
+ <>
+
+
+ {task.isActive && (
+
+ )}
+
+ {!task.isActive && (
+
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {variant === 'today' && memoOpen && (
+
+
+
+ )}
+
+ {variant === 'manage' && editOpen && (
+
+
+
+ setEditTitle(e.target.value)}
+ className="w-full rounded-xl border border-zinc-800 bg-zinc-950/60 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-zinc-400"
+ />
+
+
+
+
+
+
+
+
+
{
+ setEditStartYmd(e.target.value);
+ setEditStartYmdError(null);
+ }}
+ className="w-44 rounded-xl border border-zinc-800 bg-zinc-950/60 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-zinc-400"
+ />
+
+ {tr('task.startDateHint')}
+
+ {editStartYmdError && (
+
{editStartYmdError}
+ )}
+
+
+
+
+
setEditAutoArchiveAfter(e.target.value)}
+ placeholder="2"
+ className="w-32 rounded-xl border border-zinc-800 bg-zinc-950/60 px-3 py-2 text-sm text-zinc-100 outline-none placeholder:text-zinc-600 focus:border-zinc-400"
+ />
+
+ {tr('task.autoArchiveAfterHint')}
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/desktop/src/features/task/date.ts b/apps/desktop/src/features/task/date.ts
new file mode 100644
index 0000000..6253977
--- /dev/null
+++ b/apps/desktop/src/features/task/date.ts
@@ -0,0 +1 @@
+export * from '../../shared/utils/date';
diff --git a/apps/desktop/src/features/task/hooks/useAutoStopTick.ts b/apps/desktop/src/features/task/hooks/useAutoStopTick.ts
new file mode 100644
index 0000000..9e75aa5
--- /dev/null
+++ b/apps/desktop/src/features/task/hooks/useAutoStopTick.ts
@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+import { useDailyCheckStore } from '../../../app/store/useDailyCheckStore';
+import { useLocale } from '../../../i18n/useLocale';
+import { sendTimerDoneNotification } from '../../../infrastructure/notification';
+import { getSetting } from '../../../infrastructure/tauri/store';
+
+const TIMER_DONE_NOTIFY_KEY = 'settings.notifications.timerDone';
+
+// 자동 종료를 위한 tick
+// (role: app-wide timer tick hook, type: () => void)
+export function useAutoStopTick() {
+ const { t } = useLocale();
+
+ useEffect(() => {
+ const id = window.setInterval(() => {
+ const finishedTaskTitles = useDailyCheckStore
+ .getState()
+ .autoStopIfReached({ today: new Date() });
+
+ if (finishedTaskTitles.length === 0) return;
+
+ void (async () => {
+ const notifyEnabled = await getSetting(
+ TIMER_DONE_NOTIFY_KEY,
+ true,
+ );
+ if (!notifyEnabled) return;
+
+ for (const taskTitle of finishedTaskTitles) {
+ await sendTimerDoneNotification({
+ title: t('notify.timerDone.title'),
+ body: t('notify.timerDone.body', { task: taskTitle }),
+ });
+ }
+ })();
+ }, 5000);
+
+ return () => window.clearInterval(id);
+ }, [t]);
+}
diff --git a/apps/desktop/src/features/task/types.ts b/apps/desktop/src/features/task/types.ts
new file mode 100644
index 0000000..aaa6c2d
--- /dev/null
+++ b/apps/desktop/src/features/task/types.ts
@@ -0,0 +1 @@
+export type * from '../../shared/types';
diff --git a/apps/desktop/src/i18n/context.ts b/apps/desktop/src/i18n/context.ts
new file mode 100644
index 0000000..fac60a3
--- /dev/null
+++ b/apps/desktop/src/i18n/context.ts
@@ -0,0 +1,16 @@
+import { createContext } from 'react';
+import type { Locale } from './messages';
+
+export type TranslateParams = Record;
+
+export interface LocaleContextValue {
+ locale: Locale;
+ setLocale: (locale: Locale) => void;
+ t: (key: string, params?: TranslateParams) => string;
+}
+
+export const LocaleContext = createContext({
+ locale: 'en',
+ setLocale: () => {},
+ t: (key) => key,
+});
diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts
new file mode 100644
index 0000000..e4ba33d
--- /dev/null
+++ b/apps/desktop/src/i18n/index.ts
@@ -0,0 +1,2 @@
+export { messages } from './messages';
+export type { Locale } from './messages';
diff --git a/apps/desktop/src/i18n/messages/en.ts b/apps/desktop/src/i18n/messages/en.ts
new file mode 100644
index 0000000..82eb01f
--- /dev/null
+++ b/apps/desktop/src/i18n/messages/en.ts
@@ -0,0 +1,164 @@
+export const en = {
+ common: {
+ today: 'Today',
+ manage: 'Manage',
+ schedule: 'Schedule',
+ settings: 'Settings',
+ add: 'Add',
+ save: 'Save',
+ edit: 'Edit',
+ memo: 'Memo',
+ reset: 'Reset',
+ all: 'All',
+ weekday: 'Weekday',
+ weekend: 'Weekend',
+ daily: 'Daily',
+ custom: 'Custom',
+ archived: 'Archived',
+ running: 'Running',
+ time: 'Time',
+ completion: 'Completion',
+ },
+
+ task: {
+ createTask: 'Create task',
+ createTaskHelp: 'Choose a schedule rule, duration, and add a task.',
+ title: 'Title',
+ titlePlaceholder: 'e.g. Exercise',
+ description: 'Description',
+ descriptionPlaceholder: 'Optional long-lived note about this task',
+ startDate: 'Start date',
+ startDateHint: 'Leave blank to make it available immediately.',
+ memo: 'Memo',
+ memoPlaceholder: 'Write today-specific notes for this task',
+ autoArchiveAfter: 'Completion limit',
+ autoArchiveAfterHint: 'Leave blank for no limit.',
+ schedule: 'Schedule',
+ days: 'Days',
+ plan: 'Plan',
+ todaySpent: 'Today',
+ todayTasksDescription: 'Only tasks scheduled for today are shown here.',
+ manageTasks: 'Manage tasks',
+ manageTasksDescription: 'Your tasks list with the current filters.',
+ filters: 'Filters',
+ filtersDescription: 'Search and filter when tasks grow.',
+ search: 'Search',
+ searchPlaceholder: 'Search by title...',
+ category: 'Category',
+ viewOptions: 'View options',
+ showArchived: 'Show archived',
+ showingArchived: 'Showing archived',
+ addScheduleCustom: 'custom (pick days)',
+ addScheduleDaily: 'daily (Mon-Sun)',
+ addScheduleWeekday: 'weekday (Mon-Fri)',
+ addScheduleWeekend: 'weekend (Sat-Sun)',
+ pickDays: 'Pick days',
+ customCheckableNote:
+ 'Custom tasks are only checkable on the selected days.',
+ validation: {
+ titleRequired: 'Title is required.',
+ titleTooLong: 'Title is too long (max 80).',
+ durationMin: 'Duration must be >= 1 minute.',
+ durationTooLarge: 'Duration too large.',
+ startDateBeforeCreatedAt:
+ 'Start date cannot be earlier than created date.',
+ pickOneDay: 'Pick at least one day.',
+ },
+ archive: 'Archive',
+ restore: 'Restore',
+ delete: 'Delete',
+ todayTasks: "Today's tasks",
+ noTasks: 'No tasks.',
+ noTasksScheduledToday: 'No tasks scheduled for today.',
+ noTasksScheduledManage: 'No tasks match the current filters.',
+ noTasksInSchedule: 'No tasks scheduled.',
+ },
+
+ stats: {
+ scheduledToday: 'Scheduled today',
+ done: 'Done',
+ weeklyStats: 'Weekly stats',
+ totalCompletionRate: 'Total completion rate',
+ weekdayCompletionRate: 'Weekday completion rate',
+ weekendCompletionRate: 'Weekend completion rate',
+ dailyCompletionRate: 'Daily completion rate',
+ customCompletionRate: 'Custom completion rate',
+ weekStart: 'Week start',
+ mvpRule:
+ 'MVP rule: A task counts as completed for the week if it has at least one check within the week.',
+ },
+
+ time: {
+ durationMin: 'Duration (min)',
+ basedOnTodayPlannedMinutes: "Based on today's planned minutes.",
+ start: 'Start',
+ stop: 'Stop',
+ hourShort: 'h',
+ minuteShort: 'm',
+ day: {
+ Mon: 'Mon',
+ Tue: 'Tue',
+ Wed: 'Wed',
+ Thu: 'Thu',
+ Fri: 'Fri',
+ Sat: 'Sat',
+ Sun: 'Sun',
+ },
+ },
+
+ period: {
+ allTime: 'All time',
+ thisMonth: 'This month',
+ thisWeek: 'This week',
+ basedOnScheduledVsChecked:
+ 'Based on scheduled task-days vs checked task-days.',
+ },
+
+ empty: {
+ notScheduledToday: '(not scheduled today)',
+ },
+
+ note: {
+ clickToDismiss: 'Click to dismiss',
+ scheduleDescription:
+ 'See this week\'s tasks at a glance by weekday.\nCompleted items stay visible on the exact date they were finished.',
+ nextPlan:
+ 'Organize what matters today, track completion and time spent, and keep steady routines with repeat schedules and daily memos. Tasks that hit your goal can be auto-archived so your active list stays focused and clean.',
+ deleteConfirm: 'Delete "{title}" permanently?\nThis cannot be undone.',
+ taskNotScheduledToday: 'This task is not scheduled for today.',
+ },
+
+ schedule: {
+ prevWeek: 'Prev',
+ thisWeek: 'This Week',
+ nextWeek: 'Next',
+ weekRange: '{start} ~ {end}',
+ },
+
+ notify: {
+ timerDone: {
+ title: 'Timer completed',
+ body: '"{task}" is finished.',
+ },
+ },
+
+ settings: {
+ language: {
+ title: 'Language',
+ desc: 'Choose the display language for the app.',
+ options: {
+ en: 'English',
+ ko: 'Korean',
+ ja: 'Japanese',
+ },
+ },
+ notifications: {
+ timerDone: {
+ title: 'Timer done notification',
+ desc: 'Notify when a running timer completes automatically.',
+ hintDenied:
+ 'Notification permission was denied. Enable notifications in system settings and try again.',
+ },
+ },
+ },
+};
diff --git a/apps/desktop/src/i18n/messages/index.ts b/apps/desktop/src/i18n/messages/index.ts
new file mode 100644
index 0000000..d7f14df
--- /dev/null
+++ b/apps/desktop/src/i18n/messages/index.ts
@@ -0,0 +1,11 @@
+import { en } from './en';
+import { ja } from './ja';
+import { ko } from './ko';
+
+export const messages = {
+ en,
+ ko,
+ ja,
+} as const;
+
+export type Locale = keyof typeof messages;
diff --git a/apps/desktop/src/i18n/messages/ja.ts b/apps/desktop/src/i18n/messages/ja.ts
new file mode 100644
index 0000000..c15222b
--- /dev/null
+++ b/apps/desktop/src/i18n/messages/ja.ts
@@ -0,0 +1,165 @@
+export const ja = {
+ common: {
+ today: '今日',
+ manage: '管理',
+ schedule: 'スケジュール',
+ settings: '設定',
+ add: '追加',
+ save: '保存',
+ edit: '編集',
+ memo: 'メモ',
+ reset: 'リセット',
+ all: 'すべて',
+ weekday: '平日',
+ weekend: '週末',
+ daily: '毎日',
+ custom: 'カスタム',
+ archived: 'アーカイブ済み',
+ running: '実行中',
+ time: '時間',
+ completion: '達成率',
+ },
+
+ task: {
+ createTask: 'タスク作成',
+ createTaskHelp: '繰り返しルールと時間を設定してタスクを追加します。',
+ title: 'タイトル',
+ titlePlaceholder: '例: 運動',
+ description: '説明',
+ descriptionPlaceholder: 'このタスクの固定説明を入力してください (任意)',
+ startDate: '開始日',
+ startDateHint: '空欄ならすぐに予定対象になります。',
+ memo: 'メモ',
+ memoPlaceholder: 'このタスクの今日のメモを入力してください',
+ autoArchiveAfter: '完了数の上限',
+ autoArchiveAfterHint:
+ '空欄なら上限なしです。',
+ schedule: '予定',
+ days: '曜日',
+ plan: '計画',
+ todaySpent: '今日',
+ todayTasksDescription: 'ここには今日予定されているタスクのみ表示されます。',
+ manageTasks: 'タスク管理',
+ manageTasksDescription: '現在のフィルターが適用されたタスクリストです。',
+ filters: 'フィルター',
+ filtersDescription: 'タスクが増えたら検索とフィルターを使ってください。',
+ search: '検索',
+ searchPlaceholder: 'タイトルで検索...',
+ category: 'カテゴリ',
+ viewOptions: '表示オプション',
+ showArchived: 'アーカイブを表示',
+ showingArchived: 'アーカイブ表示中',
+ addScheduleCustom: 'カスタム (曜日を選択)',
+ addScheduleDaily: '毎日 (月-日)',
+ addScheduleWeekday: '平日 (月-金)',
+ addScheduleWeekend: '週末 (土-日)',
+ pickDays: '曜日を選択',
+ customCheckableNote: 'カスタムタスクは選択した曜日のみチェックできます。',
+ validation: {
+ titleRequired: 'タイトルは必須です。',
+ titleTooLong: 'タイトルが長すぎます (最大80文字)。',
+ durationMin: '時間は1分以上である必要があります。',
+ durationTooLarge: '時間が大きすぎます。',
+ startDateBeforeCreatedAt:
+ '開始日は作成日より前にできません。',
+ pickOneDay: '少なくとも1日選択してください。',
+ },
+ archive: 'アーカイブ',
+ restore: '復元',
+ delete: '削除',
+ todayTasks: '今日のタスク',
+ noTasks: 'タスクがありません。',
+ noTasksScheduledToday: '今日予定されているタスクはありません。',
+ noTasksScheduledManage: '現在のフィルターに一致するタスクはありません。',
+ noTasksInSchedule: '予定されているタスクはありません。',
+ },
+
+ stats: {
+ scheduledToday: '今日の予定',
+ done: '完了',
+ weeklyStats: '週間統計',
+ totalCompletionRate: '全体の達成率',
+ weekdayCompletionRate: '平日の達成率',
+ weekendCompletionRate: '週末の達成率',
+ dailyCompletionRate: '毎日の達成率',
+ customCompletionRate: 'カスタムの達成率',
+ weekStart: '週の開始日',
+ mvpRule:
+ 'MVPルール: 週内で1回以上チェックがあれば、そのタスクは週の完了として計算されます。',
+ },
+
+ time: {
+ durationMin: '時間 (分)',
+ basedOnTodayPlannedMinutes: '今日の予定時間(分)を基準にしています。',
+ start: '開始',
+ stop: '停止',
+ hourShort: '時間',
+ minuteShort: '分',
+ day: {
+ Mon: '月',
+ Tue: '火',
+ Wed: '水',
+ Thu: '木',
+ Fri: '金',
+ Sat: '土',
+ Sun: '日',
+ },
+ },
+
+ period: {
+ allTime: '全期間',
+ thisMonth: '今月',
+ thisWeek: '今週',
+ basedOnScheduledVsChecked:
+ '予定されたタスク日とチェックされたタスク日を基準にしています。',
+ },
+
+ empty: {
+ notScheduledToday: '(今日は予定なし)',
+ },
+
+ note: {
+ clickToDismiss: 'クリックで閉じる',
+ scheduleDescription:
+ '今週のタスクを曜日ごとにひと目で確認できます。\n完了した項目は完了した日付の列に表示され続けます。',
+ nextPlan:
+ '今日やることを整理し、完了状況と使った時間を記録しましょう。繰り返し予定と日別メモで習慣を整え、目標回数に達したタスクは自動アーカイブで整理できるため、アクティブ一覧をすっきり保てます。',
+ deleteConfirm:
+ '"{title}" を完全に削除しますか?\nこの操作は元に戻せません。',
+ taskNotScheduledToday: 'このタスクは今日の予定ではありません。',
+ },
+
+ schedule: {
+ prevWeek: '前へ',
+ thisWeek: '今週',
+ nextWeek: '次へ',
+ weekRange: '{start} ~ {end}',
+ },
+
+ notify: {
+ timerDone: {
+ title: 'タイマー完了',
+ body: '"{task}" のタイマーが完了しました。',
+ },
+ },
+
+ settings: {
+ language: {
+ title: '言語',
+ desc: 'アプリの表示言語を選択してください。',
+ options: {
+ en: 'English',
+ ko: '한국어',
+ ja: '日本語',
+ },
+ },
+ notifications: {
+ timerDone: {
+ title: 'タイマー完了通知',
+ desc: '実行中のタイマーが自動で終了したら通知します。',
+ hintDenied:
+ '通知権限が拒否されました。システム設定で通知を許可してから再試行してください。',
+ },
+ },
+ },
+};
diff --git a/apps/desktop/src/i18n/messages/ko.ts b/apps/desktop/src/i18n/messages/ko.ts
new file mode 100644
index 0000000..6b77345
--- /dev/null
+++ b/apps/desktop/src/i18n/messages/ko.ts
@@ -0,0 +1,163 @@
+export const ko = {
+ common: {
+ today: '오늘',
+ manage: '관리',
+ schedule: '일정',
+ settings: '설정',
+ add: '추가',
+ save: '저장',
+ edit: '편집',
+ memo: '메모',
+ reset: '초기화',
+ all: '전체',
+ weekday: '평일',
+ weekend: '주말',
+ daily: '매일',
+ custom: '사용자 지정',
+ archived: '보관됨',
+ running: '실행 중',
+ time: '시간',
+ completion: '완료율',
+ },
+
+ task: {
+ createTask: '작업 만들기',
+ createTaskHelp: '반복 규칙과 시간을 설정해 작업을 추가하세요.',
+ title: '제목',
+ titlePlaceholder: '예: 운동',
+ description: '설명',
+ descriptionPlaceholder: '작업에 대한 고정 설명을 입력하세요 (선택)',
+ startDate: '시작일',
+ startDateHint: '비워두면 생성일(기본)부터 표시됩니다.',
+ memo: '메모',
+ memoPlaceholder: '오늘의 메모를 입력하세요',
+ autoArchiveAfter: '완료 수 제한',
+ autoArchiveAfterHint: '비워두면 제한이 없습니다.',
+ schedule: '일정',
+ days: '요일',
+ plan: '계획',
+ todaySpent: '오늘',
+ todayTasksDescription: '오늘 일정에 포함된 작업만 표시됩니다.',
+ manageTasks: '작업 관리',
+ manageTasksDescription: '현재 필터가 적용된 작업 목록입니다.',
+ filters: '필터',
+ filtersDescription: '작업이 많아질 때 검색과 필터를 사용하세요.',
+ search: '검색',
+ searchPlaceholder: '제목으로 검색...',
+ category: '카테고리',
+ viewOptions: '보기 옵션',
+ showArchived: '보관된 항목 보기',
+ showingArchived: '보관된 항목 표시 중',
+ addScheduleCustom: '사용자 지정 (요일 선택)',
+ addScheduleDaily: '매일 (월-일)',
+ addScheduleWeekday: '평일 (월-금)',
+ addScheduleWeekend: '주말 (토-일)',
+ pickDays: '요일 선택',
+ customCheckableNote:
+ '사용자 지정 작업은 선택한 요일에만 체크할 수 있습니다.',
+ validation: {
+ titleRequired: '제목은 필수입니다.',
+ titleTooLong: '제목이 너무 깁니다 (최대 80자).',
+ durationMin: '시간은 최소 1분 이상이어야 합니다.',
+ durationTooLarge: '시간이 너무 큽니다.',
+ startDateBeforeCreatedAt:
+ '시작일은 생성일보다 빠를 수 없습니다.',
+ pickOneDay: '최소 하루 이상 선택하세요.',
+ },
+ archive: '보관',
+ restore: '복원',
+ delete: '삭제',
+ todayTasks: '오늘의 작업',
+ noTasks: '작업이 없습니다.',
+ noTasksScheduledToday: '오늘 예정된 작업이 없습니다.',
+ noTasksScheduledManage: '현재 필터에 맞는 작업이 없습니다.',
+ noTasksInSchedule: '예정된 작업이 없습니다.',
+ },
+
+ stats: {
+ scheduledToday: '오늘 예정',
+ done: '완료',
+ weeklyStats: '주간 통계',
+ totalCompletionRate: '전체 완료율',
+ weekdayCompletionRate: '평일 완료율',
+ weekendCompletionRate: '주말 완료율',
+ dailyCompletionRate: '매일 완료율',
+ customCompletionRate: '사용자 지정 완료율',
+ weekStart: '주 시작일',
+ mvpRule:
+ 'MVP 규칙: 한 주 내에 1회 이상 체크되면 해당 작업은 주간 완료로 계산됩니다.',
+ },
+
+ time: {
+ durationMin: '시간 (분)',
+ basedOnTodayPlannedMinutes: '오늘 계획된 시간(분) 기준입니다.',
+ start: '시작',
+ stop: '중지',
+ hourShort: '시간',
+ minuteShort: '분',
+ day: {
+ Mon: '월',
+ Tue: '화',
+ Wed: '수',
+ Thu: '목',
+ Fri: '금',
+ Sat: '토',
+ Sun: '일',
+ },
+ },
+
+ period: {
+ allTime: '전체 기간',
+ thisMonth: '이번 달',
+ thisWeek: '이번 주',
+ basedOnScheduledVsChecked: '일정된 작업일 대비 체크된 작업일 기준입니다.',
+ },
+
+ empty: {
+ notScheduledToday: '(오늘 일정 아님)',
+ },
+
+ note: {
+ clickToDismiss: '클릭하여 닫기',
+ scheduleDescription:
+ '이번 주 할 일을 요일별로 한눈에 확인할 수 있어요.\n완료한 항목은 완료한 날짜 칸에 계속 표시됩니다.',
+ nextPlan:
+ '오늘 해야 할 일을 정리하고 완료 여부와 사용 시간을 기록해 보세요. 반복 일정과 일일 메모로 루틴을 관리하고, 목표를 달성한 작업은 자동 아카이브로 목록을 깔끔하게 유지할 수 있습니다.',
+ deleteConfirm: '"{title}" 작업을 영구 삭제할까요?\n되돌릴 수 없습니다.',
+ taskNotScheduledToday: '이 작업은 오늘 일정에 없습니다.',
+ },
+
+ schedule: {
+ prevWeek: '이전',
+ thisWeek: '이번 주',
+ nextWeek: '다음',
+ weekRange: '{start} ~ {end}',
+ },
+
+ notify: {
+ timerDone: {
+ title: '타이머 완료',
+ body: '"{task}" 작업 타이머가 완료되었습니다.',
+ },
+ },
+
+ settings: {
+ language: {
+ title: '언어',
+ desc: '앱에서 표시할 언어를 선택하세요.',
+ options: {
+ en: 'English',
+ ko: '한국어',
+ ja: '日本語',
+ },
+ },
+ notifications: {
+ timerDone: {
+ title: '타이머 완료 알림',
+ desc: '실행 중인 타이머가 자동으로 끝나면 알림을 보냅니다.',
+ hintDenied:
+ '알림 권한이 거부되었습니다. 시스템 설정에서 알림 권한을 허용한 뒤 다시 시도하세요.',
+ },
+ },
+ },
+};
diff --git a/apps/desktop/src/i18n/provider.tsx b/apps/desktop/src/i18n/provider.tsx
new file mode 100644
index 0000000..e483d83
--- /dev/null
+++ b/apps/desktop/src/i18n/provider.tsx
@@ -0,0 +1,61 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import type { Locale } from './messages';
+import { LocaleContext } from './context';
+import { translate } from './translate';
+import { getSetting, setSetting } from '../infrastructure/tauri/store';
+
+// (role: safe locale parser, type: (unknown)=>Locale)
+function parseLocale(value: unknown): Locale {
+ if (value === 'en' || value === 'ko' || value === 'ja') return value;
+ return 'en';
+}
+
+export function LocaleProvider(props: {
+ children: React.ReactNode; // (role: subtree, type: React.ReactNode)
+}) {
+ const [locale, setLocaleState] = useState('en');
+ const [localeLoaded, setLocaleLoaded] = useState(false);
+
+ useEffect(() => {
+ let alive = true;
+
+ void (async () => {
+ const next = parseLocale(await getSetting('locale', 'en'));
+ if (alive) {
+ setLocaleState(next);
+ setLocaleLoaded(true);
+ }
+ })();
+
+ return () => {
+ alive = false;
+ };
+ }, []);
+
+ // (role: set locale and persist, type: (Locale)=>void)
+ const setLocale = (next: Locale) => {
+ setLocaleState(next);
+ };
+
+ useEffect(() => {
+ if (!localeLoaded) return;
+
+ void setSetting('locale', locale).catch(() => {
+ // ignore persistence failure
+ });
+ }, [locale, localeLoaded]);
+
+ const t = useMemo(() => {
+ return (key: string, params?: Record) =>
+ translate(locale, key, params);
+ }, [locale]);
+
+ // value 객체가 매 렌더마다 바뀌지 않게 메모이즈 (불필요한 re-render 감소)
+ const value = useMemo(() => ({ locale, setLocale, t }), [locale, t]);
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/apps/desktop/src/i18n/translate.ts b/apps/desktop/src/i18n/translate.ts
new file mode 100644
index 0000000..711c6a4
--- /dev/null
+++ b/apps/desktop/src/i18n/translate.ts
@@ -0,0 +1,28 @@
+import { messages, type Locale } from './messages';
+import type { TranslateParams } from './context';
+
+export function translate(
+ locale: Locale,
+ key: string,
+ params?: TranslateParams,
+): string {
+ const parts = key.split('.');
+
+ let obj: unknown = messages[locale];
+
+ for (const p of parts) {
+ if (obj && typeof obj === 'object') {
+ obj = (obj as Record)[p];
+ } else {
+ obj = undefined;
+ }
+ }
+
+ if (typeof obj !== 'string') return key;
+ if (!params) return obj;
+
+ return obj.replace(/\{(\w+)\}/g, (match: string, name: string) => {
+ const value = params[name];
+ return value == null ? match : String(value);
+ });
+}
diff --git a/apps/desktop/src/i18n/useLocale.ts b/apps/desktop/src/i18n/useLocale.ts
new file mode 100644
index 0000000..0d20549
--- /dev/null
+++ b/apps/desktop/src/i18n/useLocale.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { LocaleContext } from './context';
+
+export function useLocale() {
+ return useContext(LocaleContext);
+}
diff --git a/apps/desktop/src/infrastructure/notification/ToastBus.ts b/apps/desktop/src/infrastructure/notification/ToastBus.ts
new file mode 100644
index 0000000..f8255eb
--- /dev/null
+++ b/apps/desktop/src/infrastructure/notification/ToastBus.ts
@@ -0,0 +1,24 @@
+import type { NotifyLevel } from '../../domain/notifier';
+
+// (role: toast payload, type: interface)
+export interface ToastPayload {
+ level: NotifyLevel; // (role: severity, type: NotifyLevel)
+ message: string; // (role: message, type: string)
+}
+
+// (role: toast listener, type: (ToastPayload)=>void)
+type Listener = (p: ToastPayload) => void;
+
+// (role: in-memory pubsub, type: module state)
+const listeners = new Set();
+
+// (role: subscribe function, type: (Listener)=>()=>void)
+export function subscribeToast(listener: Listener): () => void {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+}
+
+// (role: publish toast event, type: (ToastPayload)=>void)
+export function publishToast(payload: ToastPayload): void {
+ for (const l of listeners) l(payload);
+}
diff --git a/apps/desktop/src/infrastructure/notification/ToastNotifier.ts b/apps/desktop/src/infrastructure/notification/ToastNotifier.ts
new file mode 100644
index 0000000..242e307
--- /dev/null
+++ b/apps/desktop/src/infrastructure/notification/ToastNotifier.ts
@@ -0,0 +1,9 @@
+import type { Notifier } from '../../domain/notifier';
+import { publishToast } from './ToastBus';
+
+// (role: notifier implementation that emits toast events, type: Notifier)
+export const toastNotifier: Notifier = {
+ notify: ({ level, message }) => {
+ publishToast({ level, message });
+ },
+};
diff --git a/apps/desktop/src/infrastructure/notification/index.ts b/apps/desktop/src/infrastructure/notification/index.ts
new file mode 100644
index 0000000..2c210a2
--- /dev/null
+++ b/apps/desktop/src/infrastructure/notification/index.ts
@@ -0,0 +1,50 @@
+import { isTauri } from '../tauri/runtime';
+
+export type NotifyPermission = 'granted' | 'denied' | 'prompt' | 'unsupported';
+
+export async function requestNotifyPermission(): Promise {
+ try {
+ if (isTauri()) {
+ const notification = await import('@tauri-apps/plugin-notification');
+ const result = await notification.requestPermission();
+ if (result === 'granted' || result === 'denied') return result;
+ if (result === 'default') return 'prompt';
+ return 'unsupported';
+ }
+
+ if (typeof window === 'undefined' || !('Notification' in window)) {
+ return 'unsupported';
+ }
+
+ const result = await Notification.requestPermission();
+ if (result === 'granted' || result === 'denied' || result === 'default') {
+ return result === 'default' ? 'prompt' : result;
+ }
+ return 'unsupported';
+ } catch {
+ return 'unsupported';
+ }
+}
+
+export async function sendTimerDoneNotification(payload: {
+ title: string;
+ body: string;
+}): Promise {
+ try {
+ if (isTauri()) {
+ const notification = await import('@tauri-apps/plugin-notification');
+ await notification.sendNotification({
+ title: payload.title,
+ body: payload.body,
+ });
+ return;
+ }
+
+ if (typeof window === 'undefined' || !('Notification' in window)) return;
+ if (Notification.permission !== 'granted') return;
+
+ new Notification(payload.title, { body: payload.body });
+ } catch {
+ // ignore notification failures
+ }
+}
diff --git a/apps/desktop/src/infrastructure/storage/index.ts b/apps/desktop/src/infrastructure/storage/index.ts
new file mode 100644
index 0000000..2d1599f
--- /dev/null
+++ b/apps/desktop/src/infrastructure/storage/index.ts
@@ -0,0 +1,499 @@
+import {
+ CompletionsSchema,
+ TaskDailyMemosSchema,
+ TasksSchema,
+ TimeEntriesSchema,
+ type DayOfWeek,
+} from '../../shared/schemas';
+import type {
+ Completion,
+ Task,
+ TaskDailyMemo,
+ TimeEntry,
+} from '../../shared/types';
+import { appDb } from '../tauri/db';
+import { isTauri } from '../tauri/runtime';
+
+const STORAGE_KEYS = {
+ tasks: 'dailycheck.tasks.v2',
+ completions: 'dailycheck.completions.v1',
+ timeEntries: 'dailycheck.timeEntries.v1',
+ taskDailyMemos: 'dailycheck.taskDailyMemos.v1',
+} as const;
+
+const LEGACY_STORAGE_MIGRATION_KEY = 'legacy_storage_migrated_v1';
+
+type PersistedAppData = {
+ tasks: Task[];
+ completions: Completion[];
+ timeEntries: TimeEntry[];
+ taskDailyMemos: TaskDailyMemo[];
+};
+
+type MetaRow = {
+ value: string;
+};
+
+type TaskRow = {
+ id: string;
+ title: string;
+ description: string;
+ category: Task['category'];
+ days_of_week: string;
+ duration_minutes: number;
+ start_ymd: string | null;
+ auto_archive_after: number | null;
+ repeat_count: number | null;
+ is_active: number;
+ created_at: string;
+};
+
+type CompletionRow = {
+ task_id: string;
+ date: string;
+};
+
+type TimeEntryRow = {
+ id: string;
+ task_id: string;
+ date: string;
+ started_at: string;
+ ended_at: string | null;
+ minutes: number;
+};
+
+type TaskDailyMemoRow = {
+ id: string;
+ task_id: string;
+ date: string;
+ text: string;
+ updated_at: string;
+};
+
+function parseLegacyJson(
+ key: string,
+ parse: (value: unknown) => T,
+ fallback: T,
+): T {
+ try {
+ const raw = localStorage.getItem(key);
+ if (!raw) return fallback;
+ return parse(JSON.parse(raw) as unknown);
+ } catch {
+ return fallback;
+ }
+}
+
+function parseLegacyDaysOfWeek(value: string): DayOfWeek[] {
+ try {
+ const parsed = JSON.parse(value) as unknown;
+ const result = TasksSchema.element.shape.daysOfWeek.safeParse(parsed);
+ return result.success ? [...result.data] : [];
+ } catch {
+ return [];
+ }
+}
+
+function taskFromRow(row: TaskRow): Task {
+ return {
+ id: row.id,
+ title: row.title,
+ description: row.description,
+ category: row.category,
+ daysOfWeek: parseLegacyDaysOfWeek(row.days_of_week),
+ durationMinutes: Number(row.duration_minutes),
+ startYmd: row.start_ymd,
+ autoArchiveAfter:
+ row.auto_archive_after == null ? null : Number(row.auto_archive_after),
+ repeatCount: row.repeat_count == null ? null : Number(row.repeat_count),
+ isActive: Boolean(row.is_active),
+ createdAt: row.created_at,
+ };
+}
+
+function completionFromRow(row: CompletionRow): Completion {
+ return {
+ taskId: row.task_id,
+ date: row.date,
+ };
+}
+
+function timeEntryFromRow(row: TimeEntryRow): TimeEntry {
+ return {
+ id: row.id,
+ taskId: row.task_id,
+ date: row.date,
+ startedAt: row.started_at,
+ endedAt: row.ended_at,
+ minutes: Number(row.minutes),
+ };
+}
+
+function memoFromRow(row: TaskDailyMemoRow): TaskDailyMemo {
+ return {
+ id: row.id,
+ taskId: row.task_id,
+ date: row.date,
+ text: row.text,
+ updatedAt: row.updated_at,
+ };
+}
+
+async function getMeta(key: string): Promise {
+ const rows = await appDb.select(
+ 'SELECT value FROM app_meta WHERE key = ? LIMIT 1',
+ [key],
+ );
+ return rows[0]?.value ?? null;
+}
+
+async function setMeta(key: string, value: string): Promise {
+ await appDb.execute(
+ `
+ INSERT INTO app_meta (key, value)
+ VALUES (?, ?)
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
+ `,
+ [key, value],
+ );
+}
+
+async function hasExistingData(): Promise {
+ const [tasks, completions, timeEntries, memos] = await Promise.all([
+ appDb.select<{ count: number }>('SELECT COUNT(*) AS count FROM tasks'),
+ appDb.select<{ count: number }>(
+ 'SELECT COUNT(*) AS count FROM completions',
+ ),
+ appDb.select<{ count: number }>(
+ 'SELECT COUNT(*) AS count FROM time_entries',
+ ),
+ appDb.select<{ count: number }>(
+ 'SELECT COUNT(*) AS count FROM task_daily_memos',
+ ),
+ ]);
+
+ return [tasks, completions, timeEntries, memos].some(
+ (rows) => Number(rows[0]?.count ?? 0) > 0,
+ );
+}
+
+async function replaceTasks(tasks: Task[]): Promise {
+ await appDb.execute('DELETE FROM tasks');
+
+ for (const task of tasks) {
+ await appDb.execute(
+ `
+ INSERT INTO tasks (
+ id,
+ title,
+ description,
+ category,
+ days_of_week,
+ duration_minutes,
+ start_ymd,
+ auto_archive_after,
+ repeat_count,
+ is_active,
+ created_at
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `,
+ [
+ task.id,
+ task.title,
+ task.description,
+ task.category,
+ JSON.stringify(task.daysOfWeek),
+ task.durationMinutes,
+ task.startYmd ?? null,
+ task.autoArchiveAfter ?? null,
+ task.repeatCount ?? null,
+ task.isActive ? 1 : 0,
+ task.createdAt,
+ ],
+ );
+ }
+}
+
+async function replaceCompletions(completions: Completion[]): Promise {
+ await appDb.execute('DELETE FROM completions');
+
+ for (const completion of completions) {
+ await appDb.execute(
+ 'INSERT INTO completions (task_id, date) VALUES (?, ?)',
+ [completion.taskId, completion.date],
+ );
+ }
+}
+
+async function replaceTimeEntries(timeEntries: TimeEntry[]): Promise {
+ await appDb.execute('DELETE FROM time_entries');
+
+ for (const entry of timeEntries) {
+ await appDb.execute(
+ `
+ INSERT INTO time_entries (
+ id,
+ task_id,
+ date,
+ started_at,
+ ended_at,
+ minutes
+ )
+ VALUES (?, ?, ?, ?, ?, ?)
+ `,
+ [
+ entry.id,
+ entry.taskId,
+ entry.date,
+ entry.startedAt,
+ entry.endedAt,
+ entry.minutes,
+ ],
+ );
+ }
+}
+
+async function replaceTaskDailyMemos(
+ taskDailyMemos: TaskDailyMemo[],
+): Promise {
+ await appDb.execute('DELETE FROM task_daily_memos');
+
+ for (const memo of taskDailyMemos) {
+ await appDb.execute(
+ `
+ INSERT INTO task_daily_memos (
+ id,
+ task_id,
+ date,
+ text,
+ updated_at
+ )
+ VALUES (?, ?, ?, ?, ?)
+ `,
+ [memo.id, memo.taskId, memo.date, memo.text, memo.updatedAt],
+ );
+ }
+}
+
+async function saveAppData(data: PersistedAppData): Promise {
+ if (!isTauri()) return;
+
+ await appDb.init();
+ await replaceTasks(data.tasks);
+ await replaceCompletions(data.completions);
+ await replaceTimeEntries(data.timeEntries);
+ await replaceTaskDailyMemos(data.taskDailyMemos);
+}
+
+function loadLegacyAppData(): PersistedAppData {
+ return {
+ tasks: parseLegacyJson(
+ STORAGE_KEYS.tasks,
+ (decoded) => {
+ const result = TasksSchema.safeParse(decoded);
+ return result.success ? (result.data as Task[]) : [];
+ },
+ [],
+ ),
+ completions: parseLegacyJson(
+ STORAGE_KEYS.completions,
+ (decoded) => {
+ const result = CompletionsSchema.safeParse(decoded);
+ return result.success ? (result.data as Completion[]) : [];
+ },
+ [],
+ ),
+ timeEntries: parseLegacyJson(
+ STORAGE_KEYS.timeEntries,
+ (decoded) => {
+ const result = TimeEntriesSchema.safeParse(decoded);
+ return result.success ? (result.data as TimeEntry[]) : [];
+ },
+ [],
+ ),
+ taskDailyMemos: parseLegacyJson(
+ STORAGE_KEYS.taskDailyMemos,
+ (decoded) => {
+ const result = TaskDailyMemosSchema.safeParse(decoded);
+ return result.success ? (result.data as TaskDailyMemo[]) : [];
+ },
+ [],
+ ),
+ };
+}
+
+function clearLegacyAppData(): void {
+ for (const key of Object.values(STORAGE_KEYS)) {
+ localStorage.removeItem(key);
+ }
+}
+
+async function migrateLegacyStorageIfNeeded(): Promise {
+ if (!isTauri()) return;
+
+ await appDb.init();
+
+ const alreadyMigrated = await getMeta(LEGACY_STORAGE_MIGRATION_KEY);
+ if (alreadyMigrated === '1') {
+ return;
+ }
+
+ if (await hasExistingData()) {
+ await setMeta(LEGACY_STORAGE_MIGRATION_KEY, '1');
+ clearLegacyAppData();
+ return;
+ }
+
+ const legacyData = loadLegacyAppData();
+ const hasLegacyData =
+ legacyData.tasks.length > 0 ||
+ legacyData.completions.length > 0 ||
+ legacyData.timeEntries.length > 0 ||
+ legacyData.taskDailyMemos.length > 0;
+
+ if (hasLegacyData) {
+ await saveAppData(legacyData);
+ }
+
+ clearLegacyAppData();
+ await setMeta(LEGACY_STORAGE_MIGRATION_KEY, '1');
+}
+
+export async function loadAppData(): Promise {
+ if (!isTauri()) {
+ return {
+ tasks: [],
+ completions: [],
+ timeEntries: [],
+ taskDailyMemos: [],
+ };
+ }
+
+ await migrateLegacyStorageIfNeeded();
+
+ const [taskRows, completionRows, timeEntryRows, memoRows] = await Promise.all(
+ [
+ appDb.select(
+ `
+ SELECT
+ id,
+ title,
+ description,
+ category,
+ days_of_week,
+ duration_minutes,
+ start_ymd,
+ auto_archive_after,
+ repeat_count,
+ is_active,
+ created_at
+ FROM tasks
+ ORDER BY created_at DESC
+ `,
+ ),
+ appDb.select(
+ 'SELECT task_id, date FROM completions ORDER BY date DESC, task_id ASC',
+ ),
+ appDb.select(
+ `
+ SELECT
+ id,
+ task_id,
+ date,
+ started_at,
+ ended_at,
+ minutes
+ FROM time_entries
+ ORDER BY started_at DESC
+ `,
+ ),
+ appDb.select(
+ `
+ SELECT
+ id,
+ task_id,
+ date,
+ text,
+ updated_at
+ FROM task_daily_memos
+ ORDER BY updated_at DESC
+ `,
+ ),
+ ],
+ );
+
+ return {
+ tasks: TasksSchema.parse(taskRows.map(taskFromRow)) as Task[],
+ completions: CompletionsSchema.parse(
+ completionRows.map(completionFromRow),
+ ) as Completion[],
+ timeEntries: TimeEntriesSchema.parse(
+ timeEntryRows.map(timeEntryFromRow),
+ ) as TimeEntry[],
+ taskDailyMemos: TaskDailyMemosSchema.parse(
+ memoRows.map(memoFromRow),
+ ) as TaskDailyMemo[],
+ };
+}
+
+export async function saveTasks(tasks: Task[]): Promise {
+ await saveAppData({
+ tasks,
+ completions: await loadCompletions(),
+ timeEntries: await loadTimeEntries(),
+ taskDailyMemos: await loadTaskDailyMemos(),
+ });
+}
+
+export async function saveCompletions(
+ completions: Completion[],
+): Promise {
+ await saveAppData({
+ tasks: await loadTasks(),
+ completions,
+ timeEntries: await loadTimeEntries(),
+ taskDailyMemos: await loadTaskDailyMemos(),
+ });
+}
+
+export async function saveTimeEntries(timeEntries: TimeEntry[]): Promise {
+ await saveAppData({
+ tasks: await loadTasks(),
+ completions: await loadCompletions(),
+ timeEntries,
+ taskDailyMemos: await loadTaskDailyMemos(),
+ });
+}
+
+export async function saveTaskDailyMemos(
+ taskDailyMemos: TaskDailyMemo[],
+): Promise {
+ await saveAppData({
+ tasks: await loadTasks(),
+ completions: await loadCompletions(),
+ timeEntries: await loadTimeEntries(),
+ taskDailyMemos,
+ });
+}
+
+export async function replaceAllAppData(data: PersistedAppData): Promise {
+ await migrateLegacyStorageIfNeeded();
+ await saveAppData(data);
+}
+
+export async function loadTasks(): Promise {
+ return (await loadAppData()).tasks;
+}
+
+export async function loadCompletions(): Promise {
+ return (await loadAppData()).completions;
+}
+
+export async function loadTimeEntries(): Promise {
+ return (await loadAppData()).timeEntries;
+}
+
+export async function loadTaskDailyMemos(): Promise {
+ return (await loadAppData()).taskDailyMemos;
+}
diff --git a/apps/desktop/src/infrastructure/tauri/db.ts b/apps/desktop/src/infrastructure/tauri/db.ts
new file mode 100644
index 0000000..08f760d
--- /dev/null
+++ b/apps/desktop/src/infrastructure/tauri/db.ts
@@ -0,0 +1,123 @@
+import { isTauri } from './runtime';
+
+export type AppDb = {
+ init(): Promise;
+ execute(sql: string, bind?: unknown[]): Promise;
+ select(sql: string, bind?: unknown[]): Promise;
+};
+
+const DB_URL = 'sqlite:daily_check.db';
+
+let dbPromise: Promise | null = null;
+
+async function getDatabase(): Promise {
+ if (!isTauri()) {
+ throw new Error('SQLite is unavailable in web runtime.');
+ }
+
+ if (!dbPromise) {
+ dbPromise = import('@tauri-apps/plugin-sql').then(({ default: Database }) =>
+ Database.load(DB_URL),
+ );
+ }
+
+ return dbPromise;
+}
+
+async function init(): Promise {
+ if (!isTauri()) return;
+
+ const db = await getDatabase();
+ await db.execute(
+ `
+ CREATE TABLE IF NOT EXISTS settings_kv (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ )
+ `,
+ [],
+ );
+ await db.execute(
+ `
+ CREATE TABLE IF NOT EXISTS app_meta (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ )
+ `,
+ [],
+ );
+ await db.execute(
+ `
+ CREATE TABLE IF NOT EXISTS tasks (
+ id TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ description TEXT NOT NULL,
+ category TEXT NOT NULL,
+ days_of_week TEXT NOT NULL,
+ duration_minutes INTEGER NOT NULL,
+ start_ymd TEXT,
+ auto_archive_after INTEGER,
+ repeat_count INTEGER,
+ is_active INTEGER NOT NULL,
+ created_at TEXT NOT NULL
+ )
+ `,
+ [],
+ );
+ await db.execute(
+ `
+ CREATE TABLE IF NOT EXISTS completions (
+ task_id TEXT NOT NULL,
+ date TEXT NOT NULL,
+ PRIMARY KEY (task_id, date)
+ )
+ `,
+ [],
+ );
+ await db.execute(
+ `
+ CREATE TABLE IF NOT EXISTS time_entries (
+ id TEXT PRIMARY KEY,
+ task_id TEXT NOT NULL,
+ date TEXT NOT NULL,
+ started_at TEXT NOT NULL,
+ ended_at TEXT,
+ minutes INTEGER NOT NULL
+ )
+ `,
+ [],
+ );
+ await db.execute(
+ `
+ CREATE TABLE IF NOT EXISTS task_daily_memos (
+ id TEXT PRIMARY KEY,
+ task_id TEXT NOT NULL,
+ date TEXT NOT NULL,
+ text TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ UNIQUE (task_id, date)
+ )
+ `,
+ [],
+ );
+}
+
+async function execute(sql: string, bind: unknown[] = []): Promise {
+ if (!isTauri()) return;
+ const db = await getDatabase();
+ await db.execute(sql, bind);
+}
+
+async function select(sql: string, bind: unknown[] = []): Promise {
+ if (!isTauri()) {
+ return [];
+ }
+ const db = await getDatabase();
+ return db.select(sql, bind);
+}
+
+export const appDb: AppDb = {
+ init,
+ execute,
+ select,
+};
diff --git a/apps/desktop/src/infrastructure/tauri/index.ts b/apps/desktop/src/infrastructure/tauri/index.ts
new file mode 100644
index 0000000..2228956
--- /dev/null
+++ b/apps/desktop/src/infrastructure/tauri/index.ts
@@ -0,0 +1 @@
+export { isTauri } from './runtime';
diff --git a/apps/desktop/src/infrastructure/tauri/runtime.ts b/apps/desktop/src/infrastructure/tauri/runtime.ts
new file mode 100644
index 0000000..60d418e
--- /dev/null
+++ b/apps/desktop/src/infrastructure/tauri/runtime.ts
@@ -0,0 +1,13 @@
+export function isTauri(): boolean {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+
+ const runtime = globalThis as typeof globalThis & {
+ isTauri?: boolean;
+ __TAURI__?: unknown;
+ __TAURI_INTERNALS__?: unknown;
+ };
+
+ return Boolean(runtime.isTauri || runtime.__TAURI__ || runtime.__TAURI_INTERNALS__);
+}
diff --git a/apps/desktop/src/infrastructure/tauri/shell.ts b/apps/desktop/src/infrastructure/tauri/shell.ts
new file mode 100644
index 0000000..e69de29
diff --git a/apps/desktop/src/infrastructure/tauri/store.ts b/apps/desktop/src/infrastructure/tauri/store.ts
new file mode 100644
index 0000000..e692363
--- /dev/null
+++ b/apps/desktop/src/infrastructure/tauri/store.ts
@@ -0,0 +1,135 @@
+import { appDb } from './db';
+import { isTauri } from './runtime';
+
+const SETTINGS_FILE = 'settings.json';
+const LEGACY_SETTINGS_MIGRATION_KEY = 'legacy_settings_migrated_v1';
+const LEGACY_SETTING_KEYS = ['locale', 'settings.notifications.timerDone'] as const;
+
+type SqlValueRow = {
+ value: string;
+};
+
+type MetaRow = {
+ value: string;
+};
+
+async function getMeta(key: string): Promise {
+ const rows = await appDb.select(
+ 'SELECT value FROM app_meta WHERE key = ? LIMIT 1',
+ [key],
+ );
+ return rows[0]?.value ?? null;
+}
+
+async function setMeta(key: string, value: string): Promise {
+ await appDb.execute(
+ `
+ INSERT INTO app_meta (key, value)
+ VALUES (?, ?)
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
+ `,
+ [key, value],
+ );
+}
+
+async function readSqlSetting(key: string): Promise {
+ const rows = await appDb.select(
+ 'SELECT value FROM settings_kv WHERE key = ? LIMIT 1',
+ [key],
+ );
+
+ if (!rows[0]) return null;
+
+ try {
+ return JSON.parse(rows[0].value) as T;
+ } catch {
+ return null;
+ }
+}
+
+async function writeSqlSetting(key: string, value: unknown): Promise {
+ await appDb.execute(
+ `
+ INSERT INTO settings_kv (key, value)
+ VALUES (?, ?)
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
+ `,
+ [key, JSON.stringify(value)],
+ );
+}
+
+function readLegacyStorageSetting(key: string): T | null {
+ try {
+ const raw = localStorage.getItem(key);
+ if (raw == null) return null;
+ return JSON.parse(raw) as T;
+ } catch {
+ return null;
+ }
+}
+
+async function migrateLegacySettingsIfNeeded(): Promise {
+ if (!isTauri()) return;
+
+ await appDb.init();
+
+ const alreadyMigrated = await getMeta(LEGACY_SETTINGS_MIGRATION_KEY);
+ if (alreadyMigrated === '1') {
+ return;
+ }
+
+ let legacyStoreValues = new Map();
+
+ try {
+ const { load } = await import('@tauri-apps/plugin-store');
+ const store = await load(SETTINGS_FILE, { autoSave: 150, defaults: {} });
+
+ for (const key of LEGACY_SETTING_KEYS) {
+ const value = await store.get(key);
+ if (value != null) {
+ legacyStoreValues.set(key, value);
+ }
+ }
+ } catch {
+ legacyStoreValues = new Map();
+ }
+
+ for (const key of LEGACY_SETTING_KEYS) {
+ const existing = await readSqlSetting(key);
+ if (existing != null) continue;
+
+ const legacyValue =
+ legacyStoreValues.get(key) ?? readLegacyStorageSetting(key);
+
+ if (legacyValue != null) {
+ await writeSqlSetting(key, legacyValue);
+ }
+
+ localStorage.removeItem(key);
+ }
+
+ await setMeta(LEGACY_SETTINGS_MIGRATION_KEY, '1');
+}
+
+export async function getSetting(key: string, fallback: T): Promise {
+ if (!isTauri()) {
+ return fallback;
+ }
+
+ try {
+ await migrateLegacySettingsIfNeeded();
+ const value = await readSqlSetting(key);
+ return value == null ? fallback : value;
+ } catch {
+ return fallback;
+ }
+}
+
+export async function setSetting(key: string, value: unknown): Promise {
+ if (!isTauri()) {
+ return;
+ }
+
+ await migrateLegacySettingsIfNeeded();
+ await writeSqlSetting(key, value);
+}
diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx
new file mode 100644
index 0000000..6aa2411
--- /dev/null
+++ b/apps/desktop/src/main.tsx
@@ -0,0 +1,123 @@
+import { Component, StrictMode, useEffect, useState, type ReactNode } from 'react';
+import { createRoot } from 'react-dom/client';
+import './styles/index.css';
+import App from './app/App.tsx';
+import { LocaleProvider } from './i18n/provider.tsx';
+import { appDb } from './infrastructure/tauri/db';
+import { isTauri } from './infrastructure/tauri/runtime';
+import { useDailyCheckStore } from './app/store/useDailyCheckStore';
+
+type BootState = {
+ ready: boolean;
+};
+
+function formatError(error: unknown): string {
+ if (error instanceof Error) {
+ return error.stack || error.message;
+ }
+
+ if (typeof error === 'string') {
+ return error;
+ }
+
+ try {
+ return JSON.stringify(error);
+ } catch {
+ return 'Unknown initialization error';
+ }
+}
+
+class RootErrorBoundary extends Component<
+ { children: ReactNode },
+ { error: string | null }
+> {
+ state = { error: null as string | null };
+
+ static getDerivedStateFromError(error: unknown) {
+ return {
+ error: error instanceof Error ? error.message : 'Unknown render error',
+ };
+ }
+
+ componentDidCatch(error: unknown) {
+ console.error('Root render failed', error);
+ }
+
+ render() {
+ if (this.state.error) {
+ return (
+
+
+
App failed to render
+
{this.state.error}
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+function BootstrapApp() {
+ const [boot, setBoot] = useState({ ready: false });
+
+ useEffect(() => {
+ let alive = true;
+
+ void (async () => {
+ if (isTauri()) {
+ try {
+ await appDb.init();
+ } catch (error) {
+ const detail = formatError(error);
+ console.error('App DB initialization failed', error);
+ useDailyCheckStore.setState({
+ errorMsg: `DB initialization failed. ${detail}`,
+ });
+ }
+ }
+
+ try {
+ await useDailyCheckStore.getState().hydrate();
+ } catch (error) {
+ const detail = formatError(error);
+ console.error('App bootstrap failed', error);
+ useDailyCheckStore.setState({
+ hydrated: true,
+ errorMsg: `App bootstrap failed. ${detail}`,
+ });
+ }
+
+ if (alive) {
+ setBoot({ ready: true });
+ }
+ })();
+
+ return () => {
+ alive = false;
+ };
+ }, []);
+
+ if (!boot.ready) {
+ return (
+
+ Loading data...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/apps/desktop/src/shared/hooks/useToast.ts b/apps/desktop/src/shared/hooks/useToast.ts
new file mode 100644
index 0000000..39ad7cb
--- /dev/null
+++ b/apps/desktop/src/shared/hooks/useToast.ts
@@ -0,0 +1,29 @@
+import { useRef, useState } from 'react';
+
+export function useToast(durationMs: number = 2000) {
+ const [toast, setToast] = useState(null); // (role: toast message, type: string | null)
+ const timerRef = useRef(null); // (role: timeout id, type: number | null)
+
+ const showToast = (msg: string) => {
+ setToast(msg);
+
+ if (timerRef.current) {
+ window.clearTimeout(timerRef.current);
+ }
+
+ timerRef.current = window.setTimeout(() => {
+ setToast(null);
+ timerRef.current = null;
+ }, durationMs);
+ };
+
+ const clearToast = () => {
+ if (timerRef.current) {
+ window.clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ setToast(null);
+ };
+
+ return { toast, showToast, clearToast };
+}
diff --git a/apps/desktop/src/shared/schemas.ts b/apps/desktop/src/shared/schemas.ts
new file mode 100644
index 0000000..81fca66
--- /dev/null
+++ b/apps/desktop/src/shared/schemas.ts
@@ -0,0 +1,115 @@
+import { z } from 'zod';
+
+// (role: day-of-week token schema, type: zod schema)
+export const DayOfWeekSchema = z.union([
+ z.literal('Mon'),
+ z.literal('Tue'),
+ z.literal('Wed'),
+ z.literal('Thu'),
+ z.literal('Fri'),
+ z.literal('Sat'),
+ z.literal('Sun'),
+]);
+
+// (role: category discriminator schema, type: zod schema)
+export const CategorySchema = z.union([
+ z.literal('weekday'),
+ z.literal('weekend'),
+ z.literal('daily'),
+ z.literal('custom'),
+]);
+
+const AutoArchiveAfterSchema = z.preprocess((value) => {
+ if (value === '' || value == null) return null;
+
+ const num =
+ typeof value === 'number'
+ ? value
+ : typeof value === 'string'
+ ? Number(value)
+ : NaN;
+
+ if (!Number.isInteger(num) || num < 1) return null;
+ return num;
+}, z.number().int().min(1).nullable());
+
+const RepeatCountSchema = z.preprocess((value) => {
+ if (value === '' || value == null) return null;
+
+ const num =
+ typeof value === 'number'
+ ? value
+ : typeof value === 'string'
+ ? Number(value)
+ : NaN;
+
+ if (!Number.isInteger(num) || num < 1) return null;
+ return num;
+}, z.number().int().min(1).nullable());
+
+const StartYmdSchema = z.preprocess((value) => {
+ if (value === '' || value == null) return null;
+ if (typeof value !== 'string') return null;
+ const ymd = value.trim();
+ return /^\d{4}-\d{2}-\d{2}$/.test(ymd) ? ymd : null;
+}, z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable());
+
+// (role: task schema, type: zod schema)
+export const TaskSchema = z.object({
+ id: z.string().min(1),
+ title: z.string().min(1),
+ description: z.string().optional().default(''),
+ category: CategorySchema,
+ daysOfWeek: z.array(DayOfWeekSchema).min(1),
+ durationMinutes: z.number().int().min(1).max(720),
+ startYmd: StartYmdSchema.optional().default(null),
+ autoArchiveAfter: AutoArchiveAfterSchema.optional().default(null),
+ repeatCount: RepeatCountSchema.optional().default(null),
+ isActive: z.boolean(),
+ createdAt: z.string().min(1),
+});
+
+// (role: task list schema, type: zod schema)
+export const TasksSchema = z.array(TaskSchema);
+
+// (role: completion schema, type: zod schema)
+export const CompletionSchema = z.object({
+ taskId: z.string().min(1),
+ date: z.string().min(1),
+});
+
+// (role: completions schema, type: zod schema)
+export const CompletionsSchema = z.array(CompletionSchema);
+
+// (role: time entry schema, type: zod schema)
+export const TimeEntrySchema = z.object({
+ id: z.string().min(1),
+ taskId: z.string().min(1),
+ date: z.string().min(1),
+ startedAt: z.string().min(1),
+ endedAt: z.string().nullable(),
+ minutes: z
+ .number()
+ .int()
+ .min(0)
+ .max(24 * 60),
+});
+
+// (role: time entry list schema, type: zod schema)
+export const TimeEntriesSchema = z.array(TimeEntrySchema);
+
+// (role: memo schema, type: zod schema)
+export const TaskDailyMemoSchema = z.object({
+ id: z.string().min(1),
+ taskId: z.string().min(1),
+ date: z.string().min(1),
+ text: z.string(),
+ updatedAt: z.string().min(1),
+});
+
+// (role: memo list schema, type: zod schema)
+export const TaskDailyMemosSchema = z.array(TaskDailyMemoSchema);
+
+// (role: inferred types, type: types)
+export type DayOfWeek = z.infer;
+export type Category = z.infer;
diff --git a/apps/desktop/src/shared/types.ts b/apps/desktop/src/shared/types.ts
new file mode 100644
index 0000000..c5c4dda
--- /dev/null
+++ b/apps/desktop/src/shared/types.ts
@@ -0,0 +1,48 @@
+// (role: day-of-week token, type: union)
+export type DayOfWeek = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun';
+
+// (role: category discriminator, type: union)
+export type Category = 'weekday' | 'weekend' | 'daily' | 'custom';
+
+// (role: base fields shared by all tasks, type: interface)
+export interface TaskBase {
+ id: string; // (role: task id, type: string)
+ title: string; // (role: task title, type: string)
+ description: string; // (role: persistent text, type: string)
+ category: Category; // (role: schedule rule, type: Category)
+ daysOfWeek: readonly DayOfWeek[]; // (role: schedule days, type: readonly DayOfWeek[])
+ durationMinutes: number; // (role: planned duration, type: minutes)
+ startYmd?: string | null; // (role: first eligible date YYYY-MM-DD, type: string | null | undefined)
+ autoArchiveAfter?: number | null; // (role: threshold, type: number | null | undefined)
+ repeatCount?: number | null; // (role: weekly max occurrences, type: number | null | undefined)
+ isActive: boolean; // (role: archive flag, type: boolean)
+ createdAt: string; // (role: ISO timestamp, type: string)
+}
+
+// (role: unified task type, type: alias)
+export type Task = TaskBase;
+
+// (role: completion record, type: interface)
+export interface Completion {
+ taskId: string; // (role: completed task id, type: string)
+ date: string; // (role: YYYY-MM-DD, type: string)
+}
+
+// (role: time tracking record, type: interface)
+export interface TimeEntry {
+ id: string; // (role: time entry id, type: string)
+ taskId: string; // (role: task id, type: string)
+ date: string; // (role: YYYY-MM-DD, type: string)
+ startedAt: string; // (role: ISO timestamp, type: string)
+ endedAt: string | null; // (role: ISO timestamp or null if running, type: string | null)
+ minutes: number; // (role: computed minutes, type: number)
+}
+
+// (role: per-task per-day memo, type: interface)
+export interface TaskDailyMemo {
+ id: string; // (role: unique memo id, type: string)
+ taskId: string; // (role: task id, type: string)
+ date: string; // (role: YYYY-MM-DD, type: string)
+ text: string; // (role: memo text, type: string)
+ updatedAt: string; // (role: ISO timestamp, type: string)
+}
diff --git a/apps/desktop/src/shared/utils/date.ts b/apps/desktop/src/shared/utils/date.ts
new file mode 100644
index 0000000..8f37873
--- /dev/null
+++ b/apps/desktop/src/shared/utils/date.ts
@@ -0,0 +1,55 @@
+import type { DayOfWeek } from '../types';
+
+// (role: convert Date -> YYYY-MM-DD (local), type: (Date) => string)
+export function toYmd(d: Date): string {
+ const yyyy = d.getFullYear();
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
+ const dd = String(d.getDate()).padStart(2, '0');
+ return `${yyyy}-${mm}-${dd}`;
+}
+
+// (role: get DayOfWeek from local date, type: (Date) => DayOfWeek)
+export function dayOfWeek(d: Date): DayOfWeek {
+ const idx = d.getDay(); // 0=Sun..6=Sat
+ const map: Record = {
+ 0: 'Sun',
+ 1: 'Mon',
+ 2: 'Tue',
+ 3: 'Wed',
+ 4: 'Thu',
+ 5: 'Fri',
+ 6: 'Sat',
+ };
+ return map[idx];
+}
+
+// (role: monday start of week, type: (Date) => Date)
+export function startOfWeekMonday(date: Date): Date {
+ const d = new Date(date);
+ const day = d.getDay(); // 0=Sun..6=Sat
+ const diff = (day === 0 ? -6 : 1) - day; // Monday 기준
+ d.setDate(d.getDate() + diff);
+ d.setHours(0, 0, 0, 0);
+ return d;
+}
+
+// (role: build 7 YYYY-MM-DD strings from weekStartYmd, type: (string) => string[])
+export function buildWeekDates(weekStartYmd: string): string[] {
+ const [y, m, d] = weekStartYmd.split('-').map(Number);
+ const start = new Date(y, m - 1, d);
+ const dates: string[] = [];
+ for (let i = 0; i < 7; i++) {
+ const cur = new Date(start);
+ cur.setDate(start.getDate() + i);
+ dates.push(toYmd(cur));
+ }
+ return dates;
+}
+
+// (role: minutes diff helper, type: (string,string)=>number)
+export function diffMinutes(startIso: string, endIso: string): number {
+ const start = new Date(startIso).getTime();
+ const end = new Date(endIso).getTime();
+ const ms = Math.max(0, end - start);
+ return Math.floor(ms / 60000);
+}
diff --git a/apps/desktop/src/styles/index.css b/apps/desktop/src/styles/index.css
new file mode 100644
index 0000000..d4b5078
--- /dev/null
+++ b/apps/desktop/src/styles/index.css
@@ -0,0 +1 @@
+@import 'tailwindcss';
diff --git a/apps/desktop/tsconfig.app.json b/apps/desktop/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/apps/desktop/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/apps/desktop/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/apps/desktop/tsconfig.node.json b/apps/desktop/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/apps/desktop/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts
new file mode 100644
index 0000000..125d4d1
--- /dev/null
+++ b/apps/desktop/vite.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ base: './',
+ server: {
+ host: '127.0.0.1',
+ port: 5173,
+ strictPort: true,
+ },
+});
diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml
new file mode 100644
index 0000000..ff06670
--- /dev/null
+++ b/apps/server/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "server"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs
new file mode 100644
index 0000000..e7a11a9
--- /dev/null
+++ b/apps/server/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("Hello, world!");
+}
diff --git a/crates/frilday-core/Cargo.toml b/crates/frilday-core/Cargo.toml
new file mode 100644
index 0000000..fec7b96
--- /dev/null
+++ b/crates/frilday-core/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "frilday-core"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
diff --git a/crates/frilday-core/src/lib.rs b/crates/frilday-core/src/lib.rs
new file mode 100644
index 0000000..b93cf3f
--- /dev/null
+++ b/crates/frilday-core/src/lib.rs
@@ -0,0 +1,14 @@
+pub fn add(left: u64, right: u64) -> u64 {
+ left + right
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn it_works() {
+ let result = add(2, 2);
+ assert_eq!(result, 4);
+ }
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..5e96581
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,196 @@
+# FrilDay Architecture
+
+FrilDay is designed as a cloud-first productivity application, while the first release focuses on the desktop experience.
+
+The project starts with a Tauri desktop app and an Axum server, then gradually expands to mobile and cloud synchronization.
+
+## Goals
+
+- Release the desktop app first
+- Use Axum as the API layer
+- Keep business logic independent from UI and transport layers
+- Prepare for future mobile and cloud-first usage
+- Avoid duplicating task, schedule, and statistics logic across clients
+
+## High-Level Structure
+
+```text
+frilday/
+
+apps/
+ desktop/ # Tauri + React desktop app
+ server/ # Axum HTTP API server
+
+crates/
+ frilday-core/ # Core domain and application logic
+```
+
+## Runtime Flow
+
+### Desktop v0.1
+
+```text
+React Desktop
+ ↓ HTTP
+Local Axum Server
+ ↓
+frilday-core
+ ↓
+SQLite
+```
+
+The desktop app calls the local Axum server through HTTP. This keeps the desktop client close to the future cloud API structure.
+
+### Future Cloud Version
+
+```text
+Desktop / Mobile / Web
+ ↓ HTTP
+Cloud Axum Server
+ ↓
+frilday-core
+ ↓
+PostgreSQL
+```
+
+The long-term goal is cloud-first usage, where user data is stored on the server and shared across devices.
+
+## Layer Responsibilities
+
+### apps/desktop
+
+Responsible for:
+
+- React UI
+- Tauri shell
+- Pages and user interaction
+- Calling the API
+- Desktop packaging
+
+It should not contain core business rules.
+
+### apps/server
+
+Responsible for:
+
+- Axum routes
+- HTTP request/response handling
+- API validation
+- Authentication in the future
+- Calling `frilday-core`
+
+It should not own core business rules.
+
+### crates/frilday-core
+
+Responsible for:
+
+- Task domain logic
+- Schedule rules
+- Completion rules
+- Statistics calculation
+- Core services
+- Repository traits
+
+It must not depend on:
+
+- React
+- Tauri
+- Axum
+- SQLite
+- PostgreSQL
+- HTTP
+
+## Dependency Direction
+
+```text
+apps/desktop ─┐
+ ├──▶ crates/frilday-core
+apps/server ─┘
+```
+
+`frilday-core` must not know whether it is used by Desktop, Server, Mobile, or Web.
+
+## Initial Development Plan
+
+1. Move the current Tauri app into `apps/desktop`
+2. Create `apps/server`
+3. Create `crates/frilday-core`
+4. Move domain logic into `frilday-core`
+5. Add basic Axum routes:
+ - `GET /health`
+ - `GET /tasks`
+ - `POST /tasks`
+
+6. Make the desktop app call the local Axum server
+7. Use SQLite for the first desktop release
+8. Later replace or extend storage with PostgreSQL for cloud-first usage
+
+## Design Principle
+
+FrilDay should be built around one core idea:
+
+```text
+UI and transport layers may change.
+Core rules should remain stable.
+```
+
+This allows the project to grow from a desktop app into a cloud-first multi-platform product without rewriting the business logic.
+
+## Git Workflow
+
+The repository is moving from a single desktop app into a monorepo. Changes should be grouped by layer so the move stays reviewable.
+
+### Branch Strategy
+
+- `main` stays deployable and buildable
+- short-lived feature branches start from `main`
+- prefer small PRs that touch one concern:
+ - desktop UI
+ - server API
+ - shared core
+ - docs / tooling
+
+Suggested branch names:
+
+- `feat/desktop-timer`
+- `feat/server-health-route`
+- `refactor/core-task-rules`
+- `docs/architecture-readme`
+- `chore/gitignore-workspace`
+
+### Commit Scope
+
+Keep commits intentional and easy to revert.
+
+- move files without behavior changes in one commit
+- change imports / wiring in a separate commit
+- change behavior in another commit
+- update docs when architecture or workflow changes
+
+Suggested commit prefixes:
+
+- `feat:`
+- `fix:`
+- `refactor:`
+- `docs:`
+- `chore:`
+
+### Integration Order
+
+When a feature spans multiple layers, merge in this order when possible:
+
+1. `crates/frilday-core`
+2. `apps/server`
+3. `apps/desktop`
+4. docs and cleanup
+
+This keeps dependency flow aligned with the intended architecture.
+
+### Pull Request Checklist
+
+- desktop build still passes
+- server compiles if touched
+- core crate tests pass if touched
+- docs reflect structural changes
+- no generated build outputs are committed unless intentional