From 33f34080292808113669911d0085752968a8b9d4 Mon Sep 17 00:00:00 2001 From: Anonfedora Date: Wed, 8 Jul 2026 15:03:51 +0100 Subject: [PATCH 1/7] feat: core features --- .github/workflows/ci.yml | 102 + .gitignore | 49 + Cargo.toml | 61 + README.md | 95 +- apps/cli/Cargo.toml | 16 + apps/cli/src/main.rs | 88 + apps/desktop/.gitignore | 24 + apps/desktop/README.md | 75 + apps/desktop/eslint.config.js | 22 + apps/desktop/index.html | 13 + apps/desktop/package-lock.json | 3885 +++++++++++++++++ apps/desktop/package.json | 38 + apps/desktop/postcss.config.js | 6 + apps/desktop/public/favicon.svg | 1 + apps/desktop/public/icons.svg | 24 + apps/desktop/src-tauri/Cargo.toml | 24 + apps/desktop/src-tauri/build.rs | 3 + apps/desktop/src-tauri/icons/icon.png | Bin 0 -> 70 bytes apps/desktop/src-tauri/src/main.rs | 175 + apps/desktop/src-tauri/tauri.conf.json | 45 + apps/desktop/src/App.css | 0 apps/desktop/src/App.tsx | 625 +++ apps/desktop/src/assets/hero.png | Bin 0 -> 13057 bytes apps/desktop/src/assets/react.svg | 1 + apps/desktop/src/assets/vite.svg | 1 + apps/desktop/src/index.css | 18 + apps/desktop/src/main.tsx | 10 + apps/desktop/tailwind.config.js | 11 + apps/desktop/tsconfig.app.json | 26 + apps/desktop/tsconfig.json | 7 + apps/desktop/tsconfig.node.json | 23 + apps/desktop/vite.config.ts | 7 + clippy.toml | 4 + crates/clipboard-ai/Cargo.toml | 18 + crates/clipboard-ai/src/ai.rs | 39 + crates/clipboard-ai/src/lib.rs | 19 + crates/clipboard-api/Cargo.toml | 21 + crates/clipboard-api/src/api.rs | 77 + crates/clipboard-api/src/handlers.rs | 3 + crates/clipboard-api/src/lib.rs | 20 + crates/clipboard-cli/Cargo.toml | 18 + crates/clipboard-cli/src/lib.rs | 3 + crates/clipboard-core/Cargo.toml | 19 + crates/clipboard-core/src/clipboard.rs | 39 + crates/clipboard-core/src/item.rs | 55 + crates/clipboard-core/src/lib.rs | 77 + crates/clipboard-daemon/Cargo.toml | 29 + crates/clipboard-daemon/src/main.rs | 267 ++ crates/clipboard-db/Cargo.toml | 20 + .../clipboard-db/migrations/001_initial.sql | 100 + crates/clipboard-db/src/database.rs | 163 + crates/clipboard-db/src/lib.rs | 24 + crates/clipboard-db/src/models.rs | 18 + crates/clipboard-db/src/schema.rs | 3 + crates/clipboard-encryption/Cargo.toml | 19 + crates/clipboard-encryption/src/encryption.rs | 53 + .../src/key_derivation.rs | 81 + crates/clipboard-encryption/src/lib.rs | 23 + crates/clipboard-events/Cargo.toml | 18 + crates/clipboard-events/src/bus.rs | 30 + crates/clipboard-events/src/event.rs | 77 + crates/clipboard-events/src/lib.rs | 19 + crates/clipboard-ipc/Cargo.toml | 22 + crates/clipboard-ipc/src/error.rs | 24 + crates/clipboard-ipc/src/ipc.rs | 270 ++ crates/clipboard-ipc/src/lib.rs | 7 + crates/clipboard-platform/Cargo.toml | 30 + crates/clipboard-platform/src/lib.rs | 19 + crates/clipboard-platform/src/provider.rs | 247 ++ crates/clipboard-plugin/Cargo.toml | 18 + crates/clipboard-plugin/src/lib.rs | 22 + crates/clipboard-plugin/src/plugin.rs | 39 + crates/clipboard-plugin/src/sandbox.rs | 3 + crates/clipboard-search/Cargo.toml | 18 + crates/clipboard-search/src/lib.rs | 17 + crates/clipboard-search/src/search.rs | 48 + crates/clipboard-storage/Cargo.toml | 19 + crates/clipboard-storage/src/lib.rs | 21 + crates/clipboard-storage/src/storage.rs | 67 + crates/clipboard-sync/Cargo.toml | 16 + crates/clipboard-sync/src/lib.rs | 19 + crates/clipboard-sync/src/sync.rs | 46 + docs/ARCHITECTURE.md | 810 ++++ docs/CONTRIBUTING.md | 563 +++ docs/ROADMAP.md | 438 ++ docs/SECURITY.md | 545 +++ rustfmt.toml | 15 + 87 files changed, 10173 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 apps/cli/Cargo.toml create mode 100644 apps/cli/src/main.rs create mode 100644 apps/desktop/.gitignore create mode 100644 apps/desktop/README.md create mode 100644 apps/desktop/eslint.config.js create mode 100644 apps/desktop/index.html create mode 100644 apps/desktop/package-lock.json create mode 100644 apps/desktop/package.json create mode 100644 apps/desktop/postcss.config.js create mode 100644 apps/desktop/public/favicon.svg create mode 100644 apps/desktop/public/icons.svg create mode 100644 apps/desktop/src-tauri/Cargo.toml create mode 100644 apps/desktop/src-tauri/build.rs create mode 100644 apps/desktop/src-tauri/icons/icon.png create mode 100644 apps/desktop/src-tauri/src/main.rs create mode 100644 apps/desktop/src-tauri/tauri.conf.json create mode 100644 apps/desktop/src/App.css create mode 100644 apps/desktop/src/App.tsx create mode 100644 apps/desktop/src/assets/hero.png create mode 100644 apps/desktop/src/assets/react.svg create mode 100644 apps/desktop/src/assets/vite.svg create mode 100644 apps/desktop/src/index.css create mode 100644 apps/desktop/src/main.tsx create mode 100644 apps/desktop/tailwind.config.js create mode 100644 apps/desktop/tsconfig.app.json create mode 100644 apps/desktop/tsconfig.json create mode 100644 apps/desktop/tsconfig.node.json create mode 100644 apps/desktop/vite.config.ts create mode 100644 clippy.toml create mode 100644 crates/clipboard-ai/Cargo.toml create mode 100644 crates/clipboard-ai/src/ai.rs create mode 100644 crates/clipboard-ai/src/lib.rs create mode 100644 crates/clipboard-api/Cargo.toml create mode 100644 crates/clipboard-api/src/api.rs create mode 100644 crates/clipboard-api/src/handlers.rs create mode 100644 crates/clipboard-api/src/lib.rs create mode 100644 crates/clipboard-cli/Cargo.toml create mode 100644 crates/clipboard-cli/src/lib.rs create mode 100644 crates/clipboard-core/Cargo.toml create mode 100644 crates/clipboard-core/src/clipboard.rs create mode 100644 crates/clipboard-core/src/item.rs create mode 100644 crates/clipboard-core/src/lib.rs create mode 100644 crates/clipboard-daemon/Cargo.toml create mode 100644 crates/clipboard-daemon/src/main.rs create mode 100644 crates/clipboard-db/Cargo.toml create mode 100644 crates/clipboard-db/migrations/001_initial.sql create mode 100644 crates/clipboard-db/src/database.rs create mode 100644 crates/clipboard-db/src/lib.rs create mode 100644 crates/clipboard-db/src/models.rs create mode 100644 crates/clipboard-db/src/schema.rs create mode 100644 crates/clipboard-encryption/Cargo.toml create mode 100644 crates/clipboard-encryption/src/encryption.rs create mode 100644 crates/clipboard-encryption/src/key_derivation.rs create mode 100644 crates/clipboard-encryption/src/lib.rs create mode 100644 crates/clipboard-events/Cargo.toml create mode 100644 crates/clipboard-events/src/bus.rs create mode 100644 crates/clipboard-events/src/event.rs create mode 100644 crates/clipboard-events/src/lib.rs create mode 100644 crates/clipboard-ipc/Cargo.toml create mode 100644 crates/clipboard-ipc/src/error.rs create mode 100644 crates/clipboard-ipc/src/ipc.rs create mode 100644 crates/clipboard-ipc/src/lib.rs create mode 100644 crates/clipboard-platform/Cargo.toml create mode 100644 crates/clipboard-platform/src/lib.rs create mode 100644 crates/clipboard-platform/src/provider.rs create mode 100644 crates/clipboard-plugin/Cargo.toml create mode 100644 crates/clipboard-plugin/src/lib.rs create mode 100644 crates/clipboard-plugin/src/plugin.rs create mode 100644 crates/clipboard-plugin/src/sandbox.rs create mode 100644 crates/clipboard-search/Cargo.toml create mode 100644 crates/clipboard-search/src/lib.rs create mode 100644 crates/clipboard-search/src/search.rs create mode 100644 crates/clipboard-storage/Cargo.toml create mode 100644 crates/clipboard-storage/src/lib.rs create mode 100644 crates/clipboard-storage/src/storage.rs create mode 100644 crates/clipboard-sync/Cargo.toml create mode 100644 crates/clipboard-sync/src/lib.rs create mode 100644 crates/clipboard-sync/src/sync.rs create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/SECURITY.md create mode 100644 rustfmt.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4b11e1c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + rust: [stable] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo check + run: cargo check --all --verbose + + - name: Run cargo test + run: cargo test --all --verbose + + - name: Run cargo clippy + run: cargo clippy --all -- -D warnings + + - name: Run cargo fmt + run: cargo fmt --all -- --check + + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + rust: [stable] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo build + run: cargo build --all --release --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..118f24d --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Rust +/target/ +**/*.rs.bk +Cargo.lock + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Tauri +apps/desktop/src-tauri/target/ +apps/desktop/src-tauri/Cargo.lock +apps/desktop/src-tauri/wix-target/ +apps/desktop/src-tauri/gen/ + +# Build artifacts +*.o +*.so +*.dylib +*.dll +*.exe +*.pdb +*.lib + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log + +# Environment +.env +.env.local +.env.*.local + +# Temporary files +tmp/ +temp/ +*.tmp + +# OS +Thumbs.db +.DS_Store diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6d95e50 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,61 @@ +[workspace] +members = [ + "apps/desktop/src-tauri", + "apps/cli", + "crates/clipboard-core", + "crates/clipboard-db", + "crates/clipboard-search", + "crates/clipboard-storage", + "crates/clipboard-encryption", + "crates/clipboard-platform", + "crates/clipboard-events", + "crates/clipboard-sync", + "crates/clipboard-api", + "crates/clipboard-cli", + "crates/clipboard-plugin", + "crates/clipboard-ai", + "crates/clipboard-ipc", + "crates/clipboard-daemon", +] + +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +authors = ["OpenPaste Contributors"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/openpaste/openpaste" +homepage = "https://github.com/openpaste/openpaste" + +[workspace.dependencies] +# Async runtime +tokio = { version = "1.35", features = ["full"] } +# Database +sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "chrono"] } +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +# Error handling +thiserror = "1.0" +anyhow = "1.0" +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# Cryptography +aes-gcm = "0.10" +argon2 = "0.5" +# Compression +zstd = "0.13" +# WASM +wasmtime = "15.0" +# HTTP +axum = "0.7" +reqwest = { version = "0.11", features = ["json"] } +# CLI +clap = { version = "4.4", features = ["derive"] } +# UUID +uuid = { version = "1.6", features = ["v4", "serde"] } +# Time +chrono = { version = "0.4", features = ["serde"] } diff --git a/README.md b/README.md index 3483229..e3d7a9c 100644 --- a/README.md +++ b/README.md @@ -1 +1,94 @@ -# openpaste \ No newline at end of file +# OpenPaste + +A modern, cross-platform clipboard manager with advanced features like encryption, search, and sync. + +## Current Status + +**Version:** 0.1.0 (Development) + +**Status:** Active Development + +## Features Implemented + +### Phase 1: Foundation ✅ +- Project architecture with Rust workspace +- Tauri + React desktop application +- Core clipboard capture (macOS) +- SQLite database with migrations +- IPC communication (Unix domain sockets) +- Basic search functionality +- Metallic UI design + +### Phase 2: Core Features (In Progress) +- Clipboard history management +- Pin and favorite items +- Delete items +- Copy to clipboard +- Auto-refresh (2-second polling) +- Deduplication (hash-based) +- Custom title bar with window controls +- FTS5 full-text search with triggers +- Zstd compression for storage +- AES-256-GCM encryption with proper nonce handling +- Argon2id key derivation with secure parameters + +## Technology Stack + +**Backend:** +- Rust +- SQLite (with sqlx) +- Tokio async runtime +- Unix domain sockets (IPC) + +**Frontend:** +- React + TypeScript +- Tauri +- Tailwind CSS +- Lucide React icons + +## Running the Application + +### Prerequisites +- Rust (latest stable) +- Node.js (18+) +- pnpm + +### Development + +1. Start the clipboard daemon: +```bash +cargo run --bin openpaste-daemon +``` + +2. Start the desktop app: +```bash +cd apps/desktop +pnpm install +pnpm tauri dev +``` + +## Project Structure + +``` +openpaste/ +├── apps/ +│ └── desktop/ # Tauri desktop application +├── crates/ +│ ├── clipboard-core/ # Core clipboard types +│ ├── clipboard-db/ # Database operations +│ ├── clipboard-ipc/ # IPC communication +│ └── clipboard-daemon/ # Background daemon +└── docs/ # Documentation +``` + +## Roadmap + +See [ROADMAP.md](docs/ROADMAP.md) for detailed planning. + +## Contributing + +See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for contribution guidelines. + +## License + +TBD \ No newline at end of file diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml new file mode 100644 index 0000000..e381f69 --- /dev/null +++ b/apps/cli/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "openpaste-cli" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +name = "openpaste" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +anyhow = { workspace = true } diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs new file mode 100644 index 0000000..9d56d5e --- /dev/null +++ b/apps/cli/src/main.rs @@ -0,0 +1,88 @@ +//! OpenPaste CLI binary + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "openpaste")] +#[command(about = "OpenPaste clipboard manager CLI", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Search clipboard history + Search { query: String }, + /// Get clipboard item by ID + Get { id: String }, + /// Copy item to clipboard + Copy { id: String }, + /// List clipboard items + List { + #[arg(short, long, default_value_t = 20)] + limit: usize, + }, + /// Pin an item + Pin { id: String }, + /// Favorite an item + Favorite { id: String }, + /// Delete an item + Delete { id: String }, + /// Show daemon status + Status, + /// Start daemon + DaemonStart, + /// Stop daemon + DaemonStop, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Search { query } => { + println!("Searching for: {}", query); + // TODO: Implement search + } + Commands::Get { id } => { + println!("Getting item: {}", id); + // TODO: Implement get + } + Commands::Copy { id } => { + println!("Copying item: {}", id); + // TODO: Implement copy + } + Commands::List { limit } => { + println!("Listing {} items", limit); + // TODO: Implement list + } + Commands::Pin { id } => { + println!("Pinning item: {}", id); + // TODO: Implement pin + } + Commands::Favorite { id } => { + println!("Favoriting item: {}", id); + // TODO: Implement favorite + } + Commands::Delete { id } => { + println!("Deleting item: {}", id); + // TODO: Implement delete + } + Commands::Status => { + println!("Daemon status"); + // TODO: Implement status + } + Commands::DaemonStart => { + println!("Starting daemon"); + // TODO: Implement daemon start + } + Commands::DaemonStop => { + println!("Stopping daemon"); + // TODO: Implement daemon stop + } + } + + Ok(()) +} diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/desktop/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 0000000..c300135 --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,75 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) + +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) + +``` diff --git a/apps/desktop/eslint.config.js b/apps/desktop/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/apps/desktop/eslint.config.js @@ -0,0 +1,22 @@ +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: { + globals: globals.browser, + }, + }, +]) diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 0000000..03fbbe0 --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + + desktop + + +
+ + + diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json new file mode 100644 index 0000000..597b928 --- /dev/null +++ b/apps/desktop/package-lock.json @@ -0,0 +1,3885 @@ +{ + "name": "desktop", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "desktop", + "version": "0.0.0", + "dependencies": { + "@tauri-apps/api": "^1.6.0", + "lucide-react": "^1.23.0", + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tauri-apps/cli": "^1.6.3", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "autoprefixer": "^10.5.2", + "eslint": "^10.6.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.7.0", + "postcss": "^8.5.16", + "tailwindcss": "^3.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.62.0", + "vite": "^8.1.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz", + "integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 14.6.0", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.3.tgz", + "integrity": "sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "dependencies": { + "semver": ">=7.5.2" + }, + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "1.6.3", + "@tauri-apps/cli-darwin-x64": "1.6.3", + "@tauri-apps/cli-linux-arm-gnueabihf": "1.6.3", + "@tauri-apps/cli-linux-arm64-gnu": "1.6.3", + "@tauri-apps/cli-linux-arm64-musl": "1.6.3", + "@tauri-apps/cli-linux-x64-gnu": "1.6.3", + "@tauri-apps/cli-linux-x64-musl": "1.6.3", + "@tauri-apps/cli-win32-arm64-msvc": "1.6.3", + "@tauri-apps/cli-win32-ia32-msvc": "1.6.3", + "@tauri-apps/cli-win32-x64-msvc": "1.6.3" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.3.tgz", + "integrity": "sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.3.tgz", + "integrity": "sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.3.tgz", + "integrity": "sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.3.tgz", + "integrity": "sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.3.tgz", + "integrity": "sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.3.tgz", + "integrity": "sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.3.tgz", + "integrity": "sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.3.tgz", + "integrity": "sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.3.tgz", + "integrity": "sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.3.tgz", + "integrity": "sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", + "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..9efe228 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,38 @@ +{ + "name": "desktop", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "tauri": "tauri", + "tauri:dev": "tauri dev" + }, + "dependencies": { + "@tauri-apps/api": "^1.6.0", + "lucide-react": "^1.23.0", + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tauri-apps/cli": "^1.6.3", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "autoprefixer": "^10.5.2", + "eslint": "^10.6.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.7.0", + "postcss": "^8.5.16", + "tailwindcss": "^3.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.62.0", + "vite": "^8.1.1" + } +} diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/apps/desktop/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/apps/desktop/public/favicon.svg b/apps/desktop/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/apps/desktop/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/desktop/public/icons.svg b/apps/desktop/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/apps/desktop/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..0d842b0 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "openpaste-desktop" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[build-dependencies] +tauri-build = { version = "1.5", features = [] } + +[dependencies] +tauri = { version = "1.5", features = ["shell-open"] } +serde = { workspace = true } +serde_json = { workspace = true } +clipboard-core = { path = "../../../crates/clipboard-core" } +clipboard-ipc = { path = "../../../crates/clipboard-ipc" } +chrono = { workspace = true } +dirs = "5.0" + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /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/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..08cd6f2bfd1b53ec5a4db72bed55f40907e8bdfa GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZY8JuI3K{zz}&{z5M@%E Q4U}N;boFyt=akR{0J, + pinned: bool, + favorite: bool, +} + +fn get_ipc_socket_path() -> PathBuf { + let data_dir = dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("openpaste"); + + #[cfg(unix)] + let socket_path = data_dir.join("openpaste.sock"); + + #[cfg(windows)] + let socket_path = data_dir.join("openpaste.pipe"); + + socket_path +} + +#[tauri::command] +async fn get_clipboard_history() -> Result, String> { + // Connect to daemon via IPC + let socket_path = get_ipc_socket_path(); + let client = IpcClient::new(socket_path.to_string_lossy().to_string()); + + match client.send(IpcMessage::GetHistory).await { + Ok(IpcMessage::ClipboardHistory { items }) => { + let clipboard_items: Vec = items + .into_iter() + .map(|item| ClipboardItem { + id: item.id, + content_type: item.content_type, + content: String::from_utf8_lossy(&item.content).to_string(), + hash: item.hash, + created_at: item.created_at, + accessed_at: item.accessed_at, + pinned: item.pinned, + favorite: item.favorite, + }) + .collect(); + Ok(clipboard_items) + } + Ok(IpcMessage::Error { message }) => Err(format!("Daemon error: {}", message)), + Err(e) => Err(format!("IPC error: {}", e)), + _ => Err("Unexpected response from daemon".to_string()), + } +} + +#[tauri::command] +async fn search_clipboard_items(query: String) -> Result, String> { + let socket_path = get_ipc_socket_path(); + let client = IpcClient::new(socket_path.to_string_lossy().to_string()); + + match client.send(IpcMessage::SearchItems { query }).await { + Ok(IpcMessage::ClipboardHistory { items }) => { + let clipboard_items: Vec = items + .into_iter() + .map(|item| ClipboardItem { + id: item.id, + content_type: item.content_type, + content: String::from_utf8_lossy(&item.content).to_string(), + hash: item.hash, + created_at: item.created_at, + accessed_at: item.accessed_at, + pinned: item.pinned, + favorite: item.favorite, + }) + .collect(); + Ok(clipboard_items) + } + Ok(IpcMessage::Error { message }) => Err(format!("Daemon error: {}", message)), + Err(e) => Err(format!("IPC error: {}", e)), + _ => Err("Unexpected response from daemon".to_string()), + } +} + +#[tauri::command] +async fn set_clipboard_content(content: String) -> Result<(), String> { + let socket_path = get_ipc_socket_path(); + let client = IpcClient::new(socket_path.to_string_lossy().to_string()); + + match client + .send(IpcMessage::SetClipboard { + content: content.into_bytes(), + }) + .await + { + Ok(IpcMessage::ClipboardContent { .. }) => Ok(()), + Ok(IpcMessage::Error { message }) => Err(format!("Daemon error: {}", message)), + Err(e) => Err(format!("IPC error: {}", e)), + _ => Err("Unexpected response from daemon".to_string()), + } +} + +#[tauri::command] +async fn get_current_clipboard() -> Result { + let socket_path = get_ipc_socket_path(); + let client = IpcClient::new(socket_path.to_string_lossy().to_string()); + + match client.send(IpcMessage::GetClipboard).await { + Ok(IpcMessage::ClipboardContent { content }) => { + Ok(String::from_utf8_lossy(&content).to_string()) + } + Ok(IpcMessage::Error { message }) => Err(format!("Daemon error: {}", message)), + Err(e) => Err(format!("IPC error: {}", e)), + _ => Err("Unexpected response from daemon".to_string()), + } +} + +#[tauri::command] +async fn delete_clipboard_item(id: i64) -> Result<(), String> { + let socket_path = get_ipc_socket_path(); + let client = IpcClient::new(socket_path.to_string_lossy().to_string()); + + match client.send(IpcMessage::DeleteItem { id }).await { + Ok(IpcMessage::Success) => Ok(()), + Ok(IpcMessage::Error { message }) => Err(format!("Daemon error: {}", message)), + Err(e) => Err(format!("IPC error: {}", e)), + _ => Err("Unexpected response from daemon".to_string()), + } +} + +#[tauri::command] +async fn toggle_pin_item(id: i64) -> Result<(), String> { + let socket_path = get_ipc_socket_path(); + let client = IpcClient::new(socket_path.to_string_lossy().to_string()); + + match client.send(IpcMessage::TogglePin { id }).await { + Ok(IpcMessage::Success) => Ok(()), + Ok(IpcMessage::Error { message }) => Err(format!("Daemon error: {}", message)), + Err(e) => Err(format!("IPC error: {}", e)), + _ => Err("Unexpected response from daemon".to_string()), + } +} + +#[tauri::command] +async fn toggle_favorite_item(id: i64) -> Result<(), String> { + let socket_path = get_ipc_socket_path(); + let client = IpcClient::new(socket_path.to_string_lossy().to_string()); + + match client.send(IpcMessage::ToggleFavorite { id }).await { + Ok(IpcMessage::Success) => Ok(()), + Ok(IpcMessage::Error { message }) => Err(format!("Daemon error: {}", message)), + Err(e) => Err(format!("IPC error: {}", e)), + _ => Err("Unexpected response from daemon".to_string()), + } +} + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + get_clipboard_history, + search_clipboard_items, + set_clipboard_content, + get_current_clipboard, + delete_clipboard_item, + toggle_pin_item, + toggle_favorite_item + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..4b50e9a --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,45 @@ +{ + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devPath": "http://localhost:5173", + "distDir": "../dist", + "withGlobalTauri": true + }, + "package": { + "productName": "OpenPaste", + "version": "0.1.0" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false, + "open": true + } + }, + "bundle": { + "active": false, + "targets": "all", + "identifier": "com.openpaste.desktop" + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fullscreen": false, + "height": 800, + "resizable": true, + "title": "OpenPaste", + "width": 1000, + "hiddenTitle": false, + "titleBarStyle": "Visible", + "decorations": false + } + ] + } +} diff --git a/apps/desktop/src/App.css b/apps/desktop/src/App.css new file mode 100644 index 0000000..e69de29 diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx new file mode 100644 index 0000000..edb7ce6 --- /dev/null +++ b/apps/desktop/src/App.tsx @@ -0,0 +1,625 @@ +import { useState, useEffect } from 'react' +import { invoke } from '@tauri-apps/api' +import { appWindow } from '@tauri-apps/api/window' +import { + Clipboard, + Search, + Pin, + Settings, + Menu, + Link, + FileText, + Image, + Terminal, + Copy, + Star, + Trash2, + Check, + Minimize, + Maximize2, + X +} from 'lucide-react' +import './App.css' + +interface ClipboardItem { + id: number + content_type: string + content: string + hash: string + created_at: string + accessed_at: string | null + pinned: boolean + favorite: boolean +} + +function App() { + const [items, setItems] = useState([]) + const [searchQuery, setSearchQuery] = useState('') + const [selectedItem, setSelectedItem] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + loadClipboardHistory(true) + + // Auto-refresh every 2 seconds + const interval = setInterval(() => { + loadClipboardHistory(false) + }, 2000) + + return () => clearInterval(interval) + }, []) + + const loadClipboardHistory = async (isInitial: boolean = false) => { + try { + if (isInitial) { + setLoading(true) + } + setError(null) + + const history = await invoke('get_clipboard_history') + + // Only update if data actually changed (compare by ID and hash) + const currentIds = items.map(item => `${item.id}-${item.hash}`).join(',') + const newIds = history.map(item => `${item.id}-${item.hash}`).join(',') + + if (currentIds !== newIds) { + setItems(history) + } + } catch (e) { + setError(e as string) + console.error('Failed to load clipboard history:', e) + } finally { + if (isInitial) { + setLoading(false) + } + } + } + + const copyToClipboard = async (content: string) => { + try { + await invoke('set_clipboard_content', { content }) + } catch (e) { + console.error('Failed to copy to clipboard:', e) + } + } + + const deleteItem = async (id: number) => { + try { + await invoke('delete_clipboard_item', { id }) + loadClipboardHistory() + } catch (e) { + console.error('Failed to delete item:', e) + } + } + + const togglePin = async (item: ClipboardItem) => { + try { + await invoke('toggle_pin_item', { id: Number(item.id) }) + loadClipboardHistory(false) + } catch (e) { + console.error('Failed to toggle pin:', e) + } + } + + const toggleFavorite = async (item: ClipboardItem) => { + try { + await invoke('toggle_favorite_item', { id: Number(item.id) }) + loadClipboardHistory(false) + } catch (e) { + console.error('Failed to toggle favorite:', e) + } + } + + const getIconForType = (contentType: string) => { + const type = contentType.toLowerCase() + if (type.includes('url') || type.includes('http')) return Link + if (type.includes('image')) return Image + if (type.includes('code') || type.includes('json')) return Terminal + return FileText + } + + const getTypeBadge = (contentType: string) => { + const type = contentType.toLowerCase() + if (type.includes('url') || type.includes('http')) return 'URL' + if (type.includes('image')) return 'IMAGE' + if (type.includes('code') || type.includes('json')) return 'CODE' + return 'TEXT' + } + + const getRelativeTime = (dateString: string) => { + const date = new Date(dateString) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffSecs = Math.floor(diffMs / 1000) + const diffMins = Math.floor(diffSecs / 60) + const diffHours = Math.floor(diffMins / 60) + const diffDays = Math.floor(diffHours / 24) + + if (diffSecs < 60) return `${diffSecs} sec ago` + if (diffMins < 60) return `${diffMins} min ago` + if (diffHours < 24) return `${diffHours} hr ago` + return `${diffDays} day${diffDays > 1 ? 's' : ''} ago` + } + + const filteredItems = items.filter(item => + item.content.toLowerCase().includes(searchQuery.toLowerCase()) + ) + + return ( +
+ {/* Title Bar */} +
+ {/* Window Controls */} +
+ + + +
+ + {/* Title */} +

+ OpenPaste +

+
+ + {/* Hero Header */} +
+
+ +
+

+ OpenPaste +

+

+ Clipboard history at your fingertips. +

+
+
+ + {/* Toolbar Icons */} +
+ + + + +
+
+ + {/* Search Section */} +
+
+ + setSearchQuery(e.target.value)} + style={{ + flex: 1, + marginLeft: '12px', + fontSize: '14px', + color: '#F3F5F8', + background: 'transparent', + border: 'none', + outline: 'none', + fontFamily: 'Inter, sans-serif' + }} + /> +
+ ⌘K +
+
+
+ + {/* Clipboard List */} +
+ {loading && ( +
+ Loading clipboard history... +
+ )} + + {error && ( +
+ Error: {error} +
+ )} + + {!loading && !error && ( +
+ {filteredItems.map((item) => { + const IconComponent = getIconForType(item.content_type) + return ( +
{ + e.currentTarget.style.transform = 'translateY(-1px)' + e.currentTarget.style.borderColor = '#5A6471' + e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.28)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)' + e.currentTarget.style.borderColor = '#343A44' + e.currentTarget.style.boxShadow = '0 4px 8px rgba(0,0,0,0.22)' + }} + onClick={() => setSelectedItem(item)} + > + {/* Left Icon Area */} +
+ +
+ + {/* Content Area */} +
+
+ {new Date(item.created_at).toLocaleString()} +
+
+ {item.content} +
+
+ {getTypeBadge(item.content_type)} +
+
+ + {/* Vertical Separator */} +
+ + {/* Right Side - Actions */} +
+
+ {getRelativeTime(item.created_at)} +
+
+ { + e.stopPropagation() + copyToClipboard(item.content) + }} + /> + { + e.stopPropagation() + togglePin(item) + }} + /> + { + e.stopPropagation() + toggleFavorite(item) + }} + /> + { + e.stopPropagation() + deleteItem(item.id) + }} + /> +
+
+
+ ) + })} + + {filteredItems.length === 0 && ( +
+ No clipboard items found +
+ )} +
+ )} +
+ + {/* Bottom Status Bar */} +
+
+ + {filteredItems.length} items + + + {items.filter(i => i.pinned).length} pinned + + + {items.filter(i => i.favorite).length} favorites + + + {items.filter(i => i.content_type.includes('image')).length} images + +
+
+ + + All synced + +
+
+ + {/* Selected Item Detail Modal */} + {selectedItem && ( +
+
+
+

+ Clipboard Item +

+ +
+
+
+ Type:{' '} + {selectedItem.content_type} +
+
+ Created:{' '} + {new Date(selectedItem.created_at).toLocaleString()} +
+
+ Content: +
+                  {selectedItem.content}
+                
+
+
+
+
+ )} +
+ ) +} + +export default App diff --git a/apps/desktop/src/assets/hero.png b/apps/desktop/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/apps/desktop/src/assets/react.svg b/apps/desktop/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/desktop/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/desktop/src/assets/vite.svg b/apps/desktop/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/apps/desktop/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css new file mode 100644 index 0000000..6662826 --- /dev/null +++ b/apps/desktop/src/index.css @@ -0,0 +1,18 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.title-bar { + -webkit-app-region: drag; +} + +.window-control { + -webkit-app-region: no-drag; +} + +body, html { + margin: 0; + padding: 0; + overflow: hidden; + background-color: #171A1F; +} diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/apps/desktop/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js new file mode 100644 index 0000000..dca8ba0 --- /dev/null +++ b/apps/desktop/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/apps/desktop/tsconfig.app.json b/apps/desktop/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/apps/desktop/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/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..8455dcb --- /dev/null +++ b/apps/desktop/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": 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..8b0f57b --- /dev/null +++ b/apps/desktop/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..4b2479f --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +# Clippy configuration + +# Deny warnings +warn-on-all-wildcard-imports = true diff --git a/crates/clipboard-ai/Cargo.toml b/crates/clipboard-ai/Cargo.toml new file mode 100644 index 0000000..ae5aec3 --- /dev/null +++ b/crates/clipboard-ai/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "clipboard-ai" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +reqwest = { workspace = true } +clipboard-core = { path = "../clipboard-core" } diff --git a/crates/clipboard-ai/src/ai.rs b/crates/clipboard-ai/src/ai.rs new file mode 100644 index 0000000..3599f75 --- /dev/null +++ b/crates/clipboard-ai/src/ai.rs @@ -0,0 +1,39 @@ +//! AI engine implementation + +use crate::AiError; + +/// AI engine for content analysis +pub struct AiEngine { + // TODO: Add AI provider configuration +} + +impl Default for AiEngine { + fn default() -> Self { + Self::new() + } +} + +impl AiEngine { + /// Create a new AI engine + pub fn new() -> Self { + Self {} + } + + /// Categorize content + pub async fn categorize(&self, _content: &str) -> Result { + // TODO: Implement content categorization + Err(AiError::ProviderNotConfigured) + } + + /// Generate smart suggestions + pub async fn suggestions(&self, _content: &str) -> Result, AiError> { + // TODO: Implement smart suggestions + Ok(Vec::new()) + } + + /// Summarize content + pub async fn summarize(&self, _content: &str) -> Result { + // TODO: Implement text summarization + Err(AiError::ProviderNotConfigured) + } +} diff --git a/crates/clipboard-ai/src/lib.rs b/crates/clipboard-ai/src/lib.rs new file mode 100644 index 0000000..f015583 --- /dev/null +++ b/crates/clipboard-ai/src/lib.rs @@ -0,0 +1,19 @@ +//! OpenPaste AI Module +//! +//! This module provides optional AI features for content categorization, smart search, and analysis. + +pub mod ai; + +pub use ai::AiEngine; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AiError { + #[error("AI request failed: {0}")] + RequestFailed(String), + #[error("Provider not configured")] + ProviderNotConfigured, + #[error("API key invalid")] + InvalidApiKey, +} diff --git a/crates/clipboard-api/Cargo.toml b/crates/clipboard-api/Cargo.toml new file mode 100644 index 0000000..ccc6728 --- /dev/null +++ b/crates/clipboard-api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "clipboard-api" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +axum = { workspace = true } +tower-http = { version = "0.5", features = ["cors", "trace"] } +clipboard-core = { path = "../clipboard-core" } +clipboard-db = { path = "../clipboard-db" } +clipboard-search = { path = "../clipboard-search" } diff --git a/crates/clipboard-api/src/api.rs b/crates/clipboard-api/src/api.rs new file mode 100644 index 0000000..f62c852 --- /dev/null +++ b/crates/clipboard-api/src/api.rs @@ -0,0 +1,77 @@ +//! REST API server implementation + +use crate::ApiError; +use axum::{ + routing::{get, post}, + Router, +}; +use std::net::SocketAddr; +use tokio::net::TcpListener; + +/// REST API server +pub struct ApiServer { + addr: SocketAddr, +} + +impl ApiServer { + /// Create a new API server + pub fn new(addr: SocketAddr) -> Self { + Self { addr } + } + + /// Build the router + fn router() -> Router { + Router::new() + .route("/api/v1/status", get(handlers::status)) + .route( + "/api/v1/clipboard", + get(handlers::get_clipboard).post(handlers::set_clipboard), + ) + .route("/api/v1/search", post(handlers::search)) + } + + /// Start the API server + pub async fn start(&self) -> Result<(), ApiError> { + let app = Self::router(); + let listener = TcpListener::bind(self.addr) + .await + .map_err(|e: std::io::Error| ApiError::RequestFailed(e.to_string()))?; + + axum::serve(listener, app) + .await + .map_err(|e: std::io::Error| ApiError::RequestFailed(e.to_string())) + } +} + +mod handlers { + use axum::{response::IntoResponse, Json}; + use serde_json::json; + + pub async fn status() -> impl IntoResponse { + Json(json!({ + "status": "ok", + "version": "0.1.0" + })) + } + + pub async fn get_clipboard() -> impl IntoResponse { + Json(json!({ + "success": false, + "error": "Not implemented" + })) + } + + pub async fn set_clipboard() -> impl IntoResponse { + Json(json!({ + "success": false, + "error": "Not implemented" + })) + } + + pub async fn search() -> impl IntoResponse { + Json(json!({ + "success": false, + "error": "Not implemented" + })) + } +} diff --git a/crates/clipboard-api/src/handlers.rs b/crates/clipboard-api/src/handlers.rs new file mode 100644 index 0000000..3cab35e --- /dev/null +++ b/crates/clipboard-api/src/handlers.rs @@ -0,0 +1,3 @@ +//! API request handlers + +// Handlers are defined in api.rs for simplicity diff --git a/crates/clipboard-api/src/lib.rs b/crates/clipboard-api/src/lib.rs new file mode 100644 index 0000000..d1bbe00 --- /dev/null +++ b/crates/clipboard-api/src/lib.rs @@ -0,0 +1,20 @@ +//! OpenPaste API Module +//! +//! This module provides the REST API for external integration. + +pub mod api; +pub mod handlers; + +pub use api::ApiServer; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ApiError { + #[error("Request failed: {0}")] + RequestFailed(String), + #[error("Authentication failed")] + AuthenticationFailed, + #[error("Unauthorized")] + Unauthorized, +} diff --git a/crates/clipboard-cli/Cargo.toml b/crates/clipboard-cli/Cargo.toml new file mode 100644 index 0000000..716249e --- /dev/null +++ b/crates/clipboard-cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "clipboard-cli" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +clap = { workspace = true } +clipboard-core = { path = "../clipboard-core" } diff --git a/crates/clipboard-cli/src/lib.rs b/crates/clipboard-cli/src/lib.rs new file mode 100644 index 0000000..831fa93 --- /dev/null +++ b/crates/clipboard-cli/src/lib.rs @@ -0,0 +1,3 @@ +//! OpenPaste CLI Library +//! +//! Shared CLI functionality. diff --git a/crates/clipboard-core/Cargo.toml b/crates/clipboard-core/Cargo.toml new file mode 100644 index 0000000..215508a --- /dev/null +++ b/crates/clipboard-core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "clipboard-core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } +md5 = "0.7" diff --git a/crates/clipboard-core/src/clipboard.rs b/crates/clipboard-core/src/clipboard.rs new file mode 100644 index 0000000..4a3f168 --- /dev/null +++ b/crates/clipboard-core/src/clipboard.rs @@ -0,0 +1,39 @@ +//! Clipboard management + +use crate::item::ClipboardItem; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ClipboardError { + #[error("Failed to access clipboard: {0}")] + AccessFailed(String), + #[error("Content type not supported")] + UnsupportedContentType, + #[error("Content too large: {0} bytes")] + ContentTooLarge(usize), +} + +/// Clipboard manager for capturing and managing clipboard content +pub struct ClipboardManager { + #[allow(dead_code)] + max_size: usize, +} + +impl ClipboardManager { + /// Create a new clipboard manager + pub fn new(max_size: usize) -> Self { + Self { max_size } + } + + /// Capture clipboard content + pub async fn capture(&self) -> Result { + // TODO: Implement platform-specific clipboard capture + Err(ClipboardError::AccessFailed("Not implemented".to_string())) + } + + /// Set clipboard content + pub async fn set(&self, _item: &ClipboardItem) -> Result<(), ClipboardError> { + // TODO: Implement platform-specific clipboard setting + Err(ClipboardError::AccessFailed("Not implemented".to_string())) + } +} diff --git a/crates/clipboard-core/src/item.rs b/crates/clipboard-core/src/item.rs new file mode 100644 index 0000000..fd2954e --- /dev/null +++ b/crates/clipboard-core/src/item.rs @@ -0,0 +1,55 @@ +//! Clipboard item representation + +use crate::ContentType; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// A clipboard item +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClipboardItem { + pub id: Uuid, + pub content_type: ContentType, + pub content: Vec, + pub hash: String, + pub created_at: DateTime, + pub accessed_at: Option>, + pub pinned: bool, + pub favorite: bool, +} + +impl Default for ClipboardItem { + fn default() -> Self { + Self { + id: Uuid::new_v4(), + content_type: ContentType::Text, + content: Vec::new(), + hash: String::new(), + created_at: Utc::now(), + accessed_at: None, + pinned: false, + favorite: false, + } + } +} + +impl ClipboardItem { + /// Create a new clipboard item + pub fn new(content_type: ContentType, content: Vec) -> Self { + let hash = Self::compute_hash(&content); + Self { + id: Uuid::new_v4(), + content_type, + content, + hash, + created_at: Utc::now(), + ..Default::default() + } + } + + /// Compute hash of content + fn compute_hash(content: &[u8]) -> String { + // TODO: Implement proper hashing (e.g., SHA-256) + format!("{:x}", md5::compute(content)) + } +} diff --git a/crates/clipboard-core/src/lib.rs b/crates/clipboard-core/src/lib.rs new file mode 100644 index 0000000..db130ea --- /dev/null +++ b/crates/clipboard-core/src/lib.rs @@ -0,0 +1,77 @@ +//! OpenPaste Core Module +//! +//! This module provides the core clipboard management functionality. + +pub mod clipboard; +pub mod item; + +pub use clipboard::ClipboardManager; +pub use item::ClipboardItem; + +/// Clipboard content types +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum ContentType { + Text, + Html, + Image, + File, + Binary, + Rtf, + Code, +} + +impl std::fmt::Display for ContentType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ContentType::Text => write!(f, "text"), + ContentType::Html => write!(f, "html"), + ContentType::Image => write!(f, "image"), + ContentType::File => write!(f, "file"), + ContentType::Binary => write!(f, "binary"), + ContentType::Rtf => write!(f, "rtf"), + ContentType::Code => write!(f, "code"), + } + } +} + +impl ContentType { + /// Detect content type from data + pub fn detect(data: &[u8]) -> Self { + if data.is_empty() { + return ContentType::Text; + } + + // Check for image signatures + if data.len() >= 8 { + let header = &data[..8]; + // PNG + if header.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) { + return ContentType::Image; + } + // JPEG + if header.starts_with(&[0xFF, 0xD8, 0xFF]) { + return ContentType::Image; + } + } + + // Check for HTML + let text = String::from_utf8_lossy(data); + if text.starts_with(""), ContentType::Html); + } +} diff --git a/crates/clipboard-daemon/Cargo.toml b/crates/clipboard-daemon/Cargo.toml new file mode 100644 index 0000000..efc6837 --- /dev/null +++ b/crates/clipboard-daemon/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "clipboard-daemon" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +name = "openpaste-daemon" +path = "src/main.rs" + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +clipboard-core = { path = "../clipboard-core" } +clipboard-db = { path = "../clipboard-db" } +clipboard-storage = { path = "../clipboard-storage" } +clipboard-encryption = { path = "../clipboard-encryption" } +clipboard-platform = { path = "../clipboard-platform" } +clipboard-events = { path = "../clipboard-events" } +clipboard-ipc = { path = "../clipboard-ipc" } +dirs = "5.0" diff --git a/crates/clipboard-daemon/src/main.rs b/crates/clipboard-daemon/src/main.rs new file mode 100644 index 0000000..97b6121 --- /dev/null +++ b/crates/clipboard-daemon/src/main.rs @@ -0,0 +1,267 @@ +//! OpenPaste daemon - Background service for clipboard management + +use anyhow::Result; +use clipboard_core::ClipboardItem; +use clipboard_db::Database; +use clipboard_encryption::Encryption; +use clipboard_events::{Event, EventBus}; +use clipboard_ipc::{IpcMessage, IpcServer}; +use clipboard_platform::{get_provider, ClipboardProvider}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::signal; +use tokio::sync::Mutex; +use tracing::{error, info}; +use tracing_subscriber::fmt; +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize logging + fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) + .init(); + + info!("Starting OpenPaste daemon"); + + // Initialize components + let data_dir = dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("openpaste"); + + std::fs::create_dir_all(&data_dir)?; + + let db_path = data_dir.join("clipboard.db"); + let storage_path = data_dir.join("storage"); + let ipc_socket_path = data_dir.join("openpaste.sock"); + + std::fs::create_dir_all(&storage_path)?; + + // Initialize database + let database = Arc::new( + Database::new(&db_path) + .await + .map_err(|e| anyhow::anyhow!("Failed to initialize database: {}", e))?, + ); + + // Initialize storage + let _storage = Arc::new(clipboard_storage::Storage::new(&storage_path)); + + // Initialize encryption (placeholder key - should come from user password) + let key = [0u8; 32]; // TODO: Get from user password + let _encryption = Arc::new(Encryption::new(key)); + + // Initialize event bus + let event_bus = Arc::new(EventBus::new(100)); + + // Initialize clipboard provider + let provider = Arc::new(Mutex::new(get_provider()?)); + + // Start IPC server + let ipc_server = IpcServer::new(ipc_socket_path.to_string_lossy().to_string()); + let ipc_handler = { + let database = database.clone(); + let event_bus = event_bus.clone(); + let provider = provider.clone(); + + move |message: IpcMessage| -> std::pin::Pin + Send>> { + match message { + IpcMessage::GetClipboard => { + let provider_clone = provider.clone(); + Box::pin(async move { + let provider = provider_clone.lock().await; + match provider.get_content().await { + Ok(item) => IpcMessage::ClipboardContent { content: item.content }, + Err(e) => IpcMessage::Error { message: e.to_string() }, + } + }) + } + IpcMessage::GetHistory => { + let database_clone = database.clone(); + Box::pin(async move { + match database_clone.list_items(100, 0).await { + Ok(items) => { + let history_items: Vec = items.into_iter().map(|item| { + clipboard_ipc::ClipboardHistoryItem { + id: item.id.to_string(), + content_type: item.content_type, + content: item.content, + hash: item.hash, + created_at: item.created_at.to_rfc3339(), + accessed_at: item.accessed_at.map(|t| t.to_rfc3339()), + pinned: item.pinned, + favorite: item.favorite, + } + }).collect(); + IpcMessage::ClipboardHistory { items: history_items } + } + Err(e) => IpcMessage::Error { message: e.to_string() }, + } + }) + } + IpcMessage::SearchItems { query } => { + let database_clone = database.clone(); + Box::pin(async move { + match database_clone.search_items(&query, 100, 0).await { + Ok(items) => { + let history_items: Vec = items.into_iter().map(|item| { + clipboard_ipc::ClipboardHistoryItem { + id: item.id.to_string(), + content_type: item.content_type, + content: item.content, + hash: item.hash, + created_at: item.created_at.to_rfc3339(), + accessed_at: item.accessed_at.map(|t| t.to_rfc3339()), + pinned: item.pinned, + favorite: item.favorite, + } + }).collect(); + IpcMessage::ClipboardHistory { items: history_items } + } + Err(e) => IpcMessage::Error { message: e.to_string() }, + } + }) + } + IpcMessage::SetClipboard { content } => { + let provider_clone = provider.clone(); + let database_clone = database.clone(); + let event_bus_clone = event_bus.clone(); + let item = ClipboardItem::new(clipboard_core::ContentType::Text, content.clone()); + Box::pin(async move { + let provider = provider_clone.lock().await; + match provider.set_content(&item).await { + Ok(_) => { + // Convert to DB model and save + let db_item = clipboard_db::models::ClipboardItem { + id: 0, // Will be auto-assigned + content_type: item.content_type.to_string(), + content: item.content.clone(), + hash: item.hash, + created_at: item.created_at, + accessed_at: item.accessed_at, + pinned: item.pinned, + favorite: item.favorite, + }; + let _ = database_clone.insert_item(&db_item).await; + + // Publish event + let event = Event::ClipboardAdded { id: item.id }; + let _ = event_bus_clone.publish(event); + + IpcMessage::ClipboardContent { content } + } + Err(e) => IpcMessage::Error { message: e.to_string() }, + } + }) + } + IpcMessage::DeleteItem { id } => { + let database_clone = database.clone(); + Box::pin(async move { + match database_clone.delete_item(id).await { + Ok(_) => IpcMessage::Success, + Err(e) => IpcMessage::Error { message: e.to_string() }, + } + }) + } + IpcMessage::TogglePin { id } => { + let database_clone = database.clone(); + Box::pin(async move { + match database_clone.toggle_pin(id).await { + Ok(_) => IpcMessage::Success, + Err(e) => IpcMessage::Error { message: e.to_string() }, + } + }) + } + IpcMessage::ToggleFavorite { id } => { + let database_clone = database.clone(); + Box::pin(async move { + match database_clone.toggle_favorite(id).await { + Ok(_) => IpcMessage::Success, + Err(e) => IpcMessage::Error { message: e.to_string() }, + } + }) + } + _ => Box::pin(async move { IpcMessage::Error { message: "Invalid message".to_string() } }), + } + } + }; + + tokio::spawn(async move { + if let Err(e) = ipc_server.start(ipc_handler).await { + error!("IPC server error: {}", e); + } + }); + + // Start clipboard watching with polling + let provider_watch = provider.clone(); + let event_bus_watch = event_bus.clone(); + let database_watch = database.clone(); + tokio::spawn(async move { + let mut last_hash = String::new(); + let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(500)); + + loop { + interval.tick().await; + + let provider = provider_watch.lock().await; + match provider.get_content().await { + Ok(item) => { + if item.hash != last_hash { + last_hash = item.hash.clone(); + info!("Clipboard changed: hash={}", item.hash); + + // Publish clipboard change event + let event = Event::ClipboardAdded { id: item.id }; + let _ = event_bus_watch.publish(event); + + // Save to database + let db_item = clipboard_db::models::ClipboardItem { + id: 0, // Will be auto-assigned + content_type: item.content_type.to_string(), + content: item.content.clone(), + hash: item.hash, + created_at: item.created_at, + accessed_at: item.accessed_at, + pinned: item.pinned, + favorite: item.favorite, + }; + let _ = database_watch.insert_item(&db_item).await; + } + } + Err(e) => { + error!("Failed to get clipboard content: {}", e); + } + } + } + }); + + // Setup graceful shutdown + let shutdown_signal = async { + signal::ctrl_c() + .await + .expect("Failed to install CTRL+C handler"); + info!("Received shutdown signal"); + }; + + #[cfg(unix)] + { + use signal::unix::{signal, SignalKind}; + + let mut sigterm = signal(SignalKind::terminate())?; + tokio::select! { + _ = shutdown_signal => {}, + _ = sigterm.recv() => { + info!("Received SIGTERM"); + }, + } + } + + #[cfg(not(unix))] + { + shutdown_signal.await; + } + + info!("Shutting down OpenPaste daemon"); + + Ok(()) +} diff --git a/crates/clipboard-db/Cargo.toml b/crates/clipboard-db/Cargo.toml new file mode 100644 index 0000000..35e80ed --- /dev/null +++ b/crates/clipboard-db/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "clipboard-db" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +sqlx = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } +clipboard-core = { path = "../clipboard-core" } diff --git a/crates/clipboard-db/migrations/001_initial.sql b/crates/clipboard-db/migrations/001_initial.sql new file mode 100644 index 0000000..c77efba --- /dev/null +++ b/crates/clipboard-db/migrations/001_initial.sql @@ -0,0 +1,100 @@ +-- Initial database schema for OpenPaste + +-- Clipboard items table +CREATE TABLE IF NOT EXISTS clipboard_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content_type TEXT NOT NULL, + content BLOB NOT NULL, + hash TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL, + accessed_at TEXT, + pinned INTEGER NOT NULL DEFAULT 0, + favorite INTEGER NOT NULL DEFAULT 0 +); + +-- FTS5 virtual table for full-text search +CREATE VIRTUAL TABLE IF NOT EXISTS clipboard_items_fts USING fts5( + content, + content_type, + tokenize='porter unicode61' +); + +-- Triggers to keep FTS5 table in sync with main table +CREATE TRIGGER IF NOT EXISTS clipboard_items_fts_insert AFTER INSERT ON clipboard_items BEGIN + INSERT INTO clipboard_items_fts(rowid, content, content_type) + VALUES (NEW.id, NEW.content, NEW.content_type); +END; + +CREATE TRIGGER IF NOT EXISTS clipboard_items_fts_delete AFTER DELETE ON clipboard_items BEGIN + DELETE FROM clipboard_items_fts WHERE rowid = OLD.id; +END; + +CREATE TRIGGER IF NOT EXISTS clipboard_items_fts_update AFTER UPDATE ON clipboard_items BEGIN + UPDATE clipboard_items_fts SET content = NEW.content, content_type = NEW.content_type + WHERE rowid = NEW.id; +END; + +-- Collections table +CREATE TABLE IF NOT EXISTS collections ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- Tags table +CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + color TEXT +); + +-- Clipboard item tags junction table +CREATE TABLE IF NOT EXISTS clipboard_item_tags ( + clipboard_item_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (clipboard_item_id, tag_id), + FOREIGN KEY (clipboard_item_id) REFERENCES clipboard_items(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +-- Settings table +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- Sync state table +CREATE TABLE IF NOT EXISTS sync_state ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- Plugins table +CREATE TABLE IF NOT EXISTS plugins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + version TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 0, + installed_at TEXT NOT NULL, + config TEXT +); + +-- Audit log table +CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + action TEXT NOT NULL, + entity_type TEXT NOT NULL, + entity_id TEXT, + details TEXT, + created_at TEXT NOT NULL +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_clipboard_items_created_at ON clipboard_items(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_clipboard_items_hash ON clipboard_items(hash); +CREATE INDEX IF NOT EXISTS idx_clipboard_items_pinned ON clipboard_items(pinned); +CREATE INDEX IF NOT EXISTS idx_clipboard_items_favorite ON clipboard_items(favorite); diff --git a/crates/clipboard-db/src/database.rs b/crates/clipboard-db/src/database.rs new file mode 100644 index 0000000..05c1b25 --- /dev/null +++ b/crates/clipboard-db/src/database.rs @@ -0,0 +1,163 @@ +//! Database connection and operations + +use crate::{models::ClipboardItem, DatabaseError}; +use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; +use std::path::Path; + +/// Database connection pool +pub struct Database { + #[allow(dead_code)] + pool: SqlitePool, +} + +impl Database { + /// Create a new database connection + pub async fn new(path: &Path) -> Result { + let db_url = format!("sqlite:{}?mode=rwc", path.display()); + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect(&db_url) + .await + .map_err(|e| DatabaseError::ConnectionFailed(e.to_string()))?; + + // Run migrations + sqlx::migrate!("./migrations") + .run(&pool) + .await + .map_err(|e| DatabaseError::MigrationFailed(e.to_string()))?; + + Ok(Self { pool }) + } + + /// Create an in-memory database for testing + pub async fn in_memory() -> Result { + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect("sqlite::memory:") + .await + .map_err(|e| DatabaseError::ConnectionFailed(e.to_string()))?; + + // Run migrations + sqlx::migrate!("./migrations") + .run(&pool) + .await + .map_err(|e| DatabaseError::MigrationFailed(e.to_string()))?; + + Ok(Self { pool }) + } + + /// Insert a clipboard item + pub async fn insert_item(&self, item: &ClipboardItem) -> Result { + let result = sqlx::query( + r#" + INSERT INTO clipboard_items (content_type, content, hash, created_at, accessed_at, pinned, favorite) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + "#, + ) + .bind(&item.content_type) + .bind(&item.content) + .bind(&item.hash) + .bind(item.created_at) + .bind(item.accessed_at) + .bind(item.pinned) + .bind(item.favorite) + .execute(&self.pool) + .await + .map_err(|e| DatabaseError::QueryFailed(e.to_string()))?; + + Ok(result.last_insert_rowid()) + } + + /// Get a clipboard item by ID + pub async fn get_item(&self, id: i64) -> Result { + let item = sqlx::query_as::<_, ClipboardItem>( + "SELECT id, content_type, content, hash, created_at, accessed_at, pinned, favorite FROM clipboard_items WHERE id = ?" + ) + .bind(id) + .fetch_optional(&self.pool) + .await + .map_err(|e| DatabaseError::QueryFailed(e.to_string()))? + .ok_or_else(|| DatabaseError::NotFound(id.to_string()))?; + + Ok(item) + } + + /// List clipboard items + pub async fn list_items( + &self, + limit: usize, + offset: usize, + ) -> Result, DatabaseError> { + let items = sqlx::query_as::<_, ClipboardItem>( + "SELECT id, content_type, content, hash, created_at, accessed_at, pinned, favorite FROM clipboard_items ORDER BY created_at DESC LIMIT ? OFFSET ?" + ) + .bind(limit as i64) + .bind(offset as i64) + .fetch_all(&self.pool) + .await + .map_err(|e| DatabaseError::QueryFailed(e.to_string()))?; + + Ok(items) + } + + /// Delete a clipboard item + pub async fn delete_item(&self, id: i64) -> Result<(), DatabaseError> { + sqlx::query("DELETE FROM clipboard_items WHERE id = ?") + .bind(id) + .execute(&self.pool) + .await + .map_err(|e| DatabaseError::QueryFailed(e.to_string()))?; + + Ok(()) + } + + /// Toggle pin status of a clipboard item + pub async fn toggle_pin(&self, id: i64) -> Result { + let result = sqlx::query("UPDATE clipboard_items SET pinned = NOT pinned WHERE id = ?") + .bind(id) + .execute(&self.pool) + .await + .map_err(|e| DatabaseError::QueryFailed(e.to_string()))?; + + Ok(result.rows_affected() > 0) + } + + /// Toggle favorite status of a clipboard item + pub async fn toggle_favorite(&self, id: i64) -> Result { + let result = sqlx::query("UPDATE clipboard_items SET favorite = NOT favorite WHERE id = ?") + .bind(id) + .execute(&self.pool) + .await + .map_err(|e| DatabaseError::QueryFailed(e.to_string()))?; + + Ok(result.rows_affected() > 0) + } + + /// Search clipboard items using FTS5 + pub async fn search_items( + &self, + query: &str, + limit: usize, + offset: usize, + ) -> Result, DatabaseError> { + let items = sqlx::query_as::<_, ClipboardItem>( + r#" + SELECT ci.id, ci.content_type, ci.content, ci.hash, ci.created_at, ci.accessed_at, ci.pinned, ci.favorite + FROM clipboard_items ci + INNER JOIN clipboard_items_fts fts ON ci.id = fts.rowid + WHERE clipboard_items_fts MATCH ? + ORDER BY ci.created_at DESC + LIMIT ? OFFSET ? + "# + ) + .bind(query) + .bind(limit as i64) + .bind(offset as i64) + .fetch_all(&self.pool) + .await + .map_err(|e| DatabaseError::QueryFailed(e.to_string()))?; + + Ok(items) + } +} diff --git a/crates/clipboard-db/src/lib.rs b/crates/clipboard-db/src/lib.rs new file mode 100644 index 0000000..4f06339 --- /dev/null +++ b/crates/clipboard-db/src/lib.rs @@ -0,0 +1,24 @@ +//! OpenPaste Database Module +//! +//! This module provides database functionality using SQLite. + +pub mod database; +pub mod models; +pub mod schema; + +pub use database::Database; +pub use models::*; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("Database connection failed: {0}")] + ConnectionFailed(String), + #[error("Query failed: {0}")] + QueryFailed(String), + #[error("Migration failed: {0}")] + MigrationFailed(String), + #[error("Item not found: {0}")] + NotFound(String), +} diff --git a/crates/clipboard-db/src/models.rs b/crates/clipboard-db/src/models.rs new file mode 100644 index 0000000..75c18cd --- /dev/null +++ b/crates/clipboard-db/src/models.rs @@ -0,0 +1,18 @@ +//! Database models + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +/// Clipboard item database model +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct ClipboardItem { + pub id: i64, + pub content_type: String, + pub content: Vec, + pub hash: String, + pub created_at: DateTime, + pub accessed_at: Option>, + pub pinned: bool, + pub favorite: bool, +} diff --git a/crates/clipboard-db/src/schema.rs b/crates/clipboard-db/src/schema.rs new file mode 100644 index 0000000..8ab4085 --- /dev/null +++ b/crates/clipboard-db/src/schema.rs @@ -0,0 +1,3 @@ +//! Database schema definitions + +// Schema is managed by SQL migrations in the migrations/ directory diff --git a/crates/clipboard-encryption/Cargo.toml b/crates/clipboard-encryption/Cargo.toml new file mode 100644 index 0000000..d3c06d3 --- /dev/null +++ b/crates/clipboard-encryption/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "clipboard-encryption" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +aes-gcm = { workspace = true } +argon2 = { workspace = true } +sha2 = "0.10" diff --git a/crates/clipboard-encryption/src/encryption.rs b/crates/clipboard-encryption/src/encryption.rs new file mode 100644 index 0000000..15e6bfd --- /dev/null +++ b/crates/clipboard-encryption/src/encryption.rs @@ -0,0 +1,53 @@ +//! Encryption implementation using AES-256-GCM + +use crate::EncryptionError; +use aes_gcm::aead::{AeadCore, OsRng}; +use aes_gcm::{Aes256Gcm, Nonce}; + +/// Encrypted data with nonce +#[derive(Debug, Clone)] +pub struct EncryptedData { + pub nonce: Vec, + pub ciphertext: Vec, +} + +/// Encryption manager +pub struct Encryption { + key: [u8; 32], +} + +impl Encryption { + /// Create a new encryption manager with a 32-byte key + pub fn new(key: [u8; 32]) -> Self { + Self { key } + } + + /// Encrypt data and return nonce + ciphertext + pub fn encrypt(&self, plaintext: &[u8]) -> Result { + use aes_gcm::aead::{Aead, KeyInit}; + let key = as From<[u8; 32]>>::from(self.key); + let cipher = Aes256Gcm::new(&key); + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + + let ciphertext = cipher + .encrypt(&nonce, plaintext) + .map_err(|e: aes_gcm::aead::Error| EncryptionError::EncryptionFailed(e.to_string()))?; + + Ok(EncryptedData { + nonce: nonce.to_vec(), + ciphertext, + }) + } + + /// Decrypt data using provided nonce + pub fn decrypt(&self, encrypted: &EncryptedData) -> Result, EncryptionError> { + use aes_gcm::aead::{Aead, KeyInit}; + let key = as From<[u8; 32]>>::from(self.key); + let cipher = Aes256Gcm::new(&key); + let nonce = Nonce::from_slice(&encrypted.nonce); + + cipher + .decrypt(nonce, encrypted.ciphertext.as_ref()) + .map_err(|e: aes_gcm::aead::Error| EncryptionError::DecryptionFailed(e.to_string())) + } +} diff --git a/crates/clipboard-encryption/src/key_derivation.rs b/crates/clipboard-encryption/src/key_derivation.rs new file mode 100644 index 0000000..0ec176b --- /dev/null +++ b/crates/clipboard-encryption/src/key_derivation.rs @@ -0,0 +1,81 @@ +//! Key derivation using Argon2id + +use crate::EncryptionError; +use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use argon2::Algorithm; +use argon2::Argon2; +use argon2::Params; +use argon2::Version; +use sha2::Digest; + +/// Key derivation using Argon2id +pub struct KeyDerivation { + argon2: Argon2<'static>, +} + +impl Default for KeyDerivation { + fn default() -> Self { + Self::new() + } +} + +impl KeyDerivation { + /// Create a new key derivation instance with secure defaults + pub fn new() -> Self { + // Use Argon2id with secure parameters + let params = Params::new(65536, 3, 4, None).expect("Invalid params"); + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + Self { argon2 } + } + + /// Generate a random salt + pub fn generate_salt() -> String { + let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng); + salt.as_str().to_string() + } + + /// Derive a key from password using Argon2id + pub fn derive_key(&self, password: &str, salt: &str) -> Result<[u8; 32], EncryptionError> { + let salt = SaltString::from_b64(salt) + .map_err(|e| EncryptionError::KeyDerivationFailed(e.to_string()))?; + + let password_hash = self + .argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| EncryptionError::KeyDerivationFailed(e.to_string()))?; + + // Extract the hash bytes and use SHA-256 to derive a 32-byte key + let hash_str = password_hash.to_string(); + let hash_bytes = hash_str.as_bytes(); + let mut key = [0u8; 32]; + + // Use SHA-256 to derive a 32-byte key from the hash string + let digest = sha2::Sha256::digest(hash_bytes); + key.copy_from_slice(&digest); + + Ok(key) + } + + /// Hash a password for storage + pub fn hash_password(&self, password: &str) -> Result { + let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng); + let password_hash = self + .argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| EncryptionError::KeyDerivationFailed(e.to_string()))?; + + Ok(password_hash.to_string()) + } + + /// Verify password against hash + pub fn verify(&self, password: &str, hash: &str) -> Result { + let parsed_hash = PasswordHash::new(hash) + .map_err(|e| EncryptionError::KeyDerivationFailed(e.to_string()))?; + + self.argon2 + .verify_password(password.as_bytes(), &parsed_hash) + .map(|_| true) + .map_err(|_| EncryptionError::InvalidPassword) + } +} diff --git a/crates/clipboard-encryption/src/lib.rs b/crates/clipboard-encryption/src/lib.rs new file mode 100644 index 0000000..7bc76ec --- /dev/null +++ b/crates/clipboard-encryption/src/lib.rs @@ -0,0 +1,23 @@ +//! OpenPaste Encryption Module +//! +//! This module provides encryption functionality using AES-256-GCM and Argon2id. + +pub mod encryption; +pub mod key_derivation; + +pub use encryption::{EncryptedData, Encryption}; +pub use key_derivation::KeyDerivation; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum EncryptionError { + #[error("Encryption failed: {0}")] + EncryptionFailed(String), + #[error("Decryption failed: {0}")] + DecryptionFailed(String), + #[error("Key derivation failed: {0}")] + KeyDerivationFailed(String), + #[error("Invalid password")] + InvalidPassword, +} diff --git a/crates/clipboard-events/Cargo.toml b/crates/clipboard-events/Cargo.toml new file mode 100644 index 0000000..93876c3 --- /dev/null +++ b/crates/clipboard-events/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "clipboard-events" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } diff --git a/crates/clipboard-events/src/bus.rs b/crates/clipboard-events/src/bus.rs new file mode 100644 index 0000000..c0a7c06 --- /dev/null +++ b/crates/clipboard-events/src/bus.rs @@ -0,0 +1,30 @@ +//! Event bus implementation + +use crate::{Event, EventError}; +use tokio::sync::broadcast; + +/// Event bus for publish-subscribe communication +pub struct EventBus { + sender: broadcast::Sender, +} + +impl EventBus { + /// Create a new event bus + pub fn new(capacity: usize) -> Self { + let (sender, _) = broadcast::channel(capacity); + Self { sender } + } + + /// Publish an event + pub fn publish(&self, event: Event) -> Result<(), EventError> { + self.sender + .send(event) + .map(|_| ()) + .map_err(|e| EventError::PublishFailed(e.to_string())) + } + + /// Subscribe to events + pub fn subscribe(&self) -> broadcast::Receiver { + self.sender.subscribe() + } +} diff --git a/crates/clipboard-events/src/event.rs b/crates/clipboard-events/src/event.rs new file mode 100644 index 0000000..f67e235 --- /dev/null +++ b/crates/clipboard-events/src/event.rs @@ -0,0 +1,77 @@ +//! Event types + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Event types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Event { + /// Clipboard events + ClipboardAdded { + id: Uuid, + }, + ClipboardChanged { + id: Uuid, + }, + ClipboardAccessed { + id: Uuid, + }, + ClipboardPinned { + id: Uuid, + }, + ClipboardDeleted { + id: Uuid, + }, + + /// Search events + SearchPerformed { + query: String, + }, + SearchResultSelected { + id: Uuid, + }, + + /// Storage events + StorageThresholdReached { + size: usize, + }, + ItemRetentionTriggered { + id: Uuid, + }, + + /// Encryption events + EncryptionStateChanged { + locked: bool, + }, + VaultLocked, + VaultUnlocked, + + /// Sync events + SyncStarted, + SyncCompleted, + ConflictDetected { + id: Uuid, + }, + + /// Plugin events + PluginLoaded { + name: String, + }, + PluginUnloaded { + name: String, + }, + PluginError { + name: String, + error: String, + }, + + /// System events + DaemonStarted, + DaemonShutdown, + ClientConnected { + id: Uuid, + }, + ClientDisconnected { + id: Uuid, + }, +} diff --git a/crates/clipboard-events/src/lib.rs b/crates/clipboard-events/src/lib.rs new file mode 100644 index 0000000..b2b55b0 --- /dev/null +++ b/crates/clipboard-events/src/lib.rs @@ -0,0 +1,19 @@ +//! OpenPaste Events Module +//! +//! This module provides an event bus for internal communication using publish-subscribe pattern. + +pub mod bus; +pub mod event; + +pub use bus::EventBus; +pub use event::Event; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum EventError { + #[error("Event publish failed: {0}")] + PublishFailed(String), + #[error("Event subscribe failed: {0}")] + SubscribeFailed(String), +} diff --git a/crates/clipboard-ipc/Cargo.toml b/crates/clipboard-ipc/Cargo.toml new file mode 100644 index 0000000..b880b6f --- /dev/null +++ b/crates/clipboard-ipc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "clipboard-ipc" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } + +[target.'cfg(unix)'.dependencies] +tokio = { workspace = true } + +[target.'cfg(windows)'.dependencies] +tokio = { workspace = true } diff --git a/crates/clipboard-ipc/src/error.rs b/crates/clipboard-ipc/src/error.rs new file mode 100644 index 0000000..38d68d5 --- /dev/null +++ b/crates/clipboard-ipc/src/error.rs @@ -0,0 +1,24 @@ +//! IPC error types + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum IpcError { + #[error("Connection failed: {0}")] + ConnectionFailed(String), + + #[error("Send failed: {0}")] + SendFailed(String), + + #[error("Receive failed: {0}")] + ReceiveFailed(String), + + #[error("Serialization failed: {0}")] + SerializationFailed(String), + + #[error("Deserialization failed: {0}")] + DeserializationFailed(String), + + #[error("Invalid message")] + InvalidMessage, +} diff --git a/crates/clipboard-ipc/src/ipc.rs b/crates/clipboard-ipc/src/ipc.rs new file mode 100644 index 0000000..c2a632f --- /dev/null +++ b/crates/clipboard-ipc/src/ipc.rs @@ -0,0 +1,270 @@ +//! IPC server and client implementation + +use crate::IpcError; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + +/// IPC message types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IpcMessage { + /// Request clipboard content + GetClipboard, + /// Request clipboard history + GetHistory, + /// Search clipboard items + SearchItems { query: String }, + /// Set clipboard content + SetClipboard { content: Vec }, + /// Delete clipboard item + DeleteItem { id: i64 }, + /// Toggle pin status + TogglePin { id: i64 }, + /// Toggle favorite status + ToggleFavorite { id: i64 }, + /// Clipboard content response + ClipboardContent { content: Vec }, + /// Clipboard history response + ClipboardHistory { items: Vec }, + /// Success response + Success, + /// Error response + Error { message: String }, +} + +/// Clipboard history item for IPC +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClipboardHistoryItem { + pub id: String, + pub content_type: String, + pub content: Vec, + pub hash: String, + pub created_at: String, + pub accessed_at: Option, + pub pinned: bool, + pub favorite: bool, +} + +/// IPC server +pub struct IpcServer { + path: String, +} + +impl IpcServer { + /// Create a new IPC server + pub fn new(path: String) -> Self { + Self { path } + } + + /// Start the IPC server + pub async fn start(&self, handler: F) -> Result<(), IpcError> + where + F: Fn(IpcMessage) -> Fut + Send + Sync + Clone + 'static, + Fut: std::future::Future + Send + 'static, + { + #[cfg(unix)] + { + use tokio::net::UnixListener; + + // Remove existing socket if present + let _ = std::fs::remove_file(&self.path); + + let listener = UnixListener::bind(&self.path) + .map_err(|e| IpcError::ConnectionFailed(e.to_string()))?; + + tracing::info!("IPC server listening on {}", self.path); + + loop { + match listener.accept().await { + Ok((stream, _)) => { + let handler = handler.clone(); + tokio::spawn(async move { + if let Err(e) = Self::handle_connection(stream, handler).await { + tracing::error!("Connection error: {}", e); + } + }); + } + Err(e) => { + tracing::error!("Accept error: {}", e); + } + } + } + } + + #[cfg(windows)] + { + use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions}; + + let server = ServerOptions::new() + .create(&self.path) + .map_err(|e| IpcError::ConnectionFailed(e.to_string()))?; + + tracing::info!("IPC server listening on {}", self.path); + + loop { + let mut server = server + .connect() + .await + .map_err(|e| IpcError::ConnectionFailed(e.to_string()))?; + + let handler = handler.clone(); + tokio::spawn(async move { + if let Err(e) = Self::handle_connection_windows(&mut server, handler).await { + tracing::error!("Connection error: {}", e); + } + }); + + // Recreate server for next connection + let server = ServerOptions::new() + .create(&self.path) + .map_err(|e| IpcError::ConnectionFailed(e.to_string()))?; + } + } + } + + #[cfg(unix)] + async fn handle_connection( + mut stream: tokio::net::UnixStream, + handler: F, + ) -> Result<(), IpcError> + where + F: Fn(IpcMessage) -> Fut + Clone, + Fut: std::future::Future, + { + let (reader, mut writer) = stream.split(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + reader + .read_line(&mut line) + .await + .map_err(|e| IpcError::ReceiveFailed(e.to_string()))?; + + let request: IpcMessage = serde_json::from_str(&line) + .map_err(|e| IpcError::DeserializationFailed(e.to_string()))?; + + let response = handler(request).await; + let response_json = serde_json::to_string(&response) + .map_err(|e| IpcError::SerializationFailed(e.to_string()))?; + + writer + .write_all(response_json.as_bytes()) + .await + .map_err(|e| IpcError::SendFailed(e.to_string()))?; + + writer + .write_all(b"\n") + .await + .map_err(|e| IpcError::SendFailed(e.to_string()))?; + + Ok(()) + } + + #[cfg(windows)] + async fn handle_connection_windows( + server: &mut tokio::net::windows::named_pipe::NamedPipeServer, + handler: F, + ) -> Result<(), IpcError> + where + F: Fn(IpcMessage) -> Fut + Clone, + Fut: std::future::Future, + { + let mut buf = vec![0u8; 4096]; + let n = server + .read(&mut buf) + .await + .map_err(|e| IpcError::ReceiveFailed(e.to_string()))?; + + let request: IpcMessage = serde_json::from_slice(&buf[..n]) + .map_err(|e| IpcError::DeserializationFailed(e.to_string()))?; + + let response = handler(request).await; + let response_json = serde_json::to_vec(&response) + .map_err(|e| IpcError::SerializationFailed(e.to_string()))?; + + server + .write_all(&response_json) + .await + .map_err(|e| IpcError::SendFailed(e.to_string()))?; + + Ok(()) + } +} + +/// IPC client +pub struct IpcClient { + path: String, +} + +impl IpcClient { + /// Create a new IPC client + pub fn new(path: String) -> Self { + Self { path } + } + + /// Send a message and wait for response + pub async fn send(&self, message: IpcMessage) -> Result { + #[cfg(unix)] + { + use tokio::net::UnixStream; + + let mut stream = UnixStream::connect(&self.path) + .await + .map_err(|e| IpcError::ConnectionFailed(e.to_string()))?; + + let message_json = serde_json::to_string(&message) + .map_err(|e| IpcError::SerializationFailed(e.to_string()))?; + + stream + .write_all(message_json.as_bytes()) + .await + .map_err(|e| IpcError::SendFailed(e.to_string()))?; + + stream + .write_all(b"\n") + .await + .map_err(|e| IpcError::SendFailed(e.to_string()))?; + + let (reader, _) = stream.split(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + reader + .read_line(&mut line) + .await + .map_err(|e| IpcError::ReceiveFailed(e.to_string()))?; + + let response: IpcMessage = serde_json::from_str(&line) + .map_err(|e| IpcError::DeserializationFailed(e.to_string()))?; + + Ok(response) + } + + #[cfg(windows)] + { + use tokio::net::windows::named_pipe::ClientOptions; + + let mut client = ClientOptions::new() + .open(&self.path) + .map_err(|e| IpcError::ConnectionFailed(e.to_string()))?; + + let message_json = serde_json::to_vec(&message) + .map_err(|e| IpcError::SerializationFailed(e.to_string()))?; + + client + .write_all(&message_json) + .await + .map_err(|e| IpcError::SendFailed(e.to_string()))?; + + let mut buf = vec![0u8; 4096]; + let n = client + .read(&mut buf) + .await + .map_err(|e| IpcError::ReceiveFailed(e.to_string()))?; + + let response: IpcMessage = serde_json::from_slice(&buf[..n]) + .map_err(|e| IpcError::DeserializationFailed(e.to_string()))?; + + Ok(response) + } + } +} diff --git a/crates/clipboard-ipc/src/lib.rs b/crates/clipboard-ipc/src/lib.rs new file mode 100644 index 0000000..9ccda1f --- /dev/null +++ b/crates/clipboard-ipc/src/lib.rs @@ -0,0 +1,7 @@ +//! IPC (Inter-Process Communication) module + +pub mod error; +pub mod ipc; + +pub use error::IpcError; +pub use ipc::{ClipboardHistoryItem, IpcClient, IpcMessage, IpcServer}; diff --git a/crates/clipboard-platform/Cargo.toml b/crates/clipboard-platform/Cargo.toml new file mode 100644 index 0000000..820c599 --- /dev/null +++ b/crates/clipboard-platform/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "clipboard-platform" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +clipboard-core = { path = "../clipboard-core" } +async-trait = "0.1" +arboard = "3.3" + +[target.'cfg(windows)'.dependencies] +clipboard-win = "4.5" + +[target.'cfg(target_os = "linux")'.dependencies] +wl-clipboard-rs = "0.7" +x11-clipboard = "0.8" + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.25" +objc = "0.2" diff --git a/crates/clipboard-platform/src/lib.rs b/crates/clipboard-platform/src/lib.rs new file mode 100644 index 0000000..dbd207f --- /dev/null +++ b/crates/clipboard-platform/src/lib.rs @@ -0,0 +1,19 @@ +//! OpenPaste Platform Module +//! +//! This module provides platform-specific clipboard integration for Windows, Linux, and macOS. + +pub mod provider; + +pub use provider::{get_provider, ClipboardProvider, PlatformProvider}; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PlatformError { + #[error("Platform not supported")] + UnsupportedPlatform, + #[error("Clipboard access failed: {0}")] + AccessFailed(String), + #[error("Clipboard watch failed: {0}")] + WatchFailed(String), +} diff --git a/crates/clipboard-platform/src/provider.rs b/crates/clipboard-platform/src/provider.rs new file mode 100644 index 0000000..03a6c33 --- /dev/null +++ b/crates/clipboard-platform/src/provider.rs @@ -0,0 +1,247 @@ +//! Platform-specific clipboard provider + +use crate::PlatformError; +use async_trait::async_trait; +use clipboard_core::ClipboardItem; + +/// Platform-agnostic clipboard provider trait +#[async_trait] +pub trait ClipboardProvider: Send + Sync { + /// Get clipboard content + async fn get_content(&self) -> Result; + + /// Set clipboard content + async fn set_content(&self, item: &ClipboardItem) -> Result<(), PlatformError>; + + /// Watch for clipboard changes + async fn watch_changes(&self) -> Result<(), PlatformError>; +} + +/// Platform-specific provider enum +#[derive(Clone)] +pub enum PlatformProvider { + #[cfg(windows)] + Windows(windows::WindowsProvider), + #[cfg(target_os = "linux")] + Linux(linux::LinuxProvider), + #[cfg(target_os = "macos")] + Macos(macos::MacosProvider), +} + +#[async_trait] +impl ClipboardProvider for PlatformProvider { + async fn get_content(&self) -> Result { + match self { + #[cfg(windows)] + PlatformProvider::Windows(p) => p.get_content().await, + #[cfg(target_os = "linux")] + PlatformProvider::Linux(p) => p.get_content().await, + #[cfg(target_os = "macos")] + PlatformProvider::Macos(p) => p.get_content().await, + #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] + _ => Err(PlatformError::UnsupportedPlatform), + } + } + + async fn set_content(&self, item: &ClipboardItem) -> Result<(), PlatformError> { + match self { + #[cfg(windows)] + PlatformProvider::Windows(p) => p.set_content(item).await, + #[cfg(target_os = "linux")] + PlatformProvider::Linux(p) => p.set_content(item).await, + #[cfg(target_os = "macos")] + PlatformProvider::Macos(p) => p.set_content(item).await, + #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] + _ => Err(PlatformError::UnsupportedPlatform), + } + } + + async fn watch_changes(&self) -> Result<(), PlatformError> { + match self { + #[cfg(windows)] + PlatformProvider::Windows(p) => p.watch_changes().await, + #[cfg(target_os = "linux")] + PlatformProvider::Linux(p) => p.watch_changes().await, + #[cfg(target_os = "macos")] + PlatformProvider::Macos(p) => p.watch_changes().await, + #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] + _ => Err(PlatformError::UnsupportedPlatform), + } + } +} + +/// Get the appropriate clipboard provider for the current platform +pub fn get_provider() -> Result { + #[cfg(windows)] + { + Ok(PlatformProvider::Windows(WindowsProvider::new())) + } + + #[cfg(target_os = "linux")] + { + Ok(PlatformProvider::Linux(LinuxProvider::new())) + } + + #[cfg(target_os = "macos")] + { + Ok(PlatformProvider::Macos(macos::MacosProvider::new())) + } + + #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] + { + Err(PlatformError::UnsupportedPlatform) + } +} + +#[cfg(windows)] +mod windows { + use super::*; + use arboard::Clipboard; + use clipboard_core::{ClipboardItem, ContentType}; + + #[derive(Clone)] + pub struct WindowsProvider; + + impl WindowsProvider { + pub fn new() -> Self { + Self + } + } + + #[async_trait] + impl ClipboardProvider for WindowsProvider { + async fn get_content(&self) -> Result { + let mut ctx = + Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + let text = ctx + .get_text() + .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + let content_bytes = text.into_bytes(); + let content_type = ContentType::detect(&content_bytes); + + Ok(ClipboardItem::new(content_type, content_bytes)) + } + + async fn set_content(&self, item: &ClipboardItem) -> Result<(), PlatformError> { + if item.content_type == ContentType::Text { + let text = String::from_utf8_lossy(&item.content).to_string(); + let mut ctx = + Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + ctx.set_text(&text) + .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + } + // TODO: Handle other content types + Ok(()) + } + + async fn watch_changes(&self) -> Result<(), PlatformError> { + // TODO: Implement Windows clipboard watching using clipboard format listeners + Err(PlatformError::WatchFailed("Not implemented".to_string())) + } + } +} + +#[cfg(target_os = "linux")] +mod linux { + use super::*; + use arboard::Clipboard; + use clipboard_core::{ClipboardItem, ContentType}; + + #[derive(Clone)] + pub struct LinuxProvider; + + impl LinuxProvider { + pub fn new() -> Self { + Self + } + } + + #[async_trait] + impl ClipboardProvider for LinuxProvider { + async fn get_content(&self) -> Result { + let mut ctx = + Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + let text = ctx + .get_text() + .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + let content_bytes = text.into_bytes(); + let content_type = ContentType::detect(&content_bytes); + + Ok(ClipboardItem::new(content_type, content_bytes)) + } + + async fn set_content(&self, item: &ClipboardItem) -> Result<(), PlatformError> { + if item.content_type == ContentType::Text { + let text = String::from_utf8_lossy(&item.content).to_string(); + let mut ctx = + Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + ctx.set_text(&text) + .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + } + // TODO: Handle other content types + Ok(()) + } + + async fn watch_changes(&self) -> Result<(), PlatformError> { + // TODO: Implement clipboard watching using wl-clipboard or x11 events + Err(PlatformError::WatchFailed("Not implemented".to_string())) + } + } +} + +#[cfg(target_os = "macos")] +mod macos { + use super::*; + use arboard::Clipboard; + use clipboard_core::{ClipboardItem, ContentType}; + + #[derive(Clone)] + pub struct MacosProvider; + + impl MacosProvider { + pub fn new() -> Self { + Self + } + } + + #[async_trait] + impl ClipboardProvider for MacosProvider { + async fn get_content(&self) -> Result { + let mut ctx = + Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + let text = ctx + .get_text() + .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + let content_bytes = text.into_bytes(); + let content_type = ContentType::detect(&content_bytes); + + Ok(ClipboardItem::new(content_type, content_bytes)) + } + + async fn set_content(&self, item: &ClipboardItem) -> Result<(), PlatformError> { + if item.content_type == ContentType::Text { + let text = String::from_utf8_lossy(&item.content).to_string(); + let mut ctx = + Clipboard::new().map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + + ctx.set_text(&text) + .map_err(|e| PlatformError::AccessFailed(e.to_string()))?; + } + // TODO: Handle other content types + Ok(()) + } + + async fn watch_changes(&self) -> Result<(), PlatformError> { + // TODO: Implement clipboard watching using NSPasteboard notifications + Err(PlatformError::WatchFailed("Not implemented".to_string())) + } + } +} diff --git a/crates/clipboard-plugin/Cargo.toml b/crates/clipboard-plugin/Cargo.toml new file mode 100644 index 0000000..c9e41c1 --- /dev/null +++ b/crates/clipboard-plugin/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "clipboard-plugin" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +wasmtime = { workspace = true } +clipboard-core = { path = "../clipboard-core" } diff --git a/crates/clipboard-plugin/src/lib.rs b/crates/clipboard-plugin/src/lib.rs new file mode 100644 index 0000000..3ec0809 --- /dev/null +++ b/crates/clipboard-plugin/src/lib.rs @@ -0,0 +1,22 @@ +//! OpenPaste Plugin Module +//! +//! This module provides plugin functionality using WebAssembly (WASM). + +pub mod plugin; +pub mod sandbox; + +pub use plugin::PluginManager; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PluginError { + #[error("Plugin load failed: {0}")] + LoadFailed(String), + #[error("Plugin execution failed: {0}")] + ExecutionFailed(String), + #[error("Permission denied: {0}")] + PermissionDenied(String), + #[error("Invalid manifest")] + InvalidManifest, +} diff --git a/crates/clipboard-plugin/src/plugin.rs b/crates/clipboard-plugin/src/plugin.rs new file mode 100644 index 0000000..0e655a9 --- /dev/null +++ b/crates/clipboard-plugin/src/plugin.rs @@ -0,0 +1,39 @@ +//! Plugin manager implementation + +use crate::PluginError; +use std::path::Path; + +/// Plugin manager for loading and executing WASM plugins +pub struct PluginManager { + // TODO: Add WASM runtime +} + +impl Default for PluginManager { + fn default() -> Self { + Self::new() + } +} + +impl PluginManager { + /// Create a new plugin manager + pub fn new() -> Self { + Self {} + } + + /// Load a plugin from a file + pub async fn load(&self, _path: &Path) -> Result { + // TODO: Implement plugin loading + Err(PluginError::LoadFailed("Not implemented".to_string())) + } + + /// Unload a plugin + pub async fn unload(&self, _name: &str) -> Result<(), PluginError> { + // TODO: Implement plugin unloading + Ok(()) + } + + /// List loaded plugins + pub async fn list(&self) -> Result, PluginError> { + Ok(Vec::new()) + } +} diff --git a/crates/clipboard-plugin/src/sandbox.rs b/crates/clipboard-plugin/src/sandbox.rs new file mode 100644 index 0000000..0be3ae1 --- /dev/null +++ b/crates/clipboard-plugin/src/sandbox.rs @@ -0,0 +1,3 @@ +//! WASM sandbox implementation + +// TODO: Implement WASM sandbox with permission isolation diff --git a/crates/clipboard-search/Cargo.toml b/crates/clipboard-search/Cargo.toml new file mode 100644 index 0000000..3a38f93 --- /dev/null +++ b/crates/clipboard-search/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "clipboard-search" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +sqlx = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +clipboard-core = { path = "../clipboard-core" } diff --git a/crates/clipboard-search/src/lib.rs b/crates/clipboard-search/src/lib.rs new file mode 100644 index 0000000..f08d3e4 --- /dev/null +++ b/crates/clipboard-search/src/lib.rs @@ -0,0 +1,17 @@ +//! OpenPaste Search Module +//! +//! This module provides full-text search functionality using SQLite FTS5. + +pub mod search; + +pub use search::SearchEngine; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SearchError { + #[error("Search query failed: {0}")] + QueryFailed(String), + #[error("Invalid search syntax: {0}")] + InvalidSyntax(String), +} diff --git a/crates/clipboard-search/src/search.rs b/crates/clipboard-search/src/search.rs new file mode 100644 index 0000000..133af18 --- /dev/null +++ b/crates/clipboard-search/src/search.rs @@ -0,0 +1,48 @@ +//! Search engine implementation + +use crate::SearchError; +use sqlx::SqlitePool; + +/// Search engine for clipboard items +pub struct SearchEngine { + pool: SqlitePool, +} + +impl SearchEngine { + /// Create a new search engine + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + /// Search for clipboard items + pub async fn search(&self, query: &str, limit: usize) -> Result, SearchError> { + let results = sqlx::query_scalar::<_, i64>( + "SELECT rowid FROM clipboard_items_fts WHERE clipboard_items_fts MATCH ? LIMIT ?", + ) + .bind(query) + .bind(limit as i64) + .fetch_all(&self.pool) + .await + .map_err(|e| SearchError::QueryFailed(e.to_string()))?; + + Ok(results) + } + + /// Get search suggestions + pub async fn suggestions( + &self, + prefix: &str, + limit: usize, + ) -> Result, SearchError> { + let suggestions = sqlx::query_scalar::<_, String>( + "SELECT DISTINCT substr(content, 1, 50) FROM clipboard_items_fts WHERE content MATCH ?* LIMIT ?" + ) + .bind(prefix) + .bind(limit as i64) + .fetch_all(&self.pool) + .await + .map_err(|e| SearchError::QueryFailed(e.to_string()))?; + + Ok(suggestions) + } +} diff --git a/crates/clipboard-storage/Cargo.toml b/crates/clipboard-storage/Cargo.toml new file mode 100644 index 0000000..0d6671e --- /dev/null +++ b/crates/clipboard-storage/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "clipboard-storage" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +zstd = { workspace = true } +clipboard-core = { path = "../clipboard-core" } +chrono = { workspace = true } diff --git a/crates/clipboard-storage/src/lib.rs b/crates/clipboard-storage/src/lib.rs new file mode 100644 index 0000000..31089cb --- /dev/null +++ b/crates/clipboard-storage/src/lib.rs @@ -0,0 +1,21 @@ +//! OpenPaste Storage Module +//! +//! This module handles storage of clipboard items including compression and file system operations. + +pub mod storage; + +pub use storage::Storage; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum StorageError { + #[error("Storage operation failed: {0}")] + OperationFailed(String), + #[error("Compression failed: {0}")] + CompressionFailed(String), + #[error("Decompression failed: {0}")] + DecompressionFailed(String), + #[error("File not found: {0}")] + FileNotFound(String), +} diff --git a/crates/clipboard-storage/src/storage.rs b/crates/clipboard-storage/src/storage.rs new file mode 100644 index 0000000..93cc0e5 --- /dev/null +++ b/crates/clipboard-storage/src/storage.rs @@ -0,0 +1,67 @@ +//! Storage implementation + +use crate::StorageError; +use clipboard_core::ContentType; +use std::path::Path; + +/// Storage manager for clipboard items +pub struct Storage { + #[allow(dead_code)] + base_path: std::path::PathBuf, +} + +impl Storage { + /// Create a new storage manager + pub fn new(base_path: &Path) -> Self { + Self { + base_path: base_path.to_path_buf(), + } + } + + /// Store clipboard content + pub async fn store( + &self, + content_type: ContentType, + data: &[u8], + ) -> Result { + // Generate a unique filename based on content type and timestamp + let timestamp = chrono::Utc::now().timestamp(); + let filename = format!("{}_{}.zst", content_type as i32, timestamp); + let path = self.base_path.join(&filename); + + // Compress data using zstd + let compressed = zstd::encode_all(data, 3) // Level 3 compression + .map_err(|e| StorageError::CompressionFailed(e.to_string()))?; + + // Write to file + std::fs::write(&path, compressed) + .map_err(|e| StorageError::OperationFailed(e.to_string()))?; + + Ok(filename) + } + + /// Retrieve clipboard content + pub async fn retrieve(&self, path: &str) -> Result, StorageError> { + let full_path = self.base_path.join(path); + + // Read compressed data + let compressed = + std::fs::read(&full_path).map_err(|e| StorageError::FileNotFound(e.to_string()))?; + + // Decompress + let decompressed = zstd::decode_all(&*compressed) + .map_err(|e| StorageError::DecompressionFailed(e.to_string()))?; + + Ok(decompressed) + } + + /// Delete stored content + pub async fn delete(&self, path: &str) -> Result<(), StorageError> { + let full_path = self.base_path.join(path); + + std::fs::remove_file(&full_path) + .map_err(|e| StorageError::OperationFailed(e.to_string()))?; + + Ok(()) + } +} diff --git a/crates/clipboard-sync/Cargo.toml b/crates/clipboard-sync/Cargo.toml new file mode 100644 index 0000000..17ede8a --- /dev/null +++ b/crates/clipboard-sync/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "clipboard-sync" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/clipboard-sync/src/lib.rs b/crates/clipboard-sync/src/lib.rs new file mode 100644 index 0000000..8552203 --- /dev/null +++ b/crates/clipboard-sync/src/lib.rs @@ -0,0 +1,19 @@ +//! OpenPaste Sync Module +//! +//! This module provides synchronization functionality for clipboard data across devices. + +pub mod sync; + +pub use sync::SyncManager; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SyncError { + #[error("Sync failed: {0}")] + SyncFailed(String), + #[error("Provider not configured")] + ProviderNotConfigured, + #[error("Conflict detected: {0}")] + ConflictDetected(String), +} diff --git a/crates/clipboard-sync/src/sync.rs b/crates/clipboard-sync/src/sync.rs new file mode 100644 index 0000000..2b9952f --- /dev/null +++ b/crates/clipboard-sync/src/sync.rs @@ -0,0 +1,46 @@ +//! Sync manager implementation + +use crate::SyncError; + +/// Sync manager for clipboard synchronization +pub struct SyncManager { + // TODO: Add sync provider configuration +} + +impl Default for SyncManager { + fn default() -> Self { + Self::new() + } +} + +impl SyncManager { + /// Create a new sync manager + pub fn new() -> Self { + Self {} + } + + /// Start synchronization + pub async fn start(&self) -> Result<(), SyncError> { + // TODO: Implement sync start + Err(SyncError::ProviderNotConfigured) + } + + /// Stop synchronization + pub async fn stop(&self) -> Result<(), SyncError> { + // TODO: Implement sync stop + Ok(()) + } + + /// Get sync status + pub async fn status(&self) -> Result { + Ok(SyncStatus::Stopped) + } +} + +/// Sync status +#[derive(Debug, Clone, PartialEq)] +pub enum SyncStatus { + Stopped, + Running, + Error(String), +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..2be3d47 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,810 @@ +# OpenPaste Architecture + +## System Overview + +OpenPaste follows a **daemon-client architecture** where a background Rust daemon handles all clipboard operations, storage, and business logic. Multiple clients (desktop app, CLI, future mobile apps) communicate with the daemon via IPC and REST API. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ User Environment │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Desktop UI │ │ CLI │ │ REST API │ │ +│ │ (Tauri) │ │ (Rust) │ │ (Axum) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └─────────────────┼─────────────────┘ │ +│ │ │ +│ IPC / HTTP │ +│ │ │ +│ ┌────────────────────────▼─────────────────────────────────┐ │ +│ │ OpenPaste Daemon │ │ +│ │ (Rust) │ │ +│ ├───────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Platform │ │ Events │ │ Storage │ │ │ +│ │ │ Layer │ │ Bus │ │ Engine │ │ │ +│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ +│ │ │ │ │ │ │ +│ │ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ │ +│ │ │ Search │ │ Encryption │ │ Plugin │ │ │ +│ │ │ Engine │ │ Module │ │ Runtime │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Process Architecture + +### Daemon Process (`openpaste-daemon`) + +The daemon is a long-running background process that: + +- Monitors system clipboard for changes +- Stores clipboard items in SQLite database +- Provides IPC endpoints for client communication +- Serves REST API on localhost:7890 +- Loads and manages WebAssembly plugins +- Handles encryption/decryption operations +- Maintains full-text search index +- Manages retention and cleanup policies + +**Lifecycle:** +1. Started by OS (systemd/launchd/Windows Service) or user login +2. Initializes database, loads plugins, starts clipboard watcher +3. Listens for IPC and HTTP requests +4. Runs until system shutdown or explicit stop command + +### Desktop Client (`openpaste-desktop`) + +Tauri-based desktop application that: + +- Provides graphical user interface +- Communicates with daemon via IPC +- Shows search results and clipboard history +- Manages settings and preferences +- Displays notifications +- Handles keyboard shortcuts + +**Lifecycle:** +1. Launched by user (or auto-start on login) +2. Connects to running daemon (starts daemon if not running) +3. Opens main window or tray icon +4. Runs until user closes + +### CLI Client (`openpaste`) + +Command-line tool that: + +- Provides terminal-based clipboard access +- Communicates with daemon via IPC +- Supports scripting and automation +- Can run commands without UI + +**Lifecycle:** +1. Invoked by user or script +2. Connects to daemon (starts if needed) +3. Executes command +4. Exits with result + +## Module Decomposition + +### Core Crates + +#### `clipboard-core` + +**Purpose:** Core clipboard operations and data structures + +**Responsibilities:** +- Clipboard item data models +- Content type detection and normalization +- Duplicate detection +- Content validation +- Metadata extraction + +**Dependencies:** +- `clipboard-utils` (shared types) + +**Used by:** +- `clipboard-platform` +- `clipboard-db` +- All other crates + +#### `clipboard-platform` + +**Purpose:** Platform-specific clipboard access + +**Responsibilities:** +- Windows clipboard API integration +- Linux Wayland clipboard access +- Linux X11 clipboard access +- macOS clipboard API +- Clipboard change detection +- Format conversion + +**Dependencies:** +- `clipboard-core` +- Platform-specific crates (`clipboard-win`, `x11-clipboard`, etc.) + +**Used by:** +- Daemon + +#### `clipboard-db` + +**Purpose:** Database abstraction layer + +**Responsibilities:** +- SQLite connection management +- Schema migrations +- CRUD operations for clipboard items +- Transaction management +- Connection pooling + +**Dependencies:** +- `clipboard-core` +- `rusqlite` +- `clipboard-encryption` (for encrypted databases) + +**Used by:** +- `clipboard-storage` +- `clipboard-search` + +#### `clipboard-storage` + +**Purpose:** High-level storage operations + +**Responsibilities:** +- Storing clipboard items +- Retrieving clipboard items +- Compression/decompression +- Image handling +- Binary data storage +- Retention policy enforcement + +**Dependencies:** +- `clipboard-core` +- `clipboard-db` +- `clipboard-encryption` +- `zstd` (compression) + +**Used by:** +- Daemon +- `clipboard-sync` + +#### `clipboard-index` + +**Purpose:** Full-text search indexing + +**Responsibilities:** +- FTS5 index management +- Index updates on clipboard changes +- Index optimization +- Tokenization +- Language detection + +**Dependencies:** +- `clipboard-db` +- SQLite FTS5 + +**Used by:** +- `clipboard-search` + +#### `clipboard-search` + +**Purpose:** Query processing and ranking + +**Responsibilities:** +- Query parsing +- Search execution +- Result ranking (BM25, recency, frequency) +- Regex support +- Highlighting +- Filtering + +**Dependencies:** +- `clipboard-core` +- `clipboard-index` +- `clipboard-db` + +**Used by:** +- Daemon +- REST API +- CLI + +#### `clipboard-encryption` + +**Purpose:** Cryptographic operations + +**Responsibilities:** +- Master password derivation (Argon2id) +- Key management +- AES-256-GCM encryption/decryption +- Secure memory handling +- Key rotation + +**Dependencies:** +- RustCrypto libraries +- `zeroize` + +**Used by:** +- `clipboard-db` +- `clipboard-storage` +- `clipboard-sync` + +#### `clipboard-events` + +**Purpose:** Event bus and pub/sub + +**Responsibilities:** +- Event type definitions +- Channel management +- Publish/subscribe +- Async event handling +- Backpressure management + +**Dependencies:** +- `tokio` +- `clipboard-core` + +**Used by:** +- All crates that need to react to changes + +#### `clipboard-sync` + +**Purpose:** Synchronization protocols + +**Responsibilities:** +- Sync protocol implementation +- Conflict resolution +- Change tracking +- Network operations +- WebDAV/S3/Git backends + +**Dependencies:** +- `clipboard-core` +- `clipboard-storage` +- `clipboard-encryption` +- HTTP client libraries + +**Used by:** +- Daemon + +#### `clipboard-api` + +**Purpose:** REST API server + +**Responsibilities:** +- HTTP server (Axum) +- Endpoint handlers +- Request validation +- Response serialization +- Authentication +- Rate limiting +- WebSocket support + +**Dependencies:** +- `clipboard-core` +- `clipboard-search` +- `clipboard-storage` +- `clipboard-events` +- `axum` +- `tokio` + +**Used by:** +- Daemon + +#### `clipboard-cli` + +**Purpose:** Command-line interface + +**Responsibilities:** +- Command parsing +- Argument validation +- IPC communication +- Output formatting +- Shell completion + +**Dependencies:** +- `clipboard-core` +- IPC client library +- `clap` + +**Used by:** +- CLI binary + +#### `clipboard-plugin` + +**Purpose:** WebAssembly plugin runtime + +**Responsibilities:** +- WASM runtime (Wasmtime) +- Plugin lifecycle management +- Permission enforcement +- Host API implementation +- Sandboxing +- Plugin discovery + +**Dependencies:** +- `wasmtime` +- `clipboard-core` +- `clipboard-events` +- `clipboard-storage` + +**Used by:** +- Daemon + +#### `clipboard-ai` + +**Purpose:** AI-powered features + +**Responsibilities:** +- Content categorization +- Automatic summarization +- Embedding generation +- Offline model integration +- API integration (optional) + +**Dependencies:** +- `clipboard-core` +- ML libraries (candle, ort, etc.) +- HTTP client (for cloud APIs) + +**Used by:** +- Daemon +- Plugins + +#### `clipboard-utils` + +**Purpose:** Shared utilities + +**Responsibilities:** +- Common data structures +- Error types +- Logging configuration +- Testing utilities +- Benchmarking helpers + +**Dependencies:** +- Standard library only + +**Used by:** +- All other crates + +## Layer Architecture + +### Presentation Layer + +- **Desktop UI:** Tauri + React + TypeScript +- **CLI:** Rust command-line interface +- **REST API:** HTTP endpoints for external integration + +### Application Layer + +- **IPC Handler:** Inter-process communication +- **REST Handler:** HTTP request processing +- **Plugin Manager:** WASM plugin lifecycle +- **Event Bus:** Internal event distribution + +### Domain Layer + +- **Clipboard Operations:** Core clipboard logic +- **Search Engine:** Query processing and ranking +- **Storage Engine:** Data persistence +- **Encryption:** Cryptographic operations +- **Sync:** Synchronization logic + +### Infrastructure Layer + +- **Database:** SQLite with FTS5 +- **Platform APIs:** OS-specific clipboard access +- **File System:** Configuration and data storage +- **Network:** Sync and API communication + +## Data Flow + +### Clipboard Capture Flow + +``` +System Clipboard + │ + ▼ +clipboard-platform (detect change) + │ + ▼ +clipboard-core (normalize content) + │ + ▼ +clipboard-encryption (encrypt if enabled) + │ + ▼ +clipboard-storage (compress and store) + │ + ▼ +clipboard-db (persist to SQLite) + │ + ▼ +clipboard-index (update FTS index) + │ + ▼ +clipboard-events (publish ClipboardAdded event) + │ + ▼ +Clients (desktop, CLI, plugins) receive update +``` + +### Search Flow + +``` +User Query + │ + ▼ +clipboard-search (parse query) + │ + ▼ +clipboard-index (execute FTS search) + │ + ▼ +clipboard-search (rank results) + │ + ▼ +clipboard-storage (retrieve full items) + │ + ▼ +clipboard-encryption (decrypt if needed) + │ + ▼ +Return results to client +``` + +### Plugin Execution Flow + +``` +Plugin Request + │ + ▼ +clipboard-plugin (load WASM module) + │ + ▼ +clipboard-plugin (check permissions) + │ + ▼ +clipboard-plugin (execute in sandbox) + │ + ▼ +Host API (access clipboard, storage, etc.) + │ + ▼ +Return result to plugin + │ + ▼ +Plugin returns result + │ + ▼ +Return to caller +``` + +## Threading Model + +### Main Thread + +- Clipboard monitoring (platform-specific) +- IPC request handling +- Event loop + +### Worker Threads + +- Database operations (blocking I/O) +- Encryption/decryption (CPU-intensive) +- Compression/decompression +- Search index updates +- Plugin execution (WASM runtime) + +### Async Runtime + +- Tokio async runtime for: + - HTTP server + - WebSocket connections + - Network operations (sync) + - Timer-based tasks (cleanup, retention) + +## Design Decisions + +### Why Daemon-Client Architecture? + +**Pros:** +- Single source of truth for clipboard data +- Multiple clients can share state +- CLI works without UI +- Easier to add new clients (mobile, browser extension) +- Better resource utilization (one daemon, many clients) +- Cleaner separation of concerns + +**Cons:** +- More complex deployment +- Need IPC mechanism +- Daemon lifecycle management + +**Decision:** Benefits outweigh complexity. Enables extensibility and multiple client types. + +### Why SQLite? + +**Pros:** +- Cross-platform +- Embedded (no separate server) +- Mature and reliable +- Excellent performance for our use case +- FTS5 for full-text search +- Small footprint +- Easy backup + +**Cons:** +- Limited write concurrency +- Not suitable for distributed systems + +**Decision:** Perfect fit for local-first clipboard manager. Write concurrency not an issue (single daemon). + +### Why WebAssembly for Plugins? + +**Pros:** +- Language-agnostic (Rust, Go, AssemblyScript) +- Sandboxed execution +- Near-native performance +- Portable across platforms +- Easy to distribute (single .wasm file) + +**Cons:** +- Limited host API access +- WASM file size overhead +- Tooling complexity + +**Decision:** Provides best balance of safety, performance, and extensibility. + +### Why Tauri for Desktop? + +**Pros:** +- Rust backend (shared with daemon) +- Smaller bundle size than Electron +- Better performance +- Native OS integration +- Web technologies for UI (familiar to many) + +**Cons:** +- Smaller ecosystem than Electron +- Less mature (though rapidly improving) + +**Decision:** Aligns with Rust-first philosophy, better resource usage. + +### Why REST API + IPC? + +**IPC (for local clients):** +- Lower latency +- No HTTP overhead +- Can use binary serialization +- Better for frequent updates + +**REST API (for external integration):** +- Language-agnostic +- Easy to use from any tool +- Standard authentication +- Firewall-friendly + +**Decision:** Use both - IPC for desktop/CLI, REST for external tools and future clients. + +## Dependency Graph + +``` +clipboard-utils (no dependencies) + │ + ├── clipboard-core + │ │ + │ ├── clipboard-platform + │ ├── clipboard-db + │ ├── clipboard-events + │ └── clipboard-encryption + │ + ├── clipboard-storage + │ ├── clipboard-core + │ ├── clipboard-db + │ └── clipboard-encryption + │ + ├── clipboard-index + │ └── clipboard-db + │ + ├── clipboard-search + │ ├── clipboard-core + │ ├── clipboard-index + │ └── clipboard-db + │ + ├── clipboard-sync + │ ├── clipboard-core + │ ├── clipboard-storage + │ └── clipboard-encryption + │ + ├── clipboard-api + │ ├── clipboard-core + │ ├── clipboard-search + │ ├── clipboard-storage + │ └── clipboard-events + │ + ├── clipboard-cli + │ └── clipboard-core + │ + ├── clipboard-plugin + │ ├── clipboard-core + │ ├── clipboard-events + │ └── clipboard-storage + │ + └── clipboard-ai + └── clipboard-core +``` + +## Security Architecture + +### Defense in Depth + +1. **Process Isolation:** Daemon runs as separate process +2. **Encryption at Rest:** All sensitive data encrypted +3. **Sandboxing:** Plugins run in WASM sandbox +4. **Permission System:** Plugins require explicit permissions +5. **Memory Protection:** Sensitive data zeroed after use +6. **Authentication:** IPC and API require authentication +7. **Audit Logging:** Optional logging of clipboard access + +### Threat Model + +See [SECURITY.md](SECURITY.md) for detailed threat model. + +## Performance Architecture + +### Performance Targets + +- **Search Latency:** < 20ms for 10,000 items +- **Clipboard Capture:** < 50ms from clipboard change to storage +- **Startup Time:** < 500ms for daemon +- **Memory Usage:** < 100MB with 1,000 items +- **Database Size:** < 50MB for 10,000 text items + +### Optimization Strategies + +- **FTS5 Indexing:** Fast full-text search +- **Connection Pooling:** Reuse database connections +- **Async I/O:** Non-blocking operations +- **Compression:** zstd for large text +- **Lazy Loading:** Load clipboard content on demand +- **Caching:** Cache frequent searches +- **Batch Operations:** Batch database writes + +## Scalability Considerations + +### Single-User Design + +OpenPaste is designed for single-user, single-machine use: +- No multi-tenancy +- No distributed architecture +- No horizontal scaling + +### Future Scaling + +If needed, can scale by: +- Multiple daemon instances (one per user) +- Distributed sync (for multi-device) +- Sharded databases (for very large histories) + +## Extensibility Points + +### Plugin System + +Plugins can extend: +- Clipboard processing (transform content) +- Search behavior (custom ranking) +- Storage backends (cloud storage) +- Sync protocols (custom sync) +- UI components (desktop app) +- AI features (custom models) + +### Client API + +External tools can integrate via: +- REST API +- WebSocket events +- CLI commands + +### Configuration + +Extensible configuration system for: +- User preferences +- Plugin settings +- Sync providers +- Custom keybindings + +## Error Handling Strategy + +### Error Types + +- **Recoverable Errors:** Retry with backoff +- **Transient Errors:** Log and continue +- **Permanent Errors:** Log and notify user +- **Critical Errors:** Shutdown daemon + +### Error Propagation + +- Use Rust's `Result` throughout +- Custom error types per crate +- Error context with `anyhow` or `eyre` +- Structured error logging + +## Logging Strategy + +### Log Levels + +- **ERROR:** Critical errors requiring attention +- **WARN:** Warning conditions +- **INFO:** Normal operational messages +- **DEBUG:** Detailed debugging information +- **TRACE:** Very detailed tracing + +### Log Destinations + +- **Development:** stdout/stderr +- **Production:** File with rotation +- **Optional:** Syslog/journald integration + +### Sensitive Data + +- Never log clipboard content +- Never log encryption keys +- Sanitize error messages + +## Testing Strategy + +See [TESTING.md](TESTING.md) for comprehensive testing strategy. + +## Deployment Architecture + +### Development + +- Run daemon directly from cargo +- Desktop app in development mode +- Local database in project directory + +### Production + +- Daemon installed as system service +- Desktop app installed to Applications/Program Files +- Database in user data directory +- Configuration in user config directory + +### Package Distribution + +- **Windows:** MSI installer +- **macOS:** DMG with app bundle +- **Linux:** AppImage, deb, rpm + +## Monitoring and Observability + +### Metrics + +- Clipboard capture rate +- Search latency +- Database size +- Memory usage +- Plugin execution time +- Error rates + +### Health Checks + +- Daemon liveness (IPC ping) +- Database integrity +- Clipboard watcher status +- Plugin health + +### Debugging + +- Structured logging +- Optional debug mode +- Performance profiling +- Memory profiling diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..428e06c --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,563 @@ +# Contributing to OpenPaste + +Thank you for your interest in contributing to OpenPaste! This document provides guidelines and instructions for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Pull Request Process](#pull-request-process) +- [Coding Standards](#coding-standards) +- [Testing](#testing) +- [Documentation](#documentation) +- [Issue Reporting](#issue-reporting) +- [Feature Requests](#feature-requests) + +## Code of Conduct + +### Our Pledge + +We are committed to providing a welcoming and inclusive environment for all contributors. We value respect, kindness, and collaboration. + +### Our Standards + +- Be respectful and inclusive +- Welcome newcomers and help them learn +- Focus on what is best for the community +- Show empathy towards other community members + +### Unacceptable Behavior + +- Harassment, discrimination, or exclusion +- Personal attacks or insults +- Public or private harassment +- Publishing others' private information +- Other unethical or unprofessional conduct + +### Reporting Issues + +Report conduct issues to the project maintainers via email or private message. + +## Getting Started + +### Prerequisites + +**Required:** +- Rust 1.75 or later +- Node.js 18 or later +- Git + +**Platform-Specific:** +- Windows: Visual Studio Build Tools +- Linux: build-essential, libssl-dev +- macOS: Xcode Command Line Tools + +### Setting Up Development Environment + +**1. Fork and Clone:** +```bash +git clone https://github.com/your-username/openpaste.git +cd openpaste +``` + +**2. Install Rust Dependencies:** +```bash +cargo install cargo-watch +cargo install cargo-nextest +cargo install cargo-edit +``` + +**3. Install Node Dependencies:** +```bash +cd apps/desktop +npm install +``` + +**4. Build Project:** +```bash +# Build all Rust components +cargo build + +# Build desktop app +cd apps/desktop +npm run tauri build +``` + +**5. Run Tests:** +```bash +cargo test +``` + +### Development Workflow + +**1. Create Branch:** +```bash +git checkout -b feature/your-feature-name +``` + +**2. Make Changes:** +- Write code +- Add tests +- Update documentation + +**3. Commit Changes:** +```bash +git add . +git commit -m "feat: add your feature" +``` + +**4. Push and Create PR:** +```bash +git push origin feature/your-feature-name +``` + +## Pull Request Process + +### PR Guidelines + +**Before Submitting:** +- [ ] Code follows project style guidelines +- [ ] Tests added/updated +- [ ] Documentation updated +- [ ] Commit messages follow conventions +- [ ] PR description clearly describes changes +- [ ] No merge conflicts + +### PR Description Template + +```markdown +## Description +Brief description of changes + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## Testing +How did you test this change? + +## Checklist +- [ ] Tests pass +- [ ] Documentation updated +- [ ] No breaking changes (or documented) +``` + +### Review Process + +1. **Automated Checks:** CI runs tests and linting +2. **Code Review:** Maintainers review code +3. **Feedback:** Address review comments +4. **Approval:** At least one maintainer approval +5. **Merge:** Maintainer merges PR + +### Commit Message Conventions + +**Format:** +``` +(): + + + +