diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb89fbb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm test + - run: npm run build + + rust: + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libdbus-1-dev + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci && npm run build + - run: cargo fmt --check + working-directory: src-tauri + - run: cargo clippy --all-targets -- -D warnings + working-directory: src-tauri + - run: cargo test + working-directory: src-tauri + + plugin: + runs-on: ubuntu-latest + defaults: + run: + working-directory: plugin/remote-editor-bridge + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + tools: composer + - run: composer install --no-interaction --no-progress + - run: find . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l + - run: vendor/bin/phpcs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e0e1860 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Release + +# Builds signed installers on real macOS, Linux and Windows machines when a +# version tag is pushed, and attaches them to a draft GitHub release. +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - platform: macos-latest + args: --target universal-apple-darwin --bundles app,dmg + - platform: ubuntu-22.04 + args: --bundles deb,appimage + - platform: windows-latest + args: --bundles msi + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + + - name: Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libdbus-1-dev + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ runner.os == 'macOS' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci + + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # macOS: Developer ID signing + notarization + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # Update manifests are signed with minisign once the updater is enabled + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + tagName: ${{ github.ref_name }} + releaseName: WordPress Remote Editor ${{ github.ref_name }} + releaseDraft: true + prerelease: false + args: ${{ matrix.args }} + + plugin: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Package the companion plugin + run: | + cd plugin + zip -r ../remote-editor-bridge.zip remote-editor-bridge -x 'remote-editor-bridge/vendor/*' 'remote-editor-bridge/composer.*' 'remote-editor-bridge/phpcs.xml.dist' + - uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + files: remote-editor-bridge.zip diff --git a/README.md b/README.md index 8986249..b40b6dc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,80 @@ # WordPress Remote Editor -A native desktop editor for WordPress sites (macOS, Windows, Linux). +A native desktop app for macOS, Windows and Linux that lets you write and publish posts and pages on any WordPress site, using the site's full block editor (Gutenberg), with every block plugin installed on that site working exactly as it does in wp-admin. + +- **Native shell**: real native menus and shortcuts, macOS vibrancy and traffic lights, Windows 11 Mica, GNOME/KDE-friendly styling, OS light/dark mode. +- **Full block compatibility**: the editor pane runs the site's own block editor, so third-party blocks, sidebar plugins and meta boxes all work. +- **Role-aware**: only offers the content types and actions your WordPress role allows (for example, contributors get *Submit for Review* instead of *Publish*). +- **Secure by design**: Application Passwords only, stored in the OS keychain; HTTPS enforced; isolated, origin-locked editor webview. See [docs/SECURITY.md](docs/SECURITY.md). +- **Installers**: `.dmg` (macOS, universal), `.deb` (Linux, plus AppImage), `.msi` (Windows). + +Read [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design and the reasoning behind it. + +## Requirements + +On the WordPress site: + +- WordPress 6.4 or newer, served over HTTPS. +- Application Passwords enabled (the default in WordPress core; some security plugins or hosts turn them off). +- Optional but recommended: the **Remote Editor Bridge** plugin from [`plugin/remote-editor-bridge`](plugin/remote-editor-bridge), which removes the second sign-in when opening the editor. + +On your computer: macOS 14+, Windows 10/11 (WebView2 is installed automatically if missing), or a Debian/Ubuntu-based Linux with WebKitGTK 4.1. + +## Development + +Prerequisites: Node 22+, Rust (stable), and the [Tauri prerequisites](https://tauri.app/start/prerequisites/) for your OS. On Debian/Ubuntu: + +```sh +sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libdbus-1-dev +``` + +```sh +npm install +npm run tauri dev # run the app with hot reload +npm test # frontend tests +cd src-tauri && cargo test # Rust tests +``` + +Live tests against a real site (for example a local one on `http://localhost:8888`): + +```sh +cd src-tauri +WPRE_TEST_SITE=https://example.com WPRE_TEST_USER=me WPRE_TEST_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" \ + cargo test live_tests -- --ignored --nocapture +``` + +Companion plugin checks: + +```sh +cd plugin/remote-editor-bridge +composer install +vendor/bin/phpcs +``` + +## Building installers + +```sh +npm run tauri build -- --bundles deb,appimage # on Linux +npm run tauri build -- --target universal-apple-darwin --bundles dmg # on macOS +npm run tauri build -- --bundles msi # on Windows +``` + +Pushing a `v*` tag runs [`.github/workflows/release.yml`](.github/workflows/release.yml), which builds, signs and notarizes all three on GitHub's macOS, Ubuntu and Windows runners and attaches them to a draft release, along with a zip of the companion plugin. Signing secrets are listed in the workflow file. + +## Project layout + +| Path | What it is | +| --- | --- | +| `src/` | Platform-adaptive React shell UI | +| `src-tauri/src/` | Rust core: REST client, sign-in, keychain, editor webview, menus | +| `src-tauri/capabilities/` | Least-privilege permission grants for each webview | +| `plugin/remote-editor-bridge/` | Optional companion WordPress plugin | +| `docs/` | Architecture and security notes | + +## Name and trademark + +"WordPress" is a registered trademark of the WordPress Foundation. Its trademark policy restricts using "WordPress" in product names, so the app name should be cleared (or changed to something like "Remote Editor for WordPress") before public distribution. The companion plugin already uses a compliant name. + +## License + +GPL-2.0-or-later, the same license as WordPress. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..8dc6e37 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,118 @@ +# WordPress Remote Editor: Architecture + +## Goals + +1. Create and edit posts, pages and any other REST-enabled post type on a remote WordPress site, limited to exactly what the signed-in user's role allows. +2. Run the full Gutenberg block editor, including blocks, sidebars and meta boxes added by any plugin installed on that site. +3. Install natively on macOS (`.dmg`), Linux (`.deb`) and Windows (`.msi`) and feel like a native app on each. +4. Follow WordPress standards (REST API, Application Passwords, capabilities, coding standards, GPL) and current desktop security practice. + +## The key design decision: where Gutenberg runs + +Gutenberg is a JavaScript application, and every block plugin ships JavaScript that is written against the exact versions of `wp.blocks`, `wp.blockEditor`, `wp.data`, React and so on that the site is running. Plugins can also add PHP-rendered meta boxes (ACF, Yoast, WooCommerce) and enqueue arbitrary editor assets. + +Three ways to get Gutenberg into a desktop app were considered: + +| Approach | Plugin compatibility | Notes | +| --- | --- | --- | +| Bundle our own copy of `@wordpress/*` packages and render blocks locally | Poor | Version skew with the site; plugin block scripts can't be loaded reliably; PHP meta boxes impossible. | +| Load the site's editor scripts into a local shell page | Partial | Needs a server-side asset manifest; meta boxes and many `admin_enqueue_scripts` integrations still break. | +| **Load the site's own block editor screen in an isolated, hardened webview inside a native shell** | **Complete** | The site runs its real editor, so every block, sidebar plugin and meta box works exactly as in wp-admin. | + +We use the third approach. It is the only one that can honour "fully compatible with any kind of block plugin". Everything *around* the editor canvas is native: windows, menus, keyboard shortcuts, site and content browser, toolbar, dialogs, notifications, credential storage. The wp-admin chrome (admin bar, admin menu, the "W" back button) is hidden so the editor reads as part of the app, not as a website. While a post is open the sidebar collapses (focus mode) and the editor fills the window below the app's own toolbar, which carries Save, Publish/Submit for Review, Undo/Redo, preview sizes and the settings toggle. + +## Framework: Tauri 2 (Rust core + system webview) + +| Option | Why not / why | +| --- | --- | +| Electron | Ships a whole Chromium (~150 MB), larger attack surface, looks the same everywhere. | +| Three native apps (SwiftUI, WinUI, GTK) | Most native, but three codebases and we still need a webview for Gutenberg. | +| Qt / Flutter | Custom-drawn widgets, not truly native; still needs a webview. | +| **Tauri 2** | Uses each OS's own webview (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux), real native menus, dialogs, window effects (macOS vibrancy, Windows Mica), ~10 MB installers, a Rust core with a capability-based permission model, and first-party `.dmg`/`.deb`/`.msi` bundling. | + +### Native feel per platform + +The shell UI is React, but it is *platform-adaptive*, not a single web look: + +- **macOS**: overlay title bar with traffic lights, translucent sidebar (`sidebar` vibrancy), SF Pro, macOS control metrics, ⌘ shortcuts, standard app menu. +- **Windows 11**: Mica backdrop, Segoe UI Variable, Fluent spacing and focus visuals, Ctrl shortcuts, native title bar. +- **Linux (GNOME/KDE)**: native decorations, libadwaita-like header and list styling, system font (Cantarell/Noto). +- All platforms follow the OS light/dark setting and accent colour, use native file dialogs, native notifications and the OS keychain. + +## Process and trust model + +``` +┌──────────────────────── Native window ─────────────────────────┐ +│ Shell webview (trusted, bundled assets only) │ +│ React UI: sites · post lists · toolbar ── IPC ──┐ │ +│ ▼ │ +│ Rust core (all network + secrets) +│ · WordPress REST client │ +│ · OS keychain │ +│ · window/webview manager │ +│ ┌───────────────── Editor child webview (untrusted) ──────────┐ │ +│ │ https://site/wp-admin/post.php?post=… (site's real Gutenberg)│ │ +│ │ · navigation locked to the site origin │ │ +│ │ · per-site isolated cookie store │ │ +│ │ · IPC limited to one command: editor_report │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +- **Shell webview**: loads only bundled assets, strict CSP, never talks to WordPress directly and never sees a password. +- **Rust core**: the only component that holds credentials or performs authenticated HTTP. HTTPS-only (plain HTTP allowed solely for `localhost`/`.local`/`.test` development sites after explicit opt-in), rustls with the OS trust store, redirects restricted to the same origin, response size limits. +- **Editor webview**: treated as untrusted remote content. It gets a runtime Tauri capability scoped to *that site's origin and that webview label* allowing only `editor_report` (dirty/saving/status/title state for the native toolbar). Native → editor control (save, publish, preview) is done by the core evaluating a fixed script; nothing from the page is ever evaluated by the core. + +## Authentication + +WordPress **Application Passwords** (core since 5.6) are used for all REST traffic. + +1. **Discovery**: fetch the site, follow the `Link: <…>; rel="https://api.w.org/"` header to the REST index, read `authentication["application-passwords"].endpoints.authorization`. Works with pretty and plain permalinks (`?rest_route=`). +2. **Authorise**: open WordPress's own `authorize-application.php` in an isolated, incognito auth window. The user signs in with their normal login (2FA, SSO and security plugins keep working) and approves "WordPress Remote Editor". `success_url` is `wpremoteeditor://auth/callback?state=…`; the core intercepts that navigation inside its own webview (so no other app can register the scheme and steal it), checks `state`, and stores the password. +3. **Manual fallback**: paste a username and an Application Password created under Users → Profile. +4. **Storage**: the password goes straight into the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service/libsecret on Linux) and is held in memory only in zeroizing buffers. +5. **Removal**: removing a site revokes its Application Password on the server (using `/application-passwords/introspect` to find its UUID) and deletes the keychain entry. + +### Signing the editor webview in + +wp-admin uses cookie authentication, which Application Passwords can't provide. Two modes: + +- **With the companion plugin** (`plugin/wp-remote-editor-bridge`, recommended): the core calls `POST /wpre/v1/handoff` authenticated by the Application Password. The plugin returns a single-use, 60-second, hashed token bound to that user; the editor webview opens `wp-login.php?action=wpre_handoff&token=…`, which sets the normal auth cookie and redirects into the editor. No second login. +- **Without the plugin**: the user signs in once inside the editor webview using the site's own login form. The cookie lives in that site's isolated store. + +## Roles and capabilities + +- `GET /wp/v2/users/me?context=edit` gives the user's roles and full capability map. +- `GET /wp/v2/types?context=edit` gives each post type's capability names (`edit_posts`, `publish_posts`, `create_posts`, …). +- The shell only offers post types the user can create, shows *Publish* vs *Submit for Review* according to `publish_posts`, and hides actions the user can't perform. WordPress still enforces everything server-side; the UI gating is for clarity, not security. + +## Repository layout + +``` +src/ React shell (platform-adaptive UI) +src-tauri/ Rust core: REST client, auth, keychain, editor webview + capabilities/ Tauri capability files (least privilege) +plugin/wp-remote-editor-bridge/ Optional companion WordPress plugin (GPL-2.0-or-later, WPCS) +.github/workflows/ CI (lint, test, phpcs) and signed release builds +docs/ Architecture and security notes +``` + +## Packaging and distribution + +| OS | Artifact | Signing | +| --- | --- | --- | +| macOS | `.dmg` (universal: Apple Silicon + Intel) | Developer ID signing + Apple notarization, hardened runtime | +| Linux | `.deb` (plus AppImage as a bonus) | Detached GPG signature; desktop entry registers the URL scheme | +| Windows | **`.msi`** (WiX) | Authenticode signing | + +`.msi` is chosen over `.exe` (NSIS) because it is what IT departments deploy with Intune/Group Policy, supports clean upgrades and repair, and is equally easy for home users. WebView2 is bootstrapped by the installer if missing. + +Builds run on GitHub Actions on real macOS, Ubuntu and Windows runners via `tauri-action`. Auto-update uses Tauri's updater with minisign-signed update manifests (added once a release channel exists). + +## Roadmap + +1. **Scaffold (this commit)**: shell, site onboarding, keychain, REST client, capability gating, embedded editor, companion plugin, packaging config, CI. +2. Media library browser with native drag-and-drop upload. +3. Offline drafts queued locally (encrypted at rest) and synced on reconnect. +4. Multi-site switching with per-site windows, notifications for scheduled posts. +5. Auto-update channel, crash reporting (opt-in). diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..ffe4405 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,54 @@ +# Security Model + +WordPress Remote Editor holds credentials for websites and renders remote content, so it is designed around least privilege and isolation. + +## Credentials + +- Only **Application Passwords** are used (never the user's main WordPress password). They can be revoked per app from the WordPress profile screen, and the app revokes its own when a site is removed. +- Passwords are stored only in the **OS keychain** (macOS Keychain, Windows Credential Manager, Secret Service/libsecret on Linux), never on disk in plain text, never in logs, never in the shell UI. +- In memory, secrets live in zeroizing buffers and are redacted from debug output. +- Non-secret site metadata (`sites.json`) is written atomically with `0600` permissions on macOS/Linux. + +## Network + +- HTTPS only. Plain HTTP is allowed solely for `localhost`, `*.local`, `*.test` and `*.localhost`, and only when the user ticks "local development site". +- TLS via rustls with the operating system's trust store. +- Authenticated requests never follow redirects to another origin, so credentials can't be forwarded to a third party. Discovery (unauthenticated) may follow HTTPS redirects, e.g. `example.com` → `www.example.com`. +- URLs supplied by the server (REST root, editor handoff) must be on the site's own origin. +- Response bodies are capped at 16 MB; timeouts are enforced. + +## Sign-in + +- Authorization happens on the site's own `authorize-application.php` page, so 2FA, SSO and security plugins work unchanged. +- The sign-in window is incognito (no persistent cookies) and has no IPC access. +- The callback (`wpremoteeditor://…`) is intercepted inside that window, not registered with the OS, so another app cannot hijack it. A 256-bit `state` value is checked in constant time. + +## Webviews + +| Webview | Loads | IPC | +| --- | --- | --- | +| Shell (`main`) | Bundled assets only, strict CSP, prototype freezing | App commands listed in `capabilities/main.json`; cannot call `editor_report` | +| Sign-in (`auth`) | The site's authorization page | None | +| Editor (`editor`) | The site's own wp-admin block editor | Only `editor_report`, granted at runtime for that site's origins; payload is strictly validated | + +- The editor webview's navigation is locked to the site's origins; other links open in the system browser. +- Each site has its own cookie/data store, deleted when the site is removed. +- The core controls the editor only by evaluating fixed, built-in scripts. It never evaluates strings that come from a page. + +## Companion plugin (`remote-editor-bridge`) + +- Handoff links can only be requested over REST with Application Password authentication by users who can edit content. +- Tokens are 48 random characters, single use, valid for 60 seconds, stored only as an HMAC-SHA256 hash, rate limited to 10/minute per user, and can only redirect to `post.php`/`post-new.php`. +- The standard `wp_login` action fires so audit/security plugins see the sign-in. +- Passes WordPress Coding Standards (`phpcs`), escapes all output, and stores nothing permanently. + +## Releases + +- macOS: Developer ID signed, hardened runtime, notarized. +- Windows: Authenticode-signed `.msi` (configure `bundle.windows.certificateThumbprint` or `signCommand`, e.g. Azure Trusted Signing). +- Linux: `.deb` with a detached signature. +- Auto-updates (when enabled) are verified with a minisign public key embedded in the app. + +## Reporting a vulnerability + +Please report security issues privately through GitHub Security Advisories on this repository rather than opening a public issue. diff --git a/index.html b/index.html new file mode 100644 index 0000000..fb0ac86 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + WordPress Remote Editor + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6d4dd81 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3233 @@ +{ + "name": "wordpress-remote-editor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wordpress-remote-editor", + "version": "0.1.0", + "license": "GPL-2.0-or-later", + "dependencies": { + "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-os": "^2.3.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.8.0", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.20.5", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^5.0.0", + "jsdom": "^26.1.0", + "typescript": "^5.9.0", + "vite": "^7.1.0", + "vitest": "^3.2.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "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/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.5.tgz", + "integrity": "sha512-MrfgRQSpD5hkkRg6XI16B6EDLb+6+xp8xje6bpdSwkYw+qxrBKbUVrRAQu9h82UT+QumSIVQ8HXsXjPd2dlhqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.5.tgz", + "integrity": "sha512-+uu2p/R9Tzn8GpoQyX1pAo5dmKT1DhXevfaCzqzmPFhnLjHLWw7uWkaVGlxmhwYoRiYYhsGfRcD5cTBpL7rgSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.5.tgz", + "integrity": "sha512-17dys+YEmzwzzFVHQAVDONg5FALXY9kd99HFEpfk4gJVHYxoh5bGea3D4t1fXXnwkTcOe25wPyd6nWS0I8IdLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.5.tgz", + "integrity": "sha512-wg0zQEKkEvFZf1UaaX1UELOoPs//OSKUG7D+yITuqOsAvza7LvHTv5Wu4nmMfzNG7EYI2jp2PJC7Id6OETmsYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.5.tgz", + "integrity": "sha512-CoJmWcpEMuwin+TZ/ev/H9uDyF/uxUKQSWXdmIPn/UBx/uGV9+4ebMp0FwnlhWR9PE1AdzYsKf3bZevhif38+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.5.tgz", + "integrity": "sha512-PxZnLAaTUAL5xG0W2BXLeYPKrK2b3LIASV2/e/Kf7PZvSLI1iAQIm9B9aJD+YyFIT7hc6qaMV3+OOO35R28LfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.5.tgz", + "integrity": "sha512-uPo3IvnONjhbFXwWgroBeFwOicuhT7ZwQrnzkzmOtBJzeQbkUJUE1SJDfDI31ijQ5+ZUIA6sPrdUMxTSnxUWAg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.5.tgz", + "integrity": "sha512-ywI8Jyuc4LyHYTjC0mw4yA1ulvZBkHusdpWnoiEBsmBuHvSnt90btTBGCZ+s/2y+YNNfVAEKy33nPo5UI9zTmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.5.tgz", + "integrity": "sha512-tmuXJoQ/3ovSr637aykZMdQteU/CB2APAz2CrvsbibuoHZi1BS1Db13cVViBdEARGwXCLn1DSJ8wsjoThLJPGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.5.tgz", + "integrity": "sha512-5FMjSYNcUOHx7oW8Qu9KB6jGPpvF/8sG7+BDHwU/Ihh1/6FMJ9ySmsFG4tJ1qbNRuP8OLzCBDso4sF0mQaLuFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.5.tgz", + "integrity": "sha512-K8ZJ+neLDbUkIrL8dqF/5LtuVCrKwo27kREgOlxjBEjlOXwHvDnHLU8Z/UKv/B49qi7e8hsSDOyoJwhwqhvX4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.5.tgz", + "integrity": "sha512-NlVxsq7PFPv0DBKGuNWRDKixQYQCjM53zl04mxEcKMBmJbCBIUm1JdPfIWDRHyOOFzzYPmLQzfeEwh4716ayQg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.5.tgz", + "integrity": "sha512-ARhRHJYs5eCFet5azfrHD97vOkD8OzNW2RSMlvo5CcBqXaTOLfp0OHDtUQh7/K5AL5fUYoFp2fgC9KDg4vVYMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.5.tgz", + "integrity": "sha512-atSgreM5C7jpxd9aEXN+6vJ5wTd05iYw+S3FiQpw7VsoEl8B90llbq35ARON+GOHj5QlVQEq2vOKWCHTob/BSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.5.tgz", + "integrity": "sha512-vmlmwBFGnjVWxSRoVabNkyYuscSmg52Pd0fM8jEbNT3Jmka4A6HcB84x4uAmuumznWfBVaxKOqU6jbEgomWKCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.5.tgz", + "integrity": "sha512-hhJrP6S9WkbsbcHgUkvCBS5Cn8IdsG8XjlLZyxjnv9+NdoYfbbZE4TWJYr8apdaj1M5t5Y1id2StnEslxpNFyA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.5.tgz", + "integrity": "sha512-NNVCyN9pGrwFmQa4bf+Cnf5aVH3g6ZpnlOg1Cs/H9swIHT9bRixijfzUqyEIWm1WtiAuR1LJ+rJH6zpBm8kgQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.5.tgz", + "integrity": "sha512-dR9nd6OKnvSS60Ifz/L7Mj5zzYyoqyRn2NwTuaOONKaa3xsVGXyp8YusMbvFyEOR94DxnwgcbgsFqkHd29ZSzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.5.tgz", + "integrity": "sha512-1tzsMjaqhsh3bAO6SJiJqh8hIr9sSPRd+i2vPPxJHod8/XYnf9TEnK0z/u3r1z1AkI347X0KbRSg9dWcxeGahw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.5.tgz", + "integrity": "sha512-01mWMQblKoXXsmk28KXtpbA96w7g/aOWUoV3AC16fdHdqkykGalqyyM+5u8M7uP5/MEMiiPv69Hv7iv/hEH/dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.5.tgz", + "integrity": "sha512-trdnMPb/DNm4V/CgsaoGgr90HfQQ071wfK0KoIs4YpLvjBgVQGXhGabg2Z6nK4EcYNilrb9ZZPcsP6+zpJIdxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.5.tgz", + "integrity": "sha512-x2LxZlfdfsw99T6/qb9EqE/Qi84JWzibjyOJyhJsgFJy6CafaTiqIXCCnslCGrV5XWNh7fwrS3FbRENcuKnooA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.5.tgz", + "integrity": "sha512-/Qd/9aDQIo0+jmItgBmPIs8195YVbDxRRApn47GyufgbutCDOh7eP5vSKKlI4QNFeaI+wKQAkSMsPnF3EuMpaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.5.tgz", + "integrity": "sha512-ngUJ/XvOWrqbd2IMsfXfh2xXFLTP3sY7sCKvxbGvYdBZ1KmXFd3ewDZ9n3/GNzAUgzRuhLT/UvJRivbNoAQRBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.5.tgz", + "integrity": "sha512-IhV6ew/DuzUWwHaYmlh7MrPCV3lKFUq/amTEwysi4r/mwNPAEfAMh5dYWeTSPTeMy2pFHGMEWYtPkHzfX3tG5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.3.tgz", + "integrity": "sha512-9s0PKw0p4r3kFq92jhpivYSp+cSht15o0gXpZxuQd0+dV9KCexEVkNaLELVoLdJWyoH4KlfQo5h3W2LIM8siJg==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.2.tgz", + "integrity": "sha512-PMSppzAnlT+BvZtRKuidxjRSm2r5t8kDpJ/czmmQUa0AtDNTwrjLE8vApK3phCWGg7isfnYL46Dl1jB4ArI0hA==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.12.2", + "@tauri-apps/cli-darwin-x64": "2.12.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.12.2", + "@tauri-apps/cli-linux-arm64-musl": "2.12.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.12.2", + "@tauri-apps/cli-linux-x64-gnu": "2.12.2", + "@tauri-apps/cli-linux-x64-musl": "2.12.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.12.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.12.2", + "@tauri-apps/cli-win32-x64-msvc": "2.12.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.2.tgz", + "integrity": "sha512-Lk3zaBTfzNvA7670Al5PNgo2M/95uaDPe1yp+ILJeDOmol87XiYuK5E5+FUbK5e6U6OORx9qpZdOo2U+5tETtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.2.tgz", + "integrity": "sha512-2CyYjJ833JB3sKRz4B0W/Tv4oOVC16ws1bxdSCWj1XvrmanWwvZi99e5tNIoCHwHhcOm0jh+sh0PCGQyls6Z+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.2.tgz", + "integrity": "sha512-uvt2xbone4JftKarelssi86Kqnl7JiG1Smh5+Ae+nZmtM+JUhTGmqkcqfUPXRW8Ygwog92POZvd4oGDN9j8eXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.2.tgz", + "integrity": "sha512-DZlDJ24u3tlocKOYqzR+b2M+Qr+5WFcoMBZ7EfdhHN5j3ZXwBBpms4G4gyNvPCrak5vMzmdZ65ecBXPJcC+5sA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.2.tgz", + "integrity": "sha512-pb+RTZUYH/p0gR4j4ecCCq+FNfz3W3eKuUQvVEBn7MQZx6DzfLO7FaYzt2NyDKWpdNN3lP10mdzOvw3R5P5aAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.2.tgz", + "integrity": "sha512-nidEvDUJyHYNbIv9MUk6KiEix8IXfRh4Wj2G2SA5g8nFmgbBCwazUNWYiG/LhNeNbfZpBMLerRBUtoQ5K0GidA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.2.tgz", + "integrity": "sha512-gkXyYn3w+Xfc8nz/DleEbkm1M7bBuU2cejqUXNmI98cA2P5kp+oPmS4ALTorb1FaNWjpsKoYZBWsbS9BXxYFkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.2.tgz", + "integrity": "sha512-MrjgoZ09s8Hsn99XvxSNvK8NQTZxi7XiyK3FxxytzfHgz2jb3qUT69CZtsokRkN/zdi9rCgrMtq+w9XNICjcfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.2.tgz", + "integrity": "sha512-BR+CYM7iF3lkfyTtVkwMQU/s9hwegLpj3UqDjaxKCPmixQCa1csBBPy3C75V+FhLtOjIXNna4Gt6Vm9oxbV+qA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.2.tgz", + "integrity": "sha512-zODJo9l3TbNBM+GrO/yPqGeELNMK5cjIc+QHh1WQc3l8tD+jDZGTQyYuAxViCsM6Uh5uWJ//RnCZG9LXrvsXww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.2.tgz", + "integrity": "sha512-cQXI2ZaZmwBDt0XATCqASVanYizxkEE3ceQVlVHXaQ51HzsQUWA04c9Nz7LrrY2lgAu3jFMyat+8Yz3Z2R2qBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.8.2.tgz", + "integrity": "sha512-LFqp8OTx0bHOwhKRhMMDyRDlo/2IO5XxoOclpf8+yvjAfM2ytVdbB6tk63dBlicsR8pRw38maj/GxrU6cW8mXw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.7.1.tgz", + "integrity": "sha512-XkXdt30rontti9TII/gBBxFRXWfj8DiprFW7RgBFRBj1OhdfjBNVcFqSEWkkZ4hXafHnuEnB7F1sFsmu6uyywQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, + "node_modules/@tauri-apps/plugin-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.4.0.tgz", + "integrity": "sha512-XFQjntNNI3Vlqdy4WUGwiXX8DByzKVvoemAcUVkBm9NJPGmoDtII5i+vfuAzWaJfOE5cUJJLUDqXBnd4reSAIA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "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/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "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.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.453", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.453.tgz", + "integrity": "sha512-9r/nz9akllr3SZsHUhfK10ktUH6j0Dbo3cKdh91khw0FvR6HhXGnATmOq5X8ena2aq4GXLQ1koh3nvKaQJaNmQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "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/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "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/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "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/node-releases": { + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "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.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.5.tgz", + "integrity": "sha512-EAP5ZHcz2ijbgCWwPkpI7zh6bfsri2Ez5FtCy2dCp1k62duXzfG7DLGGh7RU2QQXWwavT+u4qpvDF/TWOiiGTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.5", + "@rollup/rollup-android-arm64": "4.64.5", + "@rollup/rollup-darwin-arm64": "4.64.5", + "@rollup/rollup-darwin-x64": "4.64.5", + "@rollup/rollup-freebsd-arm64": "4.64.5", + "@rollup/rollup-freebsd-x64": "4.64.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.5", + "@rollup/rollup-linux-arm-musleabihf": "4.64.5", + "@rollup/rollup-linux-arm64-gnu": "4.64.5", + "@rollup/rollup-linux-arm64-musl": "4.64.5", + "@rollup/rollup-linux-loong64-gnu": "4.64.5", + "@rollup/rollup-linux-loong64-musl": "4.64.5", + "@rollup/rollup-linux-ppc64-gnu": "4.64.5", + "@rollup/rollup-linux-ppc64-musl": "4.64.5", + "@rollup/rollup-linux-riscv64-gnu": "4.64.5", + "@rollup/rollup-linux-riscv64-musl": "4.64.5", + "@rollup/rollup-linux-s390x-gnu": "4.64.5", + "@rollup/rollup-linux-x64-gnu": "4.64.5", + "@rollup/rollup-linux-x64-musl": "4.64.5", + "@rollup/rollup-openbsd-x64": "4.64.5", + "@rollup/rollup-openharmony-arm64": "4.64.5", + "@rollup/rollup-win32-arm64-msvc": "4.64.5", + "@rollup/rollup-win32-ia32-msvc": "4.64.5", + "@rollup/rollup-win32-x64-gnu": "4.64.5", + "@rollup/rollup-win32-x64-msvc": "4.64.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "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/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..721b90e --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "wordpress-remote-editor", + "private": true, + "version": "0.1.0", + "type": "module", + "license": "GPL-2.0-or-later", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "test": "vitest run", + "lint": "tsc --noEmit", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-os": "^2.3.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.8.0", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.20.5", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^5.0.0", + "jsdom": "^26.1.0", + "typescript": "^5.9.0", + "vite": "^7.1.0", + "vitest": "^3.2.0" + } +} diff --git a/plugin/remote-editor-bridge/assets/embedded.css b/plugin/remote-editor-bridge/assets/embedded.css new file mode 100644 index 0000000..2ec47bf --- /dev/null +++ b/plugin/remote-editor-bridge/assets/embedded.css @@ -0,0 +1,21 @@ +/* Hide wp-admin chrome around the block editor inside the desktop app. */ +html.wp-toolbar:has(body.wpre-embedded) { + padding-top: 0; +} + +body.wpre-embedded #wpadminbar, +body.wpre-embedded #adminmenumain, +body.wpre-embedded #wpfooter, +body.wpre-embedded .edit-post-fullscreen-mode-close, +body.wpre-embedded .editor-header__back-button { + display: none !important; +} + +body.wpre-embedded #wpcontent { + margin-left: 0; +} + +body.wpre-embedded .interface-interface-skeleton { + top: 0; + left: 0; +} diff --git a/plugin/remote-editor-bridge/composer.json b/plugin/remote-editor-bridge/composer.json new file mode 100644 index 0000000..645328b --- /dev/null +++ b/plugin/remote-editor-bridge/composer.json @@ -0,0 +1,21 @@ +{ + "name": "realact/remote-editor-bridge", + "description": "Companion plugin for the WordPress Remote Editor desktop app.", + "type": "wordpress-plugin", + "license": "GPL-2.0-or-later", + "require": { + "php": ">=7.4" + }, + "require-dev": { + "wp-coding-standards/wpcs": "^3.1", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "scripts": { + "lint": "phpcs" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/plugin/remote-editor-bridge/composer.lock b/plugin/remote-editor-bridge/composer.lock new file mode 100644 index 0000000..9682f87 --- /dev/null +++ b/plugin/remote-editor-bridge/composer.lock @@ -0,0 +1,437 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "1baf6cf078b38181c98f694d660844a7", + "packages": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-05-06T08:26:05+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/39467533fdb742446d68c1d10ac33d625ee0311c", + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.3", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-07-27T11:13:17+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/5f35d9408c54d7b529501f3c688b6eae562aea1f", + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-07-27T10:28:41+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.6", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-08-06T00:17:32+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "ec2ff942335f33683a5957a85d138753876a05cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/ec2ff942335f33683a5957a85d138753876a05cf", + "reference": "ec2ff942335f33683a5957a85d138753876a05cf", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.1", + "phpcsstandards/phpcsutils": "^1.2.3", + "squizlabs/php_codesniffer": "^3.13.5" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2026-07-27T11:53:23+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/plugin/remote-editor-bridge/includes/class-embedded-mode.php b/plugin/remote-editor-bridge/includes/class-embedded-mode.php new file mode 100644 index 0000000..4868d2d --- /dev/null +++ b/plugin/remote-editor-bridge/includes/class-embedded-mode.php @@ -0,0 +1,65 @@ +is_block_editor() ) { + return; + } + wp_enqueue_style( + 'remote-editor-bridge-embedded', + plugins_url( 'assets/embedded.css', __DIR__ ), + array(), + VERSION + ); + } +} diff --git a/plugin/remote-editor-bridge/includes/class-handoff.php b/plugin/remote-editor-bridge/includes/class-handoff.php new file mode 100644 index 0000000..8e6f54a --- /dev/null +++ b/plugin/remote-editor-bridge/includes/class-handoff.php @@ -0,0 +1,231 @@ + \WP_REST_Server::CREATABLE, + 'callback' => array( self::class, 'issue' ), + 'permission_callback' => array( self::class, 'can_issue' ), + 'args' => array( + 'redirect_to' => array( + 'type' => 'string', + 'required' => true, + 'format' => 'uri', + 'sanitize_callback' => 'esc_url_raw', + ), + ), + ) + ); + + register_rest_route( + REST_NAMESPACE, + '/info', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => static function () { + return rest_ensure_response( array( 'version' => VERSION ) ); + }, + 'permission_callback' => 'is_user_logged_in', + ) + ); + } + + /** + * Only requests authenticated with an Application Password, by users who + * can edit content, may mint handoff links. + * + * @return true|\WP_Error + */ + public static function can_issue() { + if ( ! is_user_logged_in() || ! rest_get_authenticated_app_password() ) { + return new \WP_Error( + 'wpre_app_password_required', + __( 'Sign-in handoff requires Application Password authentication.', 'remote-editor-bridge' ), + array( 'status' => 401 ) + ); + } + if ( ! current_user_can( 'edit_posts' ) && ! current_user_can( 'edit_pages' ) ) { + return new \WP_Error( + 'wpre_forbidden', + __( 'Your role cannot edit content on this site.', 'remote-editor-bridge' ), + array( 'status' => 403 ) + ); + } + return true; + } + + /** + * Restricts the post-login destination to the block editor screens. + * + * @param string $url Requested destination. + * @return string|null Safe destination, or null when not allowed. + */ + public static function safe_editor_url( $url ) { + $url = wp_validate_redirect( $url, '' ); + if ( '' === $url ) { + return null; + } + $admin = admin_url(); + if ( 0 !== strpos( $url, $admin . 'post.php' ) && 0 !== strpos( $url, $admin . 'post-new.php' ) ) { + return null; + } + return $url; + } + + /** + * Hashes a token for storage so a leaked cache entry can't be replayed. + * + * @param string $token Raw token. + * @return string + */ + private static function hash( $token ) { + return hash_hmac( 'sha256', $token, wp_salt( 'auth' ) ); + } + + /** + * REST callback: create a handoff link. + * + * @param \WP_REST_Request $request Request. + * @return \WP_REST_Response|\WP_Error + */ + public static function issue( \WP_REST_Request $request ) { + $user_id = get_current_user_id(); + $redirect = self::safe_editor_url( (string) $request->get_param( 'redirect_to' ) ); + if ( null === $redirect ) { + return new \WP_Error( + 'wpre_bad_redirect', + __( 'The editor address is not allowed.', 'remote-editor-bridge' ), + array( 'status' => 400 ) + ); + } + + $rate_key = self::RATE_TRANSIENT . $user_id; + $count = (int) get_transient( $rate_key ); + if ( $count >= self::MAX_PER_MINUTE ) { + return new \WP_Error( + 'wpre_rate_limited', + __( 'Too many sign-in requests. Please wait a minute.', 'remote-editor-bridge' ), + array( 'status' => 429 ) + ); + } + set_transient( $rate_key, $count + 1, MINUTE_IN_SECONDS ); + + $token = wp_generate_password( 48, false, false ); + set_transient( + self::TRANSIENT . self::hash( $token ), + array( + 'user_id' => $user_id, + 'redirect' => $redirect, + 'expires' => time() + self::TTL, + ), + self::TTL + ); + + $url = add_query_arg( + array( + 'action' => 'wpre_handoff', + 'token' => rawurlencode( $token ), + ), + wp_login_url() + ); + + $response = rest_ensure_response( array( 'url' => $url ) ); + $response->header( 'Cache-Control', 'no-store' ); + return $response; + } + + /** + * Runs on wp-login.php?action=wpre_handoff: verifies the token, signs the + * user in and redirects to the editor. + */ + public static function consume() { + nocache_headers(); + header( 'Referrer-Policy: no-referrer' ); + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The single-use token is the CSRF protection. + $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; + if ( ! preg_match( '/^[A-Za-z0-9]{48}$/', $token ) ) { + self::fail(); + } + + $key = self::TRANSIENT . self::hash( $token ); + $data = get_transient( $key ); + delete_transient( $key ); // Single use, even on failure below. + + if ( ! is_array( $data ) || empty( $data['user_id'] ) || (int) $data['expires'] < time() ) { + self::fail(); + } + + $user = get_user_by( 'id', (int) $data['user_id'] ); + if ( ! $user || ( ! user_can( $user, 'edit_posts' ) && ! user_can( $user, 'edit_pages' ) ) ) { + self::fail(); + } + + $redirect = self::safe_editor_url( $data['redirect'] ); + if ( null === $redirect ) { + $redirect = admin_url( 'post-new.php' ); + } + + wp_set_current_user( $user->ID ); + wp_set_auth_cookie( $user->ID, false, is_ssl() ); + /** This action is documented in wp-includes/user.php */ + do_action( 'wp_login', $user->user_login, $user ); + + setcookie( + self::EMBED_COOKIE, + '1', + array( + 'expires' => 0, + 'path' => ADMIN_COOKIE_PATH, + 'domain' => COOKIE_DOMAIN, + 'secure' => is_ssl(), + 'httponly' => true, + 'samesite' => 'Lax', + ) + ); + + wp_safe_redirect( $redirect ); + exit; + } + + /** + * Ends the request with a generic error. + */ + private static function fail() { + wp_die( + esc_html__( 'This sign-in link has expired or was already used. Return to WordPress Remote Editor and try again.', 'remote-editor-bridge' ), + esc_html__( 'Sign-in link expired', 'remote-editor-bridge' ), + array( 'response' => 403 ) + ); + } +} diff --git a/plugin/remote-editor-bridge/phpcs.xml.dist b/plugin/remote-editor-bridge/phpcs.xml.dist new file mode 100644 index 0000000..80b9c91 --- /dev/null +++ b/plugin/remote-editor-bridge/phpcs.xml.dist @@ -0,0 +1,24 @@ + + + WordPress Coding Standards for the companion plugin. + . + /vendor/* + + + + + + + + + + + + + + + + + remote-editor-bridge.php + + diff --git a/plugin/remote-editor-bridge/readme.txt b/plugin/remote-editor-bridge/readme.txt new file mode 100644 index 0000000..1bc6c18 --- /dev/null +++ b/plugin/remote-editor-bridge/readme.txt @@ -0,0 +1,34 @@ +=== Remote Editor Bridge === +Contributors: realact +Tags: block editor, gutenberg, desktop, application passwords, remote editing +Requires at least: 6.4 +Tested up to: 6.8 +Requires PHP: 7.4 +Stable tag: 0.1.0 +License: GPL-2.0-or-later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Companion plugin for the WordPress Remote Editor desktop app. + +== Description == + +WordPress Remote Editor is a desktop app for macOS, Windows and Linux that edits your site with its own block editor, so every block plugin you use keeps working. + +This optional plugin improves that experience: + +* **No second sign-in.** The app signs in to the REST API with an Application Password. This plugin lets it exchange that for a single-use, 60-second sign-in link when opening the editor. +* **Native look.** The wp-admin bar and menu are hidden inside the app so the editor blends in. + +The plugin adds no settings, stores nothing permanently, and changes nothing for normal wp-admin visits. + += Security = + +* Sign-in links can only be requested with Application Password authentication, by users who can edit posts or pages. +* Each link works once, expires after 60 seconds, is stored only as an HMAC hash, and can only redirect to the block editor screens. +* Requests are rate limited per user. +* The standard `wp_login` action fires, so audit and security plugins see the sign-in. + +== Changelog == + += 0.1.0 = +* First release. diff --git a/plugin/remote-editor-bridge/remote-editor-bridge.php b/plugin/remote-editor-bridge/remote-editor-bridge.php new file mode 100644 index 0000000..2c5b9f7 --- /dev/null +++ b/plugin/remote-editor-bridge/remote-editor-bridge.php @@ -0,0 +1,29 @@ +query( + $wpdb->prepare( + "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", + $wpdb->esc_like( '_transient_wpre_handoff_' ) . '%', + $wpdb->esc_like( '_transient_timeout_wpre_handoff_' ) . '%' + ) +); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..f1ea9ea --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5955 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.9", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.9", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "fastrand", + "hkdf", + "num", + "once_cell", + "sha2", + "zeroize", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.8+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_info" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" +dependencies = [ + "android_system_properties", + "log", + "nix 0.31.3", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.17+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.8", + "serde", + "sha2", + "zbus 4.4.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.3", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c404ecf956ff241bf6b39dd688614724e1bf1f63255abb4fc46d1c40371672ea" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.5", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f468f9a9bcd404f57e5c191d91024e3de953d4b610f78c2907e7110615cb1c" +dependencies = [ + "base64 0.23.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.18.1", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131bffec939b20642380317f8403a33c8888489925e14bb965ceaff0d949b1cb" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1140cf34a3b3b836a13103dcab17f18831d5cc3534cbd435dc01a5c6daa65aa2" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca325d6e66e9170f2e1b9631722e4e544bcbc51781575d266abdaba697bd51" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213fae2d85d4b76b06de5915d1862db9fa74541171f9d2c15da6ac9bd6e788c6" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85dead674ee41937d042ee855d785f07ee67206f19402200c12b9255964d5ac8" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", + "url", + "windows", + "zbus 5.19.0", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd69d272ab772b30462779de12a40f837616831c794c71531bea91474e1061f" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c142ed88deee222bd2d979269d35c73b6c1c0f6ebd5b79b4ff80066fcad6af1" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.21", + "tokio", + "tracing", + "windows-sys 0.61.2", + "zbus 5.19.0", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.21", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wordpress-remote-editor" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "gtk", + "keyring", + "log", + "rand 0.8.8", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-os", + "tauri-plugin-single-instance", + "thiserror 2.0.21", + "tokio", + "url", + "uuid", + "zeroize", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.8", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros 5.19.0", + "zbus_names 4.3.4", + "zvariant 5.15.0", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.9", + "zbus_names 4.3.4", + "zvariant 5.15.0", + "zvariant_utils 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant 5.15.0", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive 5.15.0", + "zvariant_utils 4.2.0", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.9", + "zvariant_utils 4.2.0", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.9", + "winnow 1.0.4", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..6ec9fb9 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "wordpress-remote-editor" +version = "0.1.0" +description = "A native desktop editor for WordPress sites" +authors = ["WordPress Remote Editor contributors"] +license = "GPL-2.0-or-later" +edition = "2021" +rust-version = "1.80" + +[lib] +name = "wpre_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["unstable", "macos-private-api"] } +tauri-plugin-opener = "2" +tauri-plugin-os = "2" +tauri-plugin-dialog = "2" +tauri-plugin-single-instance = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots", "http2", "charset"] } +url = { version = "2", features = ["serde"] } +uuid = { version = "1", features = ["v4", "serde"] } +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service", "crypto-rust"] } +zeroize = { version = "1", features = ["derive"] } +thiserror = "2" +tokio = { version = "1", features = ["sync", "macros", "rt-multi-thread", "time"] } +base64 = "0.22" +rand = "0.8" +log = "0.4" +chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true + +[target.'cfg(target_os = "linux")'.dependencies] +gtk = "0.18" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..8931257 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,23 @@ +fn main() { + // Every app command must be declared here. Tauri then generates an + // `allow-` permission for each one, and a webview can only call + // commands that a capability file explicitly grants it. + tauri_build::try_build(tauri_build::Attributes::new().app_manifest( + tauri_build::AppManifest::new().commands(&[ + "discover_site", + "begin_authorization", + "add_site_manual", + "list_sites", + "remove_site", + "get_site_context", + "list_posts", + "trash_post", + "open_editor", + "close_editor", + "editor_set_bounds", + "editor_command", + "editor_report", + ]), + )) + .expect("failed to run tauri-build"); +} diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json new file mode 100644 index 0000000..ec69700 --- /dev/null +++ b/src-tauri/capabilities/main.json @@ -0,0 +1,31 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main", + "description": "The bundled shell UI webview only (not the editor child webview in the same window). It never receives credentials and cannot call editor_report.", + "webviews": ["main"], + "local": true, + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "os:default", + "dialog:allow-ask", + "dialog:allow-message", + "dialog:allow-confirm", + { + "identifier": "opener:allow-open-url", + "allow": [{ "url": "https://*" }, { "url": "http://localhost*" }] + }, + "allow-discover-site", + "allow-begin-authorization", + "allow-add-site-manual", + "allow-list-sites", + "allow-remove-site", + "allow-get-site-context", + "allow-list-posts", + "allow-trash-post", + "allow-open-editor", + "allow-close-editor", + "allow-editor-set-bounds", + "allow-editor-command" + ] +} diff --git a/src-tauri/entitlements.plist b/src-tauri/entitlements.plist new file mode 100644 index 0000000..050df07 --- /dev/null +++ b/src-tauri/entitlements.plist @@ -0,0 +1,11 @@ + + + + + + com.apple.security.network.client + + com.apple.security.cs.allow-jit + + + diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..9dab72f Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..9a4d46c Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..25b03d6 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..f13e109 Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..17cc33e Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..9a66b49 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..8dec701 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..398cd21 Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..ed3cf4a Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..89467f9 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..2cab6f8 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..7e15eb1 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..83e6ad2 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..af4e3f4 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..b3549bd Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..b1cb871 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..a9398a2 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/permissions/autogenerated/add_site_manual.toml b/src-tauri/permissions/autogenerated/add_site_manual.toml new file mode 100644 index 0000000..0a7ce57 --- /dev/null +++ b/src-tauri/permissions/autogenerated/add_site_manual.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-add-site-manual" +description = "Enables the add_site_manual command without any pre-configured scope." +commands.allow = ["add_site_manual"] + +[[permission]] +identifier = "deny-add-site-manual" +description = "Denies the add_site_manual command without any pre-configured scope." +commands.deny = ["add_site_manual"] diff --git a/src-tauri/permissions/autogenerated/begin_authorization.toml b/src-tauri/permissions/autogenerated/begin_authorization.toml new file mode 100644 index 0000000..7ee6b95 --- /dev/null +++ b/src-tauri/permissions/autogenerated/begin_authorization.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-begin-authorization" +description = "Enables the begin_authorization command without any pre-configured scope." +commands.allow = ["begin_authorization"] + +[[permission]] +identifier = "deny-begin-authorization" +description = "Denies the begin_authorization command without any pre-configured scope." +commands.deny = ["begin_authorization"] diff --git a/src-tauri/permissions/autogenerated/close_editor.toml b/src-tauri/permissions/autogenerated/close_editor.toml new file mode 100644 index 0000000..6e3f870 --- /dev/null +++ b/src-tauri/permissions/autogenerated/close_editor.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-close-editor" +description = "Enables the close_editor command without any pre-configured scope." +commands.allow = ["close_editor"] + +[[permission]] +identifier = "deny-close-editor" +description = "Denies the close_editor command without any pre-configured scope." +commands.deny = ["close_editor"] diff --git a/src-tauri/permissions/autogenerated/discover_site.toml b/src-tauri/permissions/autogenerated/discover_site.toml new file mode 100644 index 0000000..a31bfa4 --- /dev/null +++ b/src-tauri/permissions/autogenerated/discover_site.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-discover-site" +description = "Enables the discover_site command without any pre-configured scope." +commands.allow = ["discover_site"] + +[[permission]] +identifier = "deny-discover-site" +description = "Denies the discover_site command without any pre-configured scope." +commands.deny = ["discover_site"] diff --git a/src-tauri/permissions/autogenerated/editor_command.toml b/src-tauri/permissions/autogenerated/editor_command.toml new file mode 100644 index 0000000..e73d594 --- /dev/null +++ b/src-tauri/permissions/autogenerated/editor_command.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-editor-command" +description = "Enables the editor_command command without any pre-configured scope." +commands.allow = ["editor_command"] + +[[permission]] +identifier = "deny-editor-command" +description = "Denies the editor_command command without any pre-configured scope." +commands.deny = ["editor_command"] diff --git a/src-tauri/permissions/autogenerated/editor_report.toml b/src-tauri/permissions/autogenerated/editor_report.toml new file mode 100644 index 0000000..1893fc6 --- /dev/null +++ b/src-tauri/permissions/autogenerated/editor_report.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-editor-report" +description = "Enables the editor_report command without any pre-configured scope." +commands.allow = ["editor_report"] + +[[permission]] +identifier = "deny-editor-report" +description = "Denies the editor_report command without any pre-configured scope." +commands.deny = ["editor_report"] diff --git a/src-tauri/permissions/autogenerated/editor_set_bounds.toml b/src-tauri/permissions/autogenerated/editor_set_bounds.toml new file mode 100644 index 0000000..370fa99 --- /dev/null +++ b/src-tauri/permissions/autogenerated/editor_set_bounds.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-editor-set-bounds" +description = "Enables the editor_set_bounds command without any pre-configured scope." +commands.allow = ["editor_set_bounds"] + +[[permission]] +identifier = "deny-editor-set-bounds" +description = "Denies the editor_set_bounds command without any pre-configured scope." +commands.deny = ["editor_set_bounds"] diff --git a/src-tauri/permissions/autogenerated/get_site_context.toml b/src-tauri/permissions/autogenerated/get_site_context.toml new file mode 100644 index 0000000..3b20d67 --- /dev/null +++ b/src-tauri/permissions/autogenerated/get_site_context.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-get-site-context" +description = "Enables the get_site_context command without any pre-configured scope." +commands.allow = ["get_site_context"] + +[[permission]] +identifier = "deny-get-site-context" +description = "Denies the get_site_context command without any pre-configured scope." +commands.deny = ["get_site_context"] diff --git a/src-tauri/permissions/autogenerated/list_posts.toml b/src-tauri/permissions/autogenerated/list_posts.toml new file mode 100644 index 0000000..f8f8b8d --- /dev/null +++ b/src-tauri/permissions/autogenerated/list_posts.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-list-posts" +description = "Enables the list_posts command without any pre-configured scope." +commands.allow = ["list_posts"] + +[[permission]] +identifier = "deny-list-posts" +description = "Denies the list_posts command without any pre-configured scope." +commands.deny = ["list_posts"] diff --git a/src-tauri/permissions/autogenerated/list_sites.toml b/src-tauri/permissions/autogenerated/list_sites.toml new file mode 100644 index 0000000..c6c588a --- /dev/null +++ b/src-tauri/permissions/autogenerated/list_sites.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-list-sites" +description = "Enables the list_sites command without any pre-configured scope." +commands.allow = ["list_sites"] + +[[permission]] +identifier = "deny-list-sites" +description = "Denies the list_sites command without any pre-configured scope." +commands.deny = ["list_sites"] diff --git a/src-tauri/permissions/autogenerated/open_editor.toml b/src-tauri/permissions/autogenerated/open_editor.toml new file mode 100644 index 0000000..3606729 --- /dev/null +++ b/src-tauri/permissions/autogenerated/open_editor.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-open-editor" +description = "Enables the open_editor command without any pre-configured scope." +commands.allow = ["open_editor"] + +[[permission]] +identifier = "deny-open-editor" +description = "Denies the open_editor command without any pre-configured scope." +commands.deny = ["open_editor"] diff --git a/src-tauri/permissions/autogenerated/remove_site.toml b/src-tauri/permissions/autogenerated/remove_site.toml new file mode 100644 index 0000000..cff72f4 --- /dev/null +++ b/src-tauri/permissions/autogenerated/remove_site.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-remove-site" +description = "Enables the remove_site command without any pre-configured scope." +commands.allow = ["remove_site"] + +[[permission]] +identifier = "deny-remove-site" +description = "Denies the remove_site command without any pre-configured scope." +commands.deny = ["remove_site"] diff --git a/src-tauri/permissions/autogenerated/trash_post.toml b/src-tauri/permissions/autogenerated/trash_post.toml new file mode 100644 index 0000000..ed78ce2 --- /dev/null +++ b/src-tauri/permissions/autogenerated/trash_post.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-trash-post" +description = "Enables the trash_post command without any pre-configured scope." +commands.allow = ["trash_post"] + +[[permission]] +identifier = "deny-trash-post" +description = "Denies the trash_post command without any pre-configured scope." +commands.deny = ["trash_post"] diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs new file mode 100644 index 0000000..2db19a2 --- /dev/null +++ b/src-tauri/src/auth.rs @@ -0,0 +1,308 @@ +//! Application Password authorization. +//! +//! WordPress's own `authorize-application.php` screen is opened in an +//! isolated, incognito window with no IPC access. The user signs in with +//! their normal login (2FA/SSO keep working) and approves the app. WordPress +//! then redirects to `wpremoteeditor://auth/callback?...`, which we intercept +//! inside our own webview, so no other app can claim the scheme. + +use crate::error::{AppError, AppResult}; +use crate::keychain::{self, Credentials}; +use crate::sites::SiteRecord; +use crate::wp::client::WpClient; +use crate::wp::models::{RestUser, SiteDiscovery, SiteSummary}; +use crate::AppState; +use rand::RngCore; +use serde::Deserialize; +use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; +use url::Url; +use uuid::Uuid; +use zeroize::Zeroizing; + +pub const CALLBACK_SCHEME: &str = "wpremoteeditor"; +/// Stable app ID so WordPress groups all passwords created by this app. +pub const APP_ID: &str = "8f4d7f3e-6b0e-4c1a-9a58-2f0b8f7f9b21"; +pub const APP_NAME: &str = "WordPress Remote Editor"; +const AUTH_WINDOW: &str = "auth"; + +pub struct PendingAuth { + pub state: String, + pub discovery: SiteDiscovery, + pub allow_insecure_local: bool, +} + +fn random_state() -> String { + let mut bytes = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut bytes); + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +fn constant_time_eq(a: &str, b: &str) -> bool { + let (a, b) = (a.as_bytes(), b.as_bytes()); + a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0 +} + +/// WordPress rejects a second Application Password with the same name, so +/// name each one after this device and the time it was created. That also +/// tells the user which computer a password belongs to on their profile page. +pub fn password_name(device: &str, now: chrono::DateTime) -> String { + let device: String = device + .chars() + .filter(|c| !c.is_control()) + .take(40) + .collect(); + let stamp = now.format("%Y-%m-%d %H:%M"); + if device.trim().is_empty() { + format!("{APP_NAME} ({stamp})") + } else { + format!("{APP_NAME} ({}, {stamp})", device.trim()) + } +} + +pub fn authorize_url(endpoint: &Url, state: &str, name: &str) -> Url { + let mut url = endpoint.clone(); + url.query_pairs_mut() + .append_pair("app_name", name) + .append_pair("app_id", APP_ID) + .append_pair( + "success_url", + &format!("{CALLBACK_SCHEME}://auth/callback?state={state}"), + ) + .append_pair( + "reject_url", + &format!("{CALLBACK_SCHEME}://auth/rejected?state={state}"), + ); + url +} + +/// Opens the authorization window. Completion is reported to the shell via +/// the `auth://completed` or `auth://failed` events. +pub fn begin( + app: &AppHandle, + discovery: SiteDiscovery, + allow_insecure_local: bool, +) -> AppResult<()> { + let endpoint = discovery + .authorization_endpoint + .clone() + .ok_or(AppError::ApplicationPasswordsUnavailable)?; + let state = random_state(); + let name = password_name(&tauri_plugin_os::hostname(), chrono::Local::now()); + let url = authorize_url(&endpoint, &state, &name); + *app.state::() + .pending_auth + .lock() + .expect("poisoned") = Some(PendingAuth { + state, + discovery: discovery.clone(), + allow_insecure_local, + }); + + // Reuse an open sign-in window (closing is asynchronous, so its label + // can't be reused immediately). Its navigation guard is site-independent. + if let Some(existing) = app.get_webview_window(AUTH_WINDOW) { + existing.set_title(&format!("Sign in to {}", discovery.name))?; + existing.navigate(url)?; + existing.unminimize().ok(); + existing.set_focus().ok(); + return Ok(()); + } + + let handle = app.clone(); + let window = WebviewWindowBuilder::new(app, AUTH_WINDOW, WebviewUrl::External(url)) + .title(format!("Sign in to {}", discovery.name)) + .inner_size(520.0, 720.0) + .resizable(true) + .center() + // Fresh, throwaway cookie jar: nothing from this sign-in persists. + .incognito(true) + .on_navigation(move |nav| { + if nav.scheme() == CALLBACK_SCHEME { + let handle = handle.clone(); + let nav = nav.clone(); + tauri::async_runtime::spawn(async move { + complete(&handle, nav).await; + }); + return false; + } + // SSO providers may live on other hosts, but never allow + // downgrades or non-web schemes in the sign-in window. + nav.scheme() == "https" + || (allow_insecure_local && nav.scheme() == "http") + || nav.scheme() == "about" + }) + .build()?; + // The app menu is attached to every window on Windows and Linux; the + // sign-in window shouldn't offer editor commands. + #[cfg(not(target_os = "macos"))] + window.remove_menu().ok(); + #[cfg(target_os = "macos")] + let _ = window; + Ok(()) +} + +#[derive(Deserialize)] +struct Introspection { + uuid: String, +} + +async fn complete(app: &AppHandle, callback: Url) { + let result = handle_callback(app, &callback).await; + if let Some(w) = app.get_webview_window(AUTH_WINDOW) { + w.close().ok(); + } + match result { + Ok(summary) => { + app.emit_to("main", "auth://completed", summary).ok(); + } + Err(e) => { + app.emit_to("main", "auth://failed", e.to_string()).ok(); + } + } +} + +async fn handle_callback(app: &AppHandle, callback: &Url) -> AppResult { + let pending = app + .state::() + .pending_auth + .lock() + .expect("poisoned") + .take() + .ok_or(AppError::AuthFlow)?; + + let mut state = None; + let mut user_login = None; + let mut password = Zeroizing::new(String::new()); + for (k, v) in callback.query_pairs() { + match k.as_ref() { + "state" => state = Some(v.into_owned()), + "user_login" => user_login = Some(v.into_owned()), + "password" => password = Zeroizing::new(v.into_owned()), + _ => {} + } + } + if !state.is_some_and(|s| constant_time_eq(&s, &pending.state)) { + return Err(AppError::AuthFlow); + } + if callback.host_str() != Some("auth") || callback.path() != "/callback" { + return Err(AppError::AuthFlow); + } + let user_login = user_login.ok_or(AppError::AuthFlow)?; + if password.is_empty() { + return Err(AppError::AuthFlow); + } + + let creds = Credentials::new(&user_login, &password); + let record = finalize_site(app, pending.discovery, creds, pending.allow_insecure_local).await?; + Ok(record.summary()) +} + +/// Verifies credentials against the site, then stores the password in the +/// keychain and the site metadata on disk. +pub async fn finalize_site( + app: &AppHandle, + discovery: SiteDiscovery, + creds: Credentials, + allow_insecure_local: bool, +) -> AppResult { + let client = WpClient::new(discovery.api_root.clone(), &creds, allow_insecure_local)?; + let (me, _) = client + .get::("wp/v2/users/me", &[("context", "edit".into())]) + .await?; + let app_password_uuid = client + .get::("wp/v2/users/me/application-passwords/introspect", &[]) + .await + .ok() + .map(|(i, _)| i.uuid); + + let avatar_url = ["96", "48", "24"] + .iter() + .find_map(|k| me.avatar_urls.get(*k)) + .and_then(|u| Url::parse(u).ok()) + .filter(|u| u.scheme() == "https"); + + let store = &app.state::().store; + let record = store.upsert(SiteRecord { + id: Uuid::new_v4(), + name: discovery.name, + url: discovery.site_url, + admin_url: discovery.admin_url, + api_root: discovery.api_root, + username: creds.username.clone(), + display_name: if me.name.is_empty() { + creds.username.clone() + } else { + me.name + }, + avatar_url, + app_password_uuid, + bridge_available: discovery.bridge_available, + allow_insecure_local, + added_at: chrono::Utc::now(), + })?; + if let Err(e) = keychain::save(&record.id, &creds) { + store.remove(&record.id).ok(); + return Err(e); + } + Ok(record) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_authorize_url_with_state() { + let ep = Url::parse("https://example.com/wp-admin/authorize-application.php").unwrap(); + let u = authorize_url( + &ep, + "abc", + "WordPress Remote Editor (laptop, 2026-10-13 09:00)", + ); + let pairs: std::collections::HashMap<_, _> = u.query_pairs().into_owned().collect(); + assert_eq!( + pairs["app_name"], + "WordPress Remote Editor (laptop, 2026-10-13 09:00)" + ); + assert_eq!(pairs["app_id"], APP_ID); + assert_eq!( + pairs["success_url"], + "wpremoteeditor://auth/callback?state=abc" + ); + assert_eq!( + pairs["reject_url"], + "wpremoteeditor://auth/rejected?state=abc" + ); + } + + #[test] + fn password_names_are_unique_per_device_and_time() { + use chrono::TimeZone; + let t = chrono::Local + .with_ymd_and_hms(2026, 10, 13, 9, 5, 0) + .unwrap(); + assert_eq!( + password_name("eli-mbp", t), + "WordPress Remote Editor (eli-mbp, 2026-10-13 09:05)" + ); + assert_eq!( + password_name(" ", t), + "WordPress Remote Editor (2026-10-13 09:05)" + ); + assert!(!password_name("bad\nname", t).contains('\n')); + } + + #[test] + fn state_is_random_and_long() { + let (a, b) = (random_state(), random_state()); + assert_eq!(a.len(), 64); + assert_ne!(a, b); + } + + #[test] + fn constant_time_eq_works() { + assert!(constant_time_eq("abc", "abc")); + assert!(!constant_time_eq("abc", "abd")); + assert!(!constant_time_eq("abc", "abcd")); + } +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..622d45d --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,256 @@ +//! IPC commands for the shell webview. Each must also be listed in build.rs +//! and granted in a capability file. + +use crate::auth; +use crate::editor::{self, Bounds, EditorCommand, EditorReport}; +use crate::error::{AppError, AppResult}; +use crate::keychain::{self, Credentials}; +use crate::wp::client::{header_u64, WpClient}; +use crate::wp::discovery; +use crate::wp::models::*; +use crate::AppState; +use std::collections::HashMap; +use tauri::{AppHandle, Manager, State, Webview}; +use uuid::Uuid; + +fn client_for(state: &AppState, site_id: &Uuid) -> AppResult<(crate::sites::SiteRecord, WpClient)> { + let site = state.store.get(site_id)?; + let creds = keychain::load(site_id)?; + let client = WpClient::new(site.api_root.clone(), &creds, site.allow_insecure_local)?; + Ok((site, client)) +} + +fn valid_rest_base(s: &str) -> AppResult<&str> { + if !s.is_empty() + && s.len() <= 64 + && s.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + Ok(s) + } else { + Err(AppError::Internal("invalid content type".into())) + } +} + +#[tauri::command] +pub async fn discover_site(url: String, allow_insecure_local: bool) -> AppResult { + discovery::discover(&url, allow_insecure_local).await +} + +#[tauri::command] +pub async fn begin_authorization( + app: AppHandle, + url: String, + allow_insecure_local: bool, +) -> AppResult<()> { + let d = discovery::discover(&url, allow_insecure_local).await?; + auth::begin(&app, d, allow_insecure_local) +} + +#[tauri::command] +pub async fn add_site_manual( + app: AppHandle, + url: String, + username: String, + app_password: String, + allow_insecure_local: bool, +) -> AppResult { + let app_password = zeroize::Zeroizing::new(app_password); + if username.trim().is_empty() || app_password.trim().is_empty() { + return Err(AppError::Unauthorized); + } + let d = discovery::discover(&url, allow_insecure_local).await?; + let creds = Credentials::new(&username, &app_password); + Ok(auth::finalize_site(&app, d, creds, allow_insecure_local) + .await? + .summary()) +} + +#[tauri::command] +pub fn list_sites(state: State<'_, AppState>) -> Vec { + state.store.list().iter().map(|s| s.summary()).collect() +} + +#[tauri::command] +pub async fn remove_site(app: AppHandle, site_id: Uuid) -> AppResult<()> { + let state = app.state::(); + let site = state.store.get(&site_id)?; + // Best effort: revoke the Application Password on the server. + if let (Some(uuid), Ok((_, client))) = (&site.app_password_uuid, client_for(&state, &site_id)) { + if uuid.chars().all(|c| c.is_ascii_hexdigit() || c == '-') { + if let Err(e) = client + .delete(&format!("wp/v2/users/me/application-passwords/{uuid}"), &[]) + .await + { + log::warn!("couldn't revoke application password: {e}"); + } + } + } + if state + .editor + .lock() + .expect("poisoned") + .as_ref() + .map(|s| s.site_id) + == Some(site_id) + { + editor::close(&app)?; + } + keychain::delete(&site_id)?; + state.store.remove(&site_id)?; + editor::clear_site_data(&app, &site_id); + Ok(()) +} + +#[tauri::command] +pub async fn get_site_context(state: State<'_, AppState>, site_id: Uuid) -> AppResult { + let (site, client) = client_for(&state, &site_id)?; + let (me, _) = client + .get::("wp/v2/users/me", &[("context", "edit".into())]) + .await?; + let (types, _) = client + .get::>("wp/v2/types", &[("context", "edit".into())]) + .await?; + let mut post_types: Vec = types + .values() + .filter(|t| t.viewable && t.slug != "attachment") + .filter(|t| t.rest_namespace.as_deref().unwrap_or("wp/v2") == "wp/v2") + .map(|t| post_type_access(t, &me.capabilities)) + .filter(|a| a.can_create || me.capabilities.get("edit_posts").copied().unwrap_or(false)) + .collect(); + // Posts, then Pages, then everything else alphabetically. + post_types.sort_by_key(|t| match t.slug.as_str() { + "post" => (0, String::new()), + "page" => (1, String::new()), + _ => (2, t.name.to_lowercase()), + }); + Ok(SiteContext { + site: site.summary(), + user_id: me.id, + roles: me.roles, + post_types, + }) +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListPostsQuery { + site_id: Uuid, + rest_base: String, + status: Option, + search: Option, + page: Option, + mine_only: bool, + can_edit_others: bool, + user_id: u64, +} + +#[tauri::command] +pub async fn list_posts(state: State<'_, AppState>, query: ListPostsQuery) -> AppResult { + let ListPostsQuery { + site_id, + rest_base, + status, + search, + page, + mine_only, + can_edit_others, + user_id, + } = query; + let (_, client) = client_for(&state, &site_id)?; + let rest_base = valid_rest_base(&rest_base)?; + let page = page.unwrap_or(1).clamp(1, 10_000); + let status = match status.as_deref() { + Some(s @ ("publish" | "draft" | "pending" | "future" | "private" | "trash")) => { + s.to_string() + } + _ => "any".to_string(), + }; + let mut q: Vec<(&str, String)> = vec![ + ("context", "edit".into()), + ("per_page", "30".into()), + ("page", page.to_string()), + ("status", status), + ("orderby", "modified".into()), + ("order", "desc".into()), + ("_fields", "id,title,status,modified,author,link".into()), + ]; + if let Some(s) = search.filter(|s| !s.trim().is_empty()) { + q.push(("search", s.chars().take(200).collect())); + } + if mine_only { + q.push(("author", user_id.to_string())); + } + let (posts, headers) = client + .get::>(&format!("wp/v2/{rest_base}"), &q) + .await?; + let items = posts + .into_iter() + .map(|p| PostSummary { + can_edit: can_edit_others || p.author == Some(user_id), + title: p + .title + .and_then(|t| t.raw.or(t.rendered)) + .unwrap_or_default(), + id: p.id, + status: p.status, + modified: p.modified, + author: p.author, + link: p + .link + .filter(|l| l.starts_with("https://") || l.starts_with("http://")), + }) + .collect(); + Ok(PostPage { + items, + total: header_u64(&headers, "x-wp-total"), + total_pages: header_u64(&headers, "x-wp-totalpages"), + page, + }) +} + +#[tauri::command] +pub async fn trash_post( + state: State<'_, AppState>, + site_id: Uuid, + rest_base: String, + post_id: u64, +) -> AppResult<()> { + let (_, client) = client_for(&state, &site_id)?; + let rest_base = valid_rest_base(&rest_base)?; + // Without `force`, WordPress moves the post to the trash (recoverable). + client + .delete(&format!("wp/v2/{rest_base}/{post_id}"), &[]) + .await +} + +#[tauri::command] +pub async fn open_editor( + app: AppHandle, + site_id: Uuid, + post_type: String, + post_id: Option, + bounds: Bounds, +) -> AppResult<()> { + editor::open(&app, site_id, &post_type, post_id, bounds).await +} + +#[tauri::command] +pub fn close_editor(app: AppHandle) -> AppResult<()> { + editor::close(&app) +} + +#[tauri::command] +pub fn editor_set_bounds(app: AppHandle, bounds: Bounds) -> AppResult<()> { + editor::set_bounds(&app, bounds) +} + +#[tauri::command] +pub fn editor_command(app: AppHandle, command: EditorCommand) -> AppResult<()> { + editor::command(&app, command) +} + +#[tauri::command] +pub fn editor_report(app: AppHandle, webview: Webview, report: EditorReport) -> AppResult<()> { + editor::report(&app, &webview, report) +} diff --git a/src-tauri/src/editor.rs b/src-tauri/src/editor.rs new file mode 100644 index 0000000..08c4faa --- /dev/null +++ b/src-tauri/src/editor.rs @@ -0,0 +1,452 @@ +//! The editor pane: a child webview that loads the site's own block editor, +//! so every block, sidebar plugin and meta box works exactly as in wp-admin. +//! +//! The webview is treated as untrusted remote content: +//! - navigation is locked to the site's origins; other links open in the +//! system browser; +//! - each site gets its own cookie store; +//! - it may call exactly one app command, `editor_report`, granted at runtime +//! for that site's origin only; +//! - the core controls it by evaluating fixed scripts, never page-supplied code. + +use crate::error::{AppError, AppResult}; +use crate::keychain; +use crate::security::same_origin; +use crate::sites::SiteRecord; +use crate::wp::client::WpClient; +use crate::AppState; +use serde::{Deserialize, Serialize}; +use tauri::ipc::CapabilityBuilder; +use tauri::webview::WebviewBuilder; +use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Webview, WebviewUrl}; +use tauri_plugin_opener::OpenerExt; +use url::Url; +use uuid::Uuid; + +pub const EDITOR_LABEL: &str = "editor"; +const BRIDGE_JS: &str = include_str!("editor_bridge.js"); + +#[derive(Debug, Clone, Copy, Deserialize)] +pub struct Bounds { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +impl Bounds { + fn sane(self) -> Self { + let clamp = |v: f64| { + if v.is_finite() { + v.clamp(0.0, 20_000.0) + } else { + 0.0 + } + }; + Self { + x: clamp(self.x), + y: clamp(self.y), + width: clamp(self.width).max(1.0), + height: clamp(self.height).max(1.0), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum EditorCommand { + Save, + Publish, + Undo, + Redo, + ToggleSettings, + DeviceDesktop, + DeviceTablet, + DeviceMobile, +} + +impl EditorCommand { + fn script(self) -> &'static str { + match self { + Self::Save => "window.__wpre && window.__wpre.save()", + Self::Publish => "window.__wpre && window.__wpre.publish()", + Self::Undo => "window.__wpre && window.__wpre.undo()", + Self::Redo => "window.__wpre && window.__wpre.redo()", + Self::ToggleSettings => "window.__wpre && window.__wpre.toggleSettings()", + Self::DeviceDesktop => "window.__wpre && window.__wpre.device('Desktop')", + Self::DeviceTablet => "window.__wpre && window.__wpre.device('Tablet')", + Self::DeviceMobile => "window.__wpre && window.__wpre.device('Mobile')", + } + } +} + +/// State reported by the bridge script. Every field is bounded; anything +/// unexpected is rejected by serde. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EditorReport { + pub kind: String, + #[serde(default)] + pub post_id: Option, + #[serde(default)] + pub post_type: Option, + #[serde(default)] + pub title: Option, + #[serde(default)] + pub status: Option, + #[serde(default)] + pub link: Option, + #[serde(default)] + pub dirty: Option, + #[serde(default)] + pub saving: Option, + #[serde(default)] + pub can_publish: Option, + #[serde(default)] + pub path: Option, +} + +impl EditorReport { + pub fn validate(mut self) -> AppResult { + if !matches!(self.kind.as_str(), "editor" | "login" | "page") { + return Err(AppError::Internal("invalid editor report".into())); + } + let cap = |s: &mut Option, n: usize| { + if let Some(v) = s { + if v.chars().count() > n { + *v = v.chars().take(n).collect(); + } + } + }; + cap(&mut self.post_type, 64); + cap(&mut self.title, 500); + cap(&mut self.status, 32); + cap(&mut self.path, 512); + // Only pass through web links; the shell may offer "View on site". + if let Some(l) = &self.link { + if l.len() > 2048 || !(l.starts_with("https://") || l.starts_with("http://")) { + self.link = None; + } + } + Ok(self) + } +} + +pub struct EditorSession { + pub site_id: Uuid, +} + +fn origin_pattern(u: &Url) -> String { + format!("{}/*", u.origin().ascii_serialization()) +} + +fn editor_target(site: &SiteRecord, post_type: &str, post_id: Option) -> AppResult { + if !post_type + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + || post_type.len() > 20 + { + return Err(AppError::Internal("invalid post type".into())); + } + let mut url = match post_id { + Some(id) => { + let mut u = site + .admin_url + .join("post.php") + .map_err(|_| AppError::InvalidUrl)?; + u.query_pairs_mut() + .append_pair("post", &id.to_string()) + .append_pair("action", "edit"); + u + } + None => { + let mut u = site + .admin_url + .join("post-new.php") + .map_err(|_| AppError::InvalidUrl)?; + if post_type != "post" { + u.query_pairs_mut().append_pair("post_type", post_type); + } + u + } + }; + url.set_fragment(None); + Ok(url) +} + +#[derive(Deserialize)] +struct Handoff { + url: String, +} + +/// With the companion plugin active, trade the Application Password for a +/// single-use, short-lived login link so the user isn't asked to sign in again. +async fn handoff_url(site: &SiteRecord, target: &Url) -> AppResult { + let creds = keychain::load(&site.id)?; + let client = WpClient::new(site.api_root.clone(), &creds, site.allow_insecure_local)?; + let h: Handoff = client + .post( + "wpre/v1/handoff", + &serde_json::json!({ "redirect_to": target.as_str() }), + ) + .await?; + let url = Url::parse(&h.url).map_err(|_| AppError::BadResponse)?; + if !same_origin(&site.admin_url, &url) { + return Err(AppError::BadResponse); + } + Ok(url) +} + +pub async fn open( + app: &AppHandle, + site_id: Uuid, + post_type: &str, + post_id: Option, + bounds: Bounds, +) -> AppResult<()> { + let state = app.state::(); + let site = state.store.get(&site_id)?; + let target = editor_target(&site, post_type, post_id)?; + let initial = if site.bridge_available { + handoff_url(&site, &target).await.unwrap_or_else(|e| { + log::warn!("editor handoff unavailable, falling back to site login: {e}"); + target.clone() + }) + } else { + target + }; + + // Reuse the existing pane for the same site; recreate it for another site + // so cookie stores never mix. + let same_site = state + .editor + .lock() + .expect("poisoned") + .as_ref() + .map(|s| s.site_id) + == Some(site_id); + if let Some(existing) = app.get_webview(EDITOR_LABEL) { + if same_site { + existing.navigate(initial)?; + set_bounds(app, bounds)?; + return Ok(()); + } + existing.close()?; + } + + let allowed: Vec = vec![site.admin_url.clone(), site.url.clone()]; + let origins: Vec = allowed + .iter() + .map(|u| u.origin().ascii_serialization()) + .collect(); + let config = serde_json::json!({ "origins": origins }).to_string(); + let script = BRIDGE_JS.replace("__WPRE_CONFIG__", &config); + + // Grant the remote page exactly one command, only on this site's origins. + let mut cap = CapabilityBuilder::new(format!("editor-remote-{site_id}")) + .webview(EDITOR_LABEL) + .permission("allow-editor-report"); + for u in &allowed { + cap = cap.remote(origin_pattern(u)); + } + app.add_capability(cap)?; + + let handle = app.clone(); + let nav_allowed = allowed.clone(); + let mut builder = WebviewBuilder::new(EDITOR_LABEL, WebviewUrl::External(initial)) + .initialization_script(&script) + .on_navigation(move |url| { + if matches!(url.scheme(), "about" | "blob" | "data") { + return true; + } + if nav_allowed.iter().any(|a| same_origin(a, url)) { + return true; + } + if matches!(url.scheme(), "https" | "http" | "mailto") { + handle.opener().open_url(url.as_str(), None::<&str>).ok(); + } + false + }); + + #[cfg(target_os = "macos")] + { + builder = builder.data_store_identifier(*site_id.as_bytes()); + } + #[cfg(not(target_os = "macos"))] + { + let dir = app + .path() + .app_local_data_dir()? + .join("editor-sessions") + .join(site_id.to_string()); + builder = builder.data_directory(dir); + } + + let window = app + .get_window("main") + .ok_or_else(|| AppError::Internal("main window missing".into()))?; + let b = bounds.sane(); + window.add_child( + builder, + LogicalPosition::new(b.x, b.y), + LogicalSize::new(b.width, b.height), + )?; + platform_layout(app, Some(b))?; + *state.editor.lock().expect("poisoned") = Some(EditorSession { site_id }); + Ok(()) +} + +pub fn set_bounds(app: &AppHandle, bounds: Bounds) -> AppResult<()> { + if let Some(w) = app.get_webview(EDITOR_LABEL) { + let b = bounds.sane(); + w.set_position(LogicalPosition::new(b.x, b.y))?; + w.set_size(LogicalSize::new(b.width, b.height))?; + platform_layout(app, Some(b))?; + } + Ok(()) +} + +/// On Linux, Tauri stacks a window's webviews in a vertical GTK box and +/// ignores explicit positions. The shell keeps the editor full-width below its +/// toolbar (focus mode, the same layout as macOS and Windows), so on Linux the +/// editor is packed at the bottom of the box with a height of "everything below +/// the toolbar", and the shell, which expands, keeps the space above it. +#[cfg(target_os = "linux")] +fn platform_layout(app: &AppHandle, editor: Option) -> AppResult<()> { + use gtk::prelude::*; + let (Some(b), Some(window), Some(webview)) = ( + editor, + app.get_window("main"), + app.get_webview(EDITOR_LABEL), + ) else { + return Ok(()); + }; + webview.with_webview(move |platform| { + let Ok(vbox) = window.default_vbox() else { + return; + }; + let widget = platform.inner(); + // Space used by non-webview children such as the menu bar. + let chrome: i32 = vbox + .children() + .iter() + .filter(|c| !c.type_().name().starts_with("WebKit")) + .map(|c| c.allocated_height()) + .sum(); + let height = (vbox.allocated_height() - chrome - b.y.round() as i32).max(1); + vbox.set_child_packing(&widget, false, false, 0, gtk::PackType::End); + widget.set_size_request(-1, height); + })?; + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +fn platform_layout(_app: &AppHandle, _editor: Option) -> AppResult<()> { + Ok(()) +} + +pub fn command(app: &AppHandle, cmd: EditorCommand) -> AppResult<()> { + let w = app + .get_webview(EDITOR_LABEL) + .ok_or_else(|| AppError::Internal("no editor open".into()))?; + w.eval(cmd.script())?; + Ok(()) +} + +pub fn close(app: &AppHandle) -> AppResult<()> { + if let Some(w) = app.get_webview(EDITOR_LABEL) { + w.close()?; + } + platform_layout(app, None)?; + *app.state::().editor.lock().expect("poisoned") = None; + Ok(()) +} + +/// Called from the remote page. Rejects calls from any webview but the editor. +pub fn report(app: &AppHandle, webview: &Webview, report: EditorReport) -> AppResult<()> { + if webview.label() != EDITOR_LABEL { + return Err(AppError::Forbidden); + } + let report = report.validate()?; + app.emit_to("main", "editor://state", report)?; + Ok(()) +} + +/// Removes a site's isolated cookie store (on site removal). +pub fn clear_site_data(app: &AppHandle, site_id: &Uuid) { + #[cfg(not(target_os = "macos"))] + if let Ok(dir) = app.path().app_local_data_dir() { + std::fs::remove_dir_all(dir.join("editor-sessions").join(site_id.to_string())).ok(); + } + #[cfg(target_os = "macos")] + { + // WKWebsiteDataStore removal needs the store to be unused; done at next launch. + let _ = (app, site_id); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn site() -> SiteRecord { + let url = Url::parse("https://example.com/").unwrap(); + SiteRecord { + id: Uuid::new_v4(), + name: "x".into(), + admin_url: url.join("wp-admin/").unwrap(), + api_root: url.join("wp-json/").unwrap(), + url, + username: "eli".into(), + display_name: "Eli".into(), + avatar_url: None, + app_password_uuid: None, + bridge_available: false, + allow_insecure_local: false, + added_at: chrono::Utc::now(), + } + } + + #[test] + fn editor_urls() { + let s = site(); + assert_eq!( + editor_target(&s, "post", Some(42)).unwrap().as_str(), + "https://example.com/wp-admin/post.php?post=42&action=edit" + ); + assert_eq!( + editor_target(&s, "page", None).unwrap().as_str(), + "https://example.com/wp-admin/post-new.php?post_type=page" + ); + assert!(editor_target(&s, "page&x=1", None).is_err()); + } + + #[test] + fn report_validation() { + let r: EditorReport = serde_json::from_value(serde_json::json!({ + "kind": "editor", "title": "x".repeat(900), "link": "javascript:alert(1)" + })) + .unwrap(); + let r = r.validate().unwrap(); + assert_eq!(r.title.unwrap().len(), 500); + assert!(r.link.is_none()); + let bad: Result = + serde_json::from_value(serde_json::json!({"kind":"editor","evil":1})); + assert!(bad.is_err()); + let bad_kind: EditorReport = + serde_json::from_value(serde_json::json!({"kind":"x"})).unwrap(); + assert!(bad_kind.validate().is_err()); + } + + #[test] + fn bounds_are_clamped() { + let b = Bounds { + x: f64::NAN, + y: -5.0, + width: 0.0, + height: 1e9, + } + .sane(); + assert_eq!((b.x, b.y, b.width, b.height), (0.0, 0.0, 1.0, 20_000.0)); + } +} diff --git a/src-tauri/src/editor_bridge.js b/src-tauri/src/editor_bridge.js new file mode 100644 index 0000000..d35fe5a --- /dev/null +++ b/src-tauri/src/editor_bridge.js @@ -0,0 +1,163 @@ +// Injected into the editor webview (main frame only). It runs inside the +// site's own wp-admin, so it must treat the page as untrusted: it only reads +// editor state and reports a small, fixed-shape object to the native shell. +(function () { + "use strict"; + var CONFIG = __WPRE_CONFIG__; + if (window.top !== window) return; + if (CONFIG.origins.indexOf(window.location.origin) === -1) return; + + // ---- Native look: hide wp-admin chrome around the block editor ---------- + var css = + "html.wp-toolbar{padding-top:0!important}" + + "#wpadminbar,#adminmenumain,#wpfooter,.edit-post-fullscreen-mode-close," + + ".editor-header__back-button,.edit-post-header__back-button{display:none!important}" + + "#wpcontent,#wpbody-content{margin-left:0!important;padding-bottom:0!important}" + + ".interface-interface-skeleton{top:0!important;left:0!important}" + + "body.is-fullscreen-mode .interface-interface-skeleton{top:0!important}"; + function injectStyle() { + if (document.getElementById("wpre-embedded-style")) return; + var style = document.createElement("style"); + style.id = "wpre-embedded-style"; + style.textContent = css; + (document.head || document.documentElement).appendChild(style); + document.documentElement.classList.add("wpre-embedded"); + } + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", injectStyle); + } else { + injectStyle(); + } + + // ---- Reporting -------------------------------------------------------- + function report(payload) { + try { + var ipc = window.__TAURI_INTERNALS__; + if (ipc && typeof ipc.invoke === "function") { + ipc.invoke("editor_report", { report: payload }); + } + } catch (e) { + /* the shell simply won't update */ + } + } + + var path = window.location.pathname; + if (/\/wp-login\.php$/.test(path)) { + report({ kind: "login" }); + return; + } + + function editorStore() { + var wp = window.wp; + return wp && wp.data && wp.data.select && wp.data.select("core/editor"); + } + + function hasPublishAction(s) { + var post = s.getCurrentPost && s.getCurrentPost(); + return !!(post && post._links && post._links["wp:action-publish"]); + } + + function snapshot(s) { + var title = s.getEditedPostAttribute("title"); + return { + kind: "editor", + postId: s.getCurrentPostId() || null, + postType: String(s.getCurrentPostType() || "").slice(0, 64), + title: String(title || "").slice(0, 500), + status: String(s.getEditedPostAttribute("status") || "").slice(0, 32), + link: String(s.getPermalink ? s.getPermalink() || "" : "").slice(0, 2048), + dirty: !!s.isEditedPostDirty(), + saving: !!s.isSavingPost() && !s.isAutosavingPost(), + canPublish: hasPublishAction(s), + }; + } + + function start() { + var s = editorStore(); + if (!s || !s.getCurrentPostId) return false; + var last = ""; + var pending = false; + window.wp.data.subscribe(function () { + if (pending) return; + pending = true; + // Coalesce bursts of store updates into one report per frame. + window.requestAnimationFrame(function () { + pending = false; + var snap = snapshot(editorStore()); + var key = JSON.stringify(snap); + if (key !== last) { + last = key; + report(snap); + } + }); + }); + report(snapshot(s)); + return true; + } + + // ---- Commands the native toolbar can trigger -------------------------- + function click(selectors) { + for (var i = 0; i < selectors.length; i++) { + var el = document.querySelector(selectors[i]); + if (el && !el.disabled && el.getAttribute("aria-disabled") !== "true") { + el.click(); + return true; + } + } + return false; + } + + Object.defineProperty(window, "__wpre", { + configurable: false, + writable: false, + value: Object.freeze({ + save: function () { + var d = window.wp.data.dispatch("core/editor"); + d.savePost(); + }, + // Use Gutenberg's own publish button so pre-publish checks and any + // plugin hooks on it (SEO, scheduling, review) still run. + publish: function () { + if ( + !click([ + ".editor-post-publish-panel__toggle", + ".editor-post-publish-button__button", + ".editor-post-publish-button", + ]) + ) { + var d = window.wp.data.dispatch("core/editor"); + var s = editorStore(); + d.editPost({ status: hasPublishAction(s) ? "publish" : "pending" }); + d.savePost(); + } + }, + device: function (type) { + var allowed = { Desktop: 1, Tablet: 1, Mobile: 1 }; + if (!allowed[type]) return; + var d = window.wp.data.dispatch("core/editor"); + if (d.setDeviceType) d.setDeviceType(type); + }, + undo: function () { + window.wp.data.dispatch("core/editor").undo(); + }, + redo: function () { + window.wp.data.dispatch("core/editor").redo(); + }, + toggleSettings: function () { + click([ + 'button[aria-controls="edit-post:document"]', + 'button[aria-label="Settings"]', + ]); + }, + }), + }); + + var tries = 0; + var timer = window.setInterval(function () { + tries += 1; + if (start() || tries > 120) { + window.clearInterval(timer); + if (tries > 120) report({ kind: "page", path: path.slice(0, 512) }); + } + }, 250); +})(); diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs new file mode 100644 index 0000000..f0b4081 --- /dev/null +++ b/src-tauri/src/error.rs @@ -0,0 +1,69 @@ +use serde::{Serialize, Serializer}; + +/// Errors returned to the shell UI. Messages are written for end users and +/// never include credentials, tokens or raw response bodies. +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("That doesn't look like a valid site address.")] + InvalidUrl, + #[error("This site must use HTTPS. Plain HTTP is only allowed for local development sites.")] + InsecureUrl, + #[error("Couldn't find the WordPress REST API on this site.")] + NotWordPress, + #[error("This site has Application Passwords turned off, so the app can't sign in. Ask the site administrator to enable them.")] + ApplicationPasswordsUnavailable, + #[error("WordPress rejected the username or application password.")] + Unauthorized, + #[error("Your WordPress role doesn't allow this action.")] + Forbidden, + #[error("The item no longer exists on the site.")] + NotFound, + #[error("WordPress returned an error: {0}")] + WordPress(String), + #[error("Couldn't reach the site: {0}")] + Network(String), + #[error("The site sent a response that was too large or malformed.")] + BadResponse, + #[error("Couldn't access the system keychain: {0}")] + Keychain(String), + #[error("Unknown site.")] + UnknownSite, + #[error("Sign-in was cancelled or didn't match the request.")] + AuthFlow, + #[error("{0}")] + Internal(String), +} + +impl Serialize for AppError { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +impl From for AppError { + fn from(e: reqwest::Error) -> Self { + // Strip the URL so query strings never reach logs or the UI. + let e = e.without_url(); + if e.is_timeout() { + AppError::Network("the connection timed out".into()) + } else if e.is_decode() { + AppError::BadResponse + } else { + AppError::Network(e.to_string()) + } + } +} + +impl From for AppError { + fn from(e: keyring::Error) -> Self { + AppError::Keychain(e.to_string()) + } +} + +impl From for AppError { + fn from(e: tauri::Error) -> Self { + AppError::Internal(e.to_string()) + } +} + +pub type AppResult = Result; diff --git a/src-tauri/src/keychain.rs b/src-tauri/src/keychain.rs new file mode 100644 index 0000000..ec12424 --- /dev/null +++ b/src-tauri/src/keychain.rs @@ -0,0 +1,100 @@ +//! Application Passwords live only in the OS keychain: +//! macOS Keychain, Windows Credential Manager, or Secret Service on Linux. + +use crate::error::{AppError, AppResult}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +const SERVICE: &str = "com.wpremoteeditor.app"; + +#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] +pub struct Credentials { + pub username: String, + pub app_password: String, +} + +impl Credentials { + /// Application Passwords are shown with spaces for readability; WordPress + /// accepts them either way, so store the compact form. + pub fn new(username: &str, app_password: &str) -> Self { + Self { + username: username.trim().to_string(), + app_password: app_password + .chars() + .filter(|c| !c.is_whitespace()) + .collect(), + } + } + + /// `Basic` authorization header value. + pub fn basic_auth_header(&self) -> Zeroizing { + use base64::Engine; + let raw = Zeroizing::new(format!("{}:{}", self.username, self.app_password)); + Zeroizing::new(format!( + "Basic {}", + base64::engine::general_purpose::STANDARD.encode(raw.as_bytes()) + )) + } +} + +impl std::fmt::Debug for Credentials { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Credentials") + .field("username", &self.username) + .field("app_password", &"") + .finish() + } +} + +fn entry(site_id: &Uuid) -> AppResult { + Ok(keyring::Entry::new(SERVICE, &format!("site:{site_id}"))?) +} + +pub fn save(site_id: &Uuid, creds: &Credentials) -> AppResult<()> { + let json = Zeroizing::new( + serde_json::to_string(creds).map_err(|e| AppError::Internal(e.to_string()))?, + ); + entry(site_id)?.set_password(&json)?; + Ok(()) +} + +pub fn load(site_id: &Uuid) -> AppResult { + let json = Zeroizing::new(entry(site_id)?.get_password().map_err(|e| match e { + keyring::Error::NoEntry => AppError::Unauthorized, + other => other.into(), + })?); + serde_json::from_str(&json) + .map_err(|_| AppError::Keychain("stored credentials are unreadable".into())) +} + +pub fn delete(site_id: &Uuid) -> AppResult<()> { + match entry(site_id)?.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => Err(e.into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_spaces_from_application_passwords() { + let c = Credentials::new(" eli ", "abcd EFGH ijkl MNOP qrst UVWX"); + assert_eq!(c.username, "eli"); + assert_eq!(c.app_password, "abcdEFGHijklMNOPqrstUVWX"); + } + + #[test] + fn debug_output_redacts_password() { + let c = Credentials::new("eli", "secret"); + assert!(!format!("{c:?}").contains("secret")); + } + + #[test] + fn builds_basic_auth_header() { + let c = Credentials::new("eli", "pw"); + assert_eq!(c.basic_auth_header().as_str(), "Basic ZWxpOnB3"); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..52753e1 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,87 @@ +mod auth; +mod commands; +mod editor; +mod error; +mod keychain; +mod menu; +mod security; +mod sites; +mod wp; + +use std::sync::Mutex; +use tauri::{LogicalPosition, Manager}; + +pub struct AppState { + pub store: sites::SiteStore, + pub pending_auth: Mutex>, + pub editor: Mutex>, +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(w) = app.get_window("main") { + w.unminimize().ok(); + w.set_focus().ok(); + } + })) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_dialog::init()) + .setup(|app| { + let data_dir = app.path().app_data_dir()?; + app.manage(AppState { + store: sites::SiteStore::load(data_dir.join("sites.json")), + pending_auth: Mutex::new(None), + editor: Mutex::new(None), + }); + create_main_window(app)?; + let menu = menu::build(app.handle())?; + app.set_menu(menu)?; + app.on_menu_event(|app, event| menu::forward(app, event.id().as_ref())); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::discover_site, + commands::begin_authorization, + commands::add_site_manual, + commands::list_sites, + commands::remove_site, + commands::get_site_context, + commands::list_posts, + commands::trash_post, + commands::open_editor, + commands::close_editor, + commands::editor_set_bounds, + commands::editor_command, + commands::editor_report, + ]) + .run(tauri::generate_context!()) + .expect("error while running WordPress Remote Editor"); +} + +/// The main window is a plain window hosting two webviews: the shell UI +/// (resized with the window) and, when open, the editor placed over the +/// shell's editor area. Building it this way keeps child-webview layout +/// identical on macOS, Windows and Linux. +fn create_main_window(app: &mut tauri::App) -> tauri::Result<()> { + let config = app + .config() + .app + .windows + .iter() + .find(|w| w.label == "main") + .cloned() + .expect("main window missing from tauri.conf.json"); + let window = tauri::window::WindowBuilder::from_config(app, &config)?.build()?; + let size = window + .inner_size()? + .to_logical::(window.scale_factor()?); + window.add_child( + tauri::webview::WebviewBuilder::from_config(&config).auto_resize(), + LogicalPosition::new(0.0, 0.0), + size, + )?; + Ok(()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..06a51b6 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an additional console window on Windows in release builds. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + wpre_lib::run(); +} diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs new file mode 100644 index 0000000..ef49862 --- /dev/null +++ b/src-tauri/src/menu.rs @@ -0,0 +1,96 @@ +//! Native application menu. Standard Edit items matter on macOS: without them +//! clipboard shortcuts don't reach the webviews. + +use tauri::menu::{AboutMetadata, Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder}; +use tauri::{AppHandle, Emitter, Runtime}; + +pub fn build(app: &AppHandle) -> tauri::Result> { + let item = |id: &str, label: &str, accel: Option<&str>| { + let mut b = MenuItemBuilder::with_id(id, label); + if let Some(a) = accel { + b = b.accelerator(a); + } + b.build(app) + }; + + let file = SubmenuBuilder::new(app, "File") + .item(&item("new-post", "New Post", Some("CmdOrCtrl+N"))?) + .item(&item("new-page", "New Page", Some("CmdOrCtrl+Shift+N"))?) + .separator() + .item(&item("save", "Save Draft", Some("CmdOrCtrl+S"))?) + .item(&item("publish", "Publish…", Some("CmdOrCtrl+Shift+P"))?) + .separator() + .item(&item("add-site", "Add Site…", None)?) + .item(&item("close-editor", "Close Editor", Some("CmdOrCtrl+W"))?); + #[cfg(not(target_os = "macos"))] + let file = file + .separator() + .item(&tauri::menu::PredefinedMenuItem::quit(app, None)?); + let file = file.build()?; + + let edit = SubmenuBuilder::new(app, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .select_all() + .build()?; + + let view = SubmenuBuilder::new(app, "View") + .item(&item( + "toggle-sidebar", + "Toggle Sidebar", + Some("CmdOrCtrl+Alt+S"), + )?) + .item(&item( + "toggle-settings", + "Toggle Block Settings", + Some("CmdOrCtrl+Shift+,"), + )?) + .separator() + .item(&item("device-desktop", "Preview Desktop", None)?) + .item(&item("device-tablet", "Preview Tablet", None)?) + .item(&item("device-mobile", "Preview Mobile", None)?) + .separator() + .fullscreen() + .build()?; + + let window = SubmenuBuilder::new(app, "Window") + .minimize() + .maximize() + .separator() + .close_window() + .build()?; + + let help = SubmenuBuilder::new(app, "Help") + .item(&item("help-docs", "WordPress Remote Editor Help", None)?) + .build()?; + + #[allow(unused_mut)] + let mut menu = MenuBuilder::new(app); + #[cfg(target_os = "macos")] + { + let app_menu = SubmenuBuilder::new(app, "WordPress Remote Editor") + .about(Some(AboutMetadata::default())) + .separator() + .services() + .separator() + .hide() + .hide_others() + .show_all() + .separator() + .quit() + .build()?; + menu = menu.item(&app_menu); + } + #[cfg(not(target_os = "macos"))] + let _ = AboutMetadata::default(); + menu.items(&[&file, &edit, &view, &window, &help]).build() +} + +/// Forwards custom menu items to the shell as `menu://action` events. +pub fn forward(app: &AppHandle, id: &str) { + app.emit_to("main", "menu://action", id).ok(); +} diff --git a/src-tauri/src/security.rs b/src-tauri/src/security.rs new file mode 100644 index 0000000..b073825 --- /dev/null +++ b/src-tauri/src/security.rs @@ -0,0 +1,122 @@ +//! URL normalisation and origin rules shared by the REST client, the auth +//! window and the editor webview. + +use crate::error::{AppError, AppResult}; +use url::Url; + +/// Hosts where plain HTTP is acceptable (local development only). +pub fn is_local_dev_host(host: &str) -> bool { + let host = host.trim_end_matches('.').to_ascii_lowercase(); + host == "localhost" + || host == "127.0.0.1" + || host == "[::1]" + || host == "::1" + || host.ends_with(".localhost") + || host.ends_with(".local") + || host.ends_with(".test") +} + +/// Turns what the user typed ("example.com", "https://example.com/blog/") +/// into a canonical site URL with no query, fragment or credentials. +pub fn normalize_site_url(input: &str, allow_insecure_local: bool) -> AppResult { + let trimmed = input.trim(); + if trimmed.is_empty() || trimmed.len() > 2048 { + return Err(AppError::InvalidUrl); + } + let with_scheme = if trimmed.contains("://") { + trimmed.to_string() + } else { + format!("https://{trimmed}") + }; + let mut url = Url::parse(&with_scheme).map_err(|_| AppError::InvalidUrl)?; + if !url.username().is_empty() || url.password().is_some() { + return Err(AppError::InvalidUrl); + } + let host = url.host_str().ok_or(AppError::InvalidUrl)?.to_string(); + match url.scheme() { + "https" => {} + "http" if allow_insecure_local && is_local_dev_host(&host) => {} + "http" => return Err(AppError::InsecureUrl), + _ => return Err(AppError::InvalidUrl), + } + url.set_query(None); + url.set_fragment(None); + // Strip common admin paths people paste by accident. + let mut path = url.path().trim_end_matches('/').to_string(); + for suffix in ["/wp-admin", "/wp-login.php", "/wp-json"] { + if let Some(stripped) = path.strip_suffix(suffix) { + path = stripped.to_string(); + } + } + url.set_path(&format!("{path}/")); + Ok(url) +} + +/// True when `candidate` has the same scheme, host and port as `origin`. +pub fn same_origin(origin: &Url, candidate: &Url) -> bool { + origin.scheme() == candidate.scheme() + && origin.host_str() == candidate.host_str() + && origin.port_or_known_default() == candidate.port_or_known_default() +} + +/// Rejects a URL handed to us by a server (e.g. the authorization endpoint or +/// the REST root) unless it stays on the same origin as the site and is not a +/// downgrade. +pub fn require_same_origin(site: &Url, candidate: &str) -> AppResult { + let url = Url::parse(candidate).map_err(|_| AppError::BadResponse)?; + if same_origin(site, &url) { + return Ok(url); + } + // Allow http -> https upgrade on the same host. + if site.scheme() == "http" && url.scheme() == "https" && site.host_str() == url.host_str() { + return Ok(url); + } + Err(AppError::BadResponse) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adds_https_and_trailing_slash() { + let u = normalize_site_url("example.com", false).unwrap(); + assert_eq!(u.as_str(), "https://example.com/"); + } + + #[test] + fn strips_admin_paths_query_and_fragment() { + let u = normalize_site_url("https://example.com/blog/wp-admin/?x=1#y", false).unwrap(); + assert_eq!(u.as_str(), "https://example.com/blog/"); + } + + #[test] + fn rejects_plain_http_for_public_hosts() { + assert!(matches!( + normalize_site_url("http://example.com", true), + Err(AppError::InsecureUrl) + )); + } + + #[test] + fn allows_http_for_local_dev_only_when_opted_in() { + assert!(normalize_site_url("http://mysite.test", true).is_ok()); + assert!(normalize_site_url("http://mysite.test", false).is_err()); + } + + #[test] + fn rejects_credentials_and_odd_schemes() { + assert!(normalize_site_url("https://user:pw@example.com", false).is_err()); + assert!(normalize_site_url("javascript:alert(1)", false).is_err()); + assert!(normalize_site_url("file:///etc/passwd", false).is_err()); + } + + #[test] + fn origin_checks() { + let site = Url::parse("https://example.com/").unwrap(); + assert!(require_same_origin(&site, "https://example.com/wp-json/").is_ok()); + assert!(require_same_origin(&site, "https://evil.com/wp-json/").is_err()); + assert!(require_same_origin(&site, "http://example.com/wp-json/").is_err()); + assert!(require_same_origin(&site, "https://example.com:8443/").is_err()); + } +} diff --git a/src-tauri/src/sites.rs b/src-tauri/src/sites.rs new file mode 100644 index 0000000..8c28970 --- /dev/null +++ b/src-tauri/src/sites.rs @@ -0,0 +1,166 @@ +//! Non-secret site metadata, persisted as JSON in the app data directory. +//! Secrets are never written here; see `keychain`. + +use crate::error::{AppError, AppResult}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::Mutex; +use url::Url; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteRecord { + pub id: Uuid, + pub name: String, + pub url: Url, + /// `siteurl` + `wp-admin/`, where the block editor is loaded from. + pub admin_url: Url, + pub api_root: Url, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + /// UUID of the Application Password, used to revoke it on removal. + pub app_password_uuid: Option, + /// Whether the companion plugin is active (enables editor sign-in handoff). + pub bridge_available: bool, + pub allow_insecure_local: bool, + pub added_at: chrono::DateTime, +} + +impl SiteRecord { + pub fn summary(&self) -> crate::wp::models::SiteSummary { + crate::wp::models::SiteSummary { + id: self.id, + name: self.name.clone(), + url: self.url.clone(), + username: self.username.clone(), + display_name: self.display_name.clone(), + avatar_url: self.avatar_url.clone(), + bridge_available: self.bridge_available, + } + } +} + +pub struct SiteStore { + path: PathBuf, + sites: Mutex>, +} + +impl SiteStore { + pub fn load(path: PathBuf) -> Self { + let sites = std::fs::read(&path) + .ok() + .and_then(|bytes| serde_json::from_slice(&bytes).ok()) + .unwrap_or_default(); + Self { + path, + sites: Mutex::new(sites), + } + } + + pub fn list(&self) -> Vec { + self.sites.lock().expect("site store poisoned").clone() + } + + pub fn get(&self, id: &Uuid) -> AppResult { + self.list() + .into_iter() + .find(|s| &s.id == id) + .ok_or(AppError::UnknownSite) + } + + /// Inserts or replaces (matched by URL + username so re-adding a site + /// doesn't create duplicates). + pub fn upsert(&self, mut record: SiteRecord) -> AppResult { + let mut sites = self.sites.lock().expect("site store poisoned"); + if let Some(existing) = sites + .iter_mut() + .find(|s| s.url == record.url && s.username == record.username) + { + record.id = existing.id; + record.added_at = existing.added_at; + *existing = record.clone(); + } else { + sites.push(record.clone()); + } + self.persist(&sites)?; + Ok(record) + } + + pub fn remove(&self, id: &Uuid) -> AppResult<()> { + let mut sites = self.sites.lock().expect("site store poisoned"); + sites.retain(|s| &s.id != id); + self.persist(&sites) + } + + fn persist(&self, sites: &[SiteRecord]) -> AppResult<()> { + let io = |e: std::io::Error| AppError::Internal(format!("couldn't save site list: {e}")); + if let Some(dir) = self.path.parent() { + std::fs::create_dir_all(dir).map_err(io)?; + } + let tmp = self.path.with_extension("json.tmp"); + let bytes = + serde_json::to_vec_pretty(sites).map_err(|e| AppError::Internal(e.to_string()))?; + write_private(&tmp, &bytes).map_err(io)?; + std::fs::rename(&tmp, &self.path).map_err(io) + } +} + +#[cfg(unix)] +fn write_private(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + let mut f = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path)?; + f.write_all(bytes)?; + f.sync_all() +} + +#[cfg(not(unix))] +fn write_private(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> { + // The per-user app data directory on Windows is already ACL'd to the user. + std::fs::write(path, bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn record(url: &str, user: &str) -> SiteRecord { + let url = Url::parse(url).unwrap(); + SiteRecord { + id: Uuid::new_v4(), + name: "Site".into(), + api_root: url.join("wp-json/").unwrap(), + admin_url: url.join("wp-admin/").unwrap(), + url, + username: user.into(), + display_name: user.into(), + avatar_url: None, + app_password_uuid: None, + bridge_available: false, + allow_insecure_local: false, + added_at: chrono::Utc::now(), + } + } + + #[test] + fn upsert_dedupes_and_persists() { + let dir = std::env::temp_dir().join(format!("wpre-test-{}", Uuid::new_v4())); + let path = dir.join("sites.json"); + let store = SiteStore::load(path.clone()); + let a = store.upsert(record("https://a.com/", "eli")).unwrap(); + let again = store.upsert(record("https://a.com/", "eli")).unwrap(); + assert_eq!(a.id, again.id); + store.upsert(record("https://b.com/", "eli")).unwrap(); + assert_eq!(SiteStore::load(path).list().len(), 2); + store.remove(&a.id).unwrap(); + assert_eq!(store.list().len(), 1); + std::fs::remove_dir_all(dir).ok(); + } +} diff --git a/src-tauri/src/wp/client.rs b/src-tauri/src/wp/client.rs new file mode 100644 index 0000000..66232c1 --- /dev/null +++ b/src-tauri/src/wp/client.rs @@ -0,0 +1,242 @@ +//! Authenticated WordPress REST client. All requests go through the Rust core; +//! the shell webview never sees credentials or talks to the site directly. + +use crate::error::{AppError, AppResult}; +use crate::keychain::Credentials; +use crate::security::{is_local_dev_host, same_origin}; +use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION}; +use reqwest::{Method, Response, StatusCode}; +use serde::de::DeserializeOwned; +use std::time::Duration; +use url::Url; +use zeroize::Zeroizing; + +pub const USER_AGENT: &str = concat!("WordPressRemoteEditor/", env!("CARGO_PKG_VERSION")); +const MAX_RESPONSE_BYTES: usize = 16 * 1024 * 1024; +const MAX_REDIRECTS: usize = 5; + +/// Builds an HTTP client. `pin_origin` restricts redirects to that origin +/// (used for authenticated clients); `None` allows HTTPS redirects anywhere +/// (used for unauthenticated discovery, e.g. example.com -> www.example.com). +pub fn http_client( + pin_origin: Option, + allow_insecure_local: bool, +) -> AppResult { + let policy = reqwest::redirect::Policy::custom(move |attempt| { + if attempt.previous().len() >= MAX_REDIRECTS { + return attempt.error("too many redirects"); + } + let next = attempt.url(); + let ok_scheme = next.scheme() == "https" + || (allow_insecure_local + && next.scheme() == "http" + && next.host_str().is_some_and(is_local_dev_host)); + if !ok_scheme { + return attempt.stop(); + } + match &pin_origin { + Some(origin) if !same_origin(origin, next) => attempt.stop(), + _ => attempt.follow(), + } + }); + reqwest::Client::builder() + .user_agent(USER_AGENT) + .https_only(!allow_insecure_local) + .redirect(policy) + .connect_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(60)) + .build() + .map_err(AppError::from) +} + +pub async fn read_limited(mut resp: Response) -> AppResult> { + if resp + .content_length() + .is_some_and(|l| l as usize > MAX_RESPONSE_BYTES) + { + return Err(AppError::BadResponse); + } + let mut buf = Vec::new(); + while let Some(chunk) = resp.chunk().await? { + if buf.len() + chunk.len() > MAX_RESPONSE_BYTES { + return Err(AppError::BadResponse); + } + buf.extend_from_slice(&chunk); + } + Ok(buf) +} + +/// Maps a non-success response to a user-facing error, using WordPress's +/// `{ code, message }` error body when present. +pub async fn error_for(resp: Response) -> AppError { + let status = resp.status(); + let body = read_limited(resp).await.unwrap_or_default(); + let wp: Option = serde_json::from_slice(&body).ok(); + match status { + StatusCode::UNAUTHORIZED => AppError::Unauthorized, + StatusCode::FORBIDDEN => AppError::Forbidden, + StatusCode::NOT_FOUND | StatusCode::GONE => AppError::NotFound, + _ => match wp { + Some(e) if !e.message.is_empty() => { + let mut msg: String = strip_tags(&e.message).chars().take(300).collect(); + if msg.is_empty() { + msg = e.code; + } + AppError::WordPress(msg) + } + _ => AppError::WordPress(format!("HTTP {}", status.as_u16())), + }, + } +} + +fn strip_tags(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut in_tag = false; + for c in s.chars() { + match c { + '<' => in_tag = true, + '>' => in_tag = false, + _ if !in_tag => out.push(c), + _ => {} + } + } + out +} + +/// Builds a REST URL for `route` (e.g. `wp/v2/posts`) under `api_root`, +/// supporting both pretty permalinks (`/wp-json/`) and plain permalinks +/// (`/?rest_route=/`). +pub fn route_url(api_root: &Url, route: &str, query: &[(&str, String)]) -> AppResult { + let route = route.trim_start_matches('/'); + let uses_rest_route = api_root.query_pairs().any(|(k, _)| k == "rest_route"); + let mut url = if uses_rest_route { + let mut base = api_root.clone(); + base.set_query(None); + base.query_pairs_mut() + .append_pair("rest_route", &format!("/{route}")); + base + } else { + let mut root = api_root.clone(); + if !root.path().ends_with('/') { + root.set_path(&format!("{}/", root.path())); + } + root.join(route).map_err(|_| AppError::InvalidUrl)? + }; + if !query.is_empty() { + let mut pairs = url.query_pairs_mut(); + for (k, v) in query { + pairs.append_pair(k, v); + } + } + Ok(url) +} + +pub struct WpClient { + http: reqwest::Client, + api_root: Url, + auth: Zeroizing, +} + +impl WpClient { + pub fn new(api_root: Url, creds: &Credentials, allow_insecure_local: bool) -> AppResult { + Ok(Self { + http: http_client(Some(api_root.clone()), allow_insecure_local)?, + auth: creds.basic_auth_header(), + api_root, + }) + } + + async fn send( + &self, + method: Method, + route: &str, + query: &[(&str, String)], + body: Option<&serde_json::Value>, + ) -> AppResult<(Vec, HeaderMap)> { + let url = route_url(&self.api_root, route, query)?; + let mut req = self + .http + .request(method, url) + .header(AUTHORIZATION, self.auth.as_str()) + .header(ACCEPT, "application/json"); + if let Some(b) = body { + req = req.json(b); + } + let resp = req.send().await?; + if !resp.status().is_success() { + return Err(error_for(resp).await); + } + let headers = resp.headers().clone(); + Ok((read_limited(resp).await?, headers)) + } + + pub async fn get( + &self, + route: &str, + query: &[(&str, String)], + ) -> AppResult<(T, HeaderMap)> { + let (bytes, headers) = self.send(Method::GET, route, query, None).await?; + let value = serde_json::from_slice(&bytes).map_err(|_| AppError::BadResponse)?; + Ok((value, headers)) + } + + pub async fn post( + &self, + route: &str, + body: &serde_json::Value, + ) -> AppResult { + let (bytes, _) = self.send(Method::POST, route, &[], Some(body)).await?; + serde_json::from_slice(&bytes).map_err(|_| AppError::BadResponse) + } + + pub async fn delete(&self, route: &str, query: &[(&str, String)]) -> AppResult<()> { + self.send(Method::DELETE, route, query, None) + .await + .map(|_| ()) + } +} + +pub fn header_u64(headers: &HeaderMap, name: &str) -> u64 { + headers + .get(name) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.trim().parse().ok()) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pretty_permalink_routes() { + let root = Url::parse("https://example.com/wp-json/").unwrap(); + let u = route_url(&root, "wp/v2/posts", &[("context", "edit".into())]).unwrap(); + assert_eq!( + u.as_str(), + "https://example.com/wp-json/wp/v2/posts?context=edit" + ); + } + + #[test] + fn plain_permalink_routes() { + let root = Url::parse("https://example.com/?rest_route=/").unwrap(); + let u = route_url(&root, "/wp/v2/posts", &[("page", "2".into())]).unwrap(); + assert_eq!( + u.as_str(), + "https://example.com/?rest_route=%2Fwp%2Fv2%2Fposts&page=2" + ); + } + + #[test] + fn query_values_are_encoded() { + let root = Url::parse("https://example.com/wp-json/").unwrap(); + let u = route_url(&root, "wp/v2/posts", &[("search", "a&b=c".into())]).unwrap(); + assert!(u.as_str().ends_with("search=a%26b%3Dc")); + } + + #[test] + fn strips_html_from_error_messages() { + assert_eq!(strip_tags("Error: nope"), "Error: nope"); + } +} diff --git a/src-tauri/src/wp/discovery.rs b/src-tauri/src/wp/discovery.rs new file mode 100644 index 0000000..657050e --- /dev/null +++ b/src-tauri/src/wp/discovery.rs @@ -0,0 +1,136 @@ +//! Finds a site's REST API and Application Password authorization endpoint +//! the way WordPress documents it: via the `rel="https://api.w.org/"` link. + +use super::client::{error_for, http_client, read_limited}; +use super::models::{RestIndex, SiteDiscovery}; +use crate::error::{AppError, AppResult}; +use crate::security::{normalize_site_url, require_same_origin}; +use reqwest::header::LINK; +use url::Url; + +const API_REL: &str = "https://api.w.org/"; + +/// Extracts the REST API root from one or more `Link` header values. +pub fn api_root_from_link_headers<'a>(values: impl Iterator) -> Option { + for value in values { + for part in value.split(',') { + let mut segments = part.split(';'); + let target = segments.next()?.trim(); + let is_api = segments.any(|s| { + let s = s.trim(); + s == format!("rel=\"{API_REL}\"") || s == format!("rel={API_REL}") + }); + if is_api { + let t = target.trim_start_matches('<').trim_end_matches('>'); + return Some(t.to_string()); + } + } + } + None +} + +pub async fn discover(input: &str, allow_insecure_local: bool) -> AppResult { + let entered = normalize_site_url(input, allow_insecure_local)?; + let http = http_client(None, allow_insecure_local)?; + + // 1. Load the site and follow redirects (e.g. to www.) to learn its real address. + let resp = http.get(entered.clone()).send().await?; + let site_url = normalize_site_url(resp.url().as_str(), allow_insecure_local)?; + let link = api_root_from_link_headers( + resp.headers() + .get_all(LINK) + .iter() + .filter_map(|v| v.to_str().ok()), + ); + drop(resp); + + let api_root = match link { + Some(l) => require_same_origin(&site_url, &l)?, + None => site_url + .join("wp-json/") + .map_err(|_| AppError::InvalidUrl)?, + }; + + // 2. Read the REST index. + let resp = http + .get(api_root.clone()) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .await?; + if !resp.status().is_success() { + return Err(match error_for(resp).await { + AppError::NotFound => AppError::NotWordPress, + e => e, + }); + } + let bytes = read_limited(resp).await?; + let index: RestIndex = serde_json::from_slice(&bytes).map_err(|_| AppError::NotWordPress)?; + if !index.namespaces.iter().any(|n| n == "wp/v2") { + return Err(AppError::NotWordPress); + } + + // `url` (siteurl) is where wp-admin lives and may differ from `home`. + // It must use an acceptable scheme, but may be on a different host. + let admin_base = if index.url.is_empty() { + site_url.clone() + } else { + normalize_site_url(&index.url, allow_insecure_local)? + }; + let admin_url = admin_base + .join("wp-admin/") + .map_err(|_| AppError::BadResponse)?; + + let authorization_endpoint = index + .authentication + .get("application-passwords") + .and_then(|v| v.pointer("/endpoints/authorization")) + .and_then(|v| v.as_str()) + .and_then(|s| Url::parse(s).ok()) + .filter(|u| u.scheme() == "https" || (allow_insecure_local && u.scheme() == "http")); + + Ok(SiteDiscovery { + name: if index.name.is_empty() { + site_url.host_str().unwrap_or_default().to_string() + } else { + index.name + }, + description: index.description, + site_url, + admin_url, + api_root, + supports_application_passwords: authorization_endpoint.is_some(), + authorization_endpoint, + bridge_available: index.namespaces.iter().any(|n| n == "wpre/v1"), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finds_api_link_among_several() { + let headers = [ + "; rel=\"alternate\"; type=\"application/json\"", + "; rel=\"https://api.w.org/\"", + ]; + assert_eq!( + api_root_from_link_headers(headers.iter().copied()).as_deref(), + Some("https://example.com/wp-json/") + ); + } + + #[test] + fn handles_comma_joined_links_and_plain_permalinks() { + let headers = ["; rel=shortlink, ; rel=\"https://api.w.org/\""]; + assert_eq!( + api_root_from_link_headers(headers.iter().copied()).as_deref(), + Some("https://e.com/?rest_route=/") + ); + } + + #[test] + fn none_when_absent() { + assert!(api_root_from_link_headers(["; rel=\"next\""].iter().copied()).is_none()); + } +} diff --git a/src-tauri/src/wp/mod.rs b/src-tauri/src/wp/mod.rs new file mode 100644 index 0000000..ccd6d98 --- /dev/null +++ b/src-tauri/src/wp/mod.rs @@ -0,0 +1,117 @@ +pub mod client; +pub mod discovery; +pub mod models; + +/// Live tests against a real WordPress site. Run with: +/// `WPRE_TEST_SITE=… WPRE_TEST_USER=… WPRE_TEST_APP_PASSWORD=… cargo test -- --ignored` +#[cfg(test)] +mod live_tests { + use super::client::WpClient; + use super::discovery::discover; + use super::models::{post_type_access, RestPost, RestPostType, RestUser}; + use crate::keychain::Credentials; + use std::collections::HashMap; + + fn env() -> Option<(String, String, String)> { + Some(( + std::env::var("WPRE_TEST_SITE").ok()?, + std::env::var("WPRE_TEST_USER").ok()?, + std::env::var("WPRE_TEST_APP_PASSWORD").ok()?, + )) + } + + #[tokio::test] + #[ignore = "needs a live WordPress site"] + async fn discovers_and_reads_as_the_user() { + let Some((site, user, pw)) = env() else { + return; + }; + let local = site.starts_with("http://"); + let d = discover(&site, local).await.expect("discovery"); + assert!(d.supports_application_passwords); + let client = + WpClient::new(d.api_root.clone(), &Credentials::new(&user, &pw), local).unwrap(); + + let (me, _) = client + .get::("wp/v2/users/me", &[("context", "edit".into())]) + .await + .expect("users/me"); + let (types, _) = client + .get::>("wp/v2/types", &[("context", "edit".into())]) + .await + .expect("types"); + let access: Vec<_> = types + .values() + .map(|t| post_type_access(t, &me.capabilities)) + .collect(); + println!( + "site={} bridge={} roles={:?}", + d.name, d.bridge_available, me.roles + ); + for a in &access { + println!( + " {} create={} publish={}", + a.slug, a.can_create, a.can_publish + ); + } + let (posts, headers) = client + .get::>( + "wp/v2/posts", + &[ + ("context", "edit".into()), + ("status", "any".into()), + ("per_page", "5".into()), + ], + ) + .await + .expect("posts"); + println!( + " posts={} total={:?}", + posts.len(), + headers.get("x-wp-total") + ); + + if d.bridge_available { + #[derive(serde::Deserialize)] + struct Handoff { + url: String, + } + let target = d.admin_url.join("post-new.php").unwrap(); + let h: Handoff = client + .post( + "wpre/v1/handoff", + &serde_json::json!({ "redirect_to": target.as_str() }), + ) + .await + .expect("handoff"); + let url = url::Url::parse(&h.url).unwrap(); + assert!(crate::security::same_origin(&d.admin_url, &url)); + assert!(url + .query_pairs() + .any(|(k, v)| k == "action" && v == "wpre_handoff")); + println!(" handoff ok"); + } + } + + #[tokio::test] + #[ignore = "needs a live WordPress site"] + async fn wrong_password_is_unauthorized() { + let Some((site, user, _)) = env() else { return }; + let local = site.starts_with("http://"); + let d = discover(&site, local).await.unwrap(); + let client = WpClient::new( + d.api_root, + &Credentials::new(&user, "wrong wrong wrong"), + local, + ) + .unwrap(); + let err = client + .get::("wp/v2/users/me", &[]) + .await + .unwrap_err(); + assert!( + matches!(err, crate::error::AppError::Unauthorized), + "{err:?}" + ); + } +} diff --git a/src-tauri/src/wp/models.rs b/src-tauri/src/wp/models.rs new file mode 100644 index 0000000..eecbb87 --- /dev/null +++ b/src-tauri/src/wp/models.rs @@ -0,0 +1,242 @@ +//! WordPress REST API shapes (only the fields the app uses) and the +//! camelCase DTOs sent to the shell UI. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +// ---- REST responses ------------------------------------------------------ + +#[derive(Debug, Deserialize)] +pub struct RestIndex { + #[serde(default)] + pub name: String, + #[serde(default)] + pub description: String, + /// `siteurl`: where wp-admin lives. + #[serde(default)] + pub url: String, + #[serde(default)] + pub namespaces: Vec, + #[serde(default)] + pub authentication: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +pub struct RestUser { + pub id: u64, + #[serde(default)] + pub name: String, + #[serde(default)] + pub avatar_urls: HashMap, + #[serde(default)] + pub roles: Vec, + #[serde(default)] + pub capabilities: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct RestPostType { + pub slug: String, + #[serde(default)] + pub name: String, + pub rest_base: String, + #[serde(default)] + pub rest_namespace: Option, + #[serde(default)] + pub viewable: bool, + #[serde(default)] + pub hierarchical: bool, + #[serde(default)] + pub capabilities: HashMap, + #[serde(default)] + pub labels: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct RestRendered { + #[serde(default)] + pub raw: Option, + #[serde(default)] + pub rendered: Option, +} + +#[derive(Debug, Deserialize)] +pub struct RestPost { + pub id: u64, + #[serde(default)] + pub title: Option, + #[serde(default)] + pub status: String, + #[serde(default)] + pub modified: Option, + #[serde(default)] + pub author: Option, + #[serde(default)] + pub link: Option, +} + +#[derive(Debug, Deserialize)] +pub struct RestErrorBody { + #[serde(default)] + pub code: String, + #[serde(default)] + pub message: String, +} + +// ---- DTOs for the shell UI ----------------------------------------------- + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteDiscovery { + pub site_url: Url, + pub admin_url: Url, + pub api_root: Url, + pub name: String, + pub description: String, + pub supports_application_passwords: bool, + #[serde(skip)] + pub authorization_endpoint: Option, + pub bridge_available: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteSummary { + pub id: uuid::Uuid, + pub name: String, + pub url: Url, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + pub bridge_available: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostTypeAccess { + pub slug: String, + pub rest_base: String, + pub name: String, + pub singular_name: String, + pub hierarchical: bool, + pub can_create: bool, + pub can_publish: bool, + pub can_edit_others: bool, + pub can_delete: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteContext { + pub site: SiteSummary, + pub user_id: u64, + pub roles: Vec, + pub post_types: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostSummary { + pub id: u64, + pub title: String, + pub status: String, + pub modified: Option, + pub author: Option, + pub link: Option, + pub can_edit: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostPage { + pub items: Vec, + pub total: u64, + pub total_pages: u64, + pub page: u32, +} + +/// Works out what the user may do with a post type from their capability map +/// and the post type's capability names. WordPress enforces all of this on +/// the server too; this only decides what the UI offers. +pub fn post_type_access(t: &RestPostType, caps: &HashMap) -> PostTypeAccess { + let has = |key: &str, fallback: &str| { + let cap = t + .capabilities + .get(key) + .map(String::as_str) + .unwrap_or(fallback); + caps.get(cap).copied().unwrap_or(false) + }; + let singular = t + .labels + .get("singular_name") + .and_then(|v| v.as_str()) + .unwrap_or(&t.name) + .to_string(); + PostTypeAccess { + slug: t.slug.clone(), + rest_base: t.rest_base.clone(), + name: t.name.clone(), + singular_name: singular, + hierarchical: t.hierarchical, + can_create: has("create_posts", "edit_posts") && has("edit_posts", "edit_posts"), + can_publish: has("publish_posts", "publish_posts"), + can_edit_others: has("edit_others_posts", "edit_others_posts"), + can_delete: has("delete_posts", "delete_posts"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn page_type() -> RestPostType { + serde_json::from_value(serde_json::json!({ + "slug": "page", "name": "Pages", "rest_base": "pages", "viewable": true, + "hierarchical": true, + "capabilities": { + "edit_posts": "edit_pages", "create_posts": "edit_pages", + "publish_posts": "publish_pages", "edit_others_posts": "edit_others_pages", + "delete_posts": "delete_pages" + }, + "labels": { "singular_name": "Page" } + })) + .unwrap() + } + + #[test] + fn contributor_cannot_create_pages() { + let caps = HashMap::from([("edit_posts".to_string(), true)]); + let a = post_type_access(&page_type(), &caps); + assert!(!a.can_create); + assert!(!a.can_publish); + } + + #[test] + fn editor_can_do_everything_with_pages() { + let caps: HashMap = [ + "edit_pages", + "publish_pages", + "edit_others_pages", + "delete_pages", + ] + .into_iter() + .map(|c| (c.to_string(), true)) + .collect(); + let a = post_type_access(&page_type(), &caps); + assert!(a.can_create && a.can_publish && a.can_edit_others && a.can_delete); + assert_eq!(a.singular_name, "Page"); + } + + #[test] + fn false_capabilities_are_respected() { + let caps = HashMap::from([ + ("edit_pages".to_string(), true), + ("publish_pages".to_string(), false), + ]); + let a = post_type_access(&page_type(), &caps); + assert!(a.can_create); + assert!(!a.can_publish); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..e970dcc --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "WordPress Remote Editor", + "mainBinaryName": "wordpress-remote-editor", + "version": "0.1.0", + "identifier": "com.wpremoteeditor.app", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "macOSPrivateApi": true, + "withGlobalTauri": false, + "windows": [ + { + "label": "main", + "title": "WordPress Remote Editor", + "width": 1280, + "height": 820, + "minWidth": 900, + "minHeight": 600, + "center": true, + "dragDropEnabled": false, + "create": false + } + ], + "security": { + "freezePrototype": true, + "csp": { + "default-src": "'self'", + "script-src": "'self'", + "style-src": "'self' 'unsafe-inline'", + "img-src": "'self' https: data:", + "font-src": "'self'", + "connect-src": "ipc: http://ipc.localhost", + "object-src": "'none'", + "frame-src": "'none'", + "base-uri": "'none'", + "form-action": "'none'" + } + } + }, + "bundle": { + "active": true, + "targets": [ + "app", + "dmg", + "deb", + "appimage", + "msi" + ], + "category": "Productivity", + "shortDescription": "A native desktop editor for WordPress sites", + "longDescription": "Write and publish posts and pages on your WordPress sites with the full block editor, including blocks from any plugin, in a native desktop app.", + "copyright": "\u00a9 WordPress Remote Editor contributors", + "license": "GPL-2.0-or-later", + "publisher": "WordPress Remote Editor", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "macOS": { + "minimumSystemVersion": "14.0", + "hardenedRuntime": true, + "entitlements": "entitlements.plist", + "dmg": { + "windowSize": { + "width": 660, + "height": 400 + }, + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + } + } + }, + "linux": { + "deb": { + "section": "editors", + "depends": [ + "libsecret-1-0" + ] + } + }, + "windows": { + "digestAlgorithm": "sha256", + "timestampUrl": "http://timestamp.digicert.com", + "webviewInstallMode": { + "type": "downloadBootstrapper", + "silent": true + }, + "wix": { + "language": "en-US" + } + } + } +} diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json new file mode 100644 index 0000000..d1f6590 --- /dev/null +++ b/src-tauri/tauri.macos.conf.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "app": { + "windows": [ + { + "label": "main", + "title": "WordPress Remote Editor", + "width": 1280, + "height": 820, + "minWidth": 900, + "minHeight": 600, + "center": true, + "dragDropEnabled": false, + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "transparent": true, + "trafficLightPosition": { + "x": 18, + "y": 22 + }, + "windowEffects": { + "effects": [ + "sidebar" + ], + "state": "followsWindowActiveState" + }, + "create": false + } + ] + } +} diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000..34f4c09 --- /dev/null +++ b/src-tauri/tauri.windows.conf.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "app": { + "windows": [ + { + "label": "main", + "title": "WordPress Remote Editor", + "width": 1280, + "height": 820, + "minWidth": 900, + "minHeight": 600, + "center": true, + "dragDropEnabled": false, + "transparent": true, + "windowEffects": { + "effects": [ + "mica" + ] + }, + "create": false + } + ] + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..be55b33 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,262 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { ask, message } from "@tauri-apps/plugin-dialog"; +import { api, errorMessage, type EditorReport, type PostTypeAccess, type SiteContext, type SiteSummary } from "./lib/api"; +import type { Platform } from "./lib/platform"; +import { AddSiteSheet } from "./components/AddSiteSheet"; +import { EditorView } from "./components/EditorView"; +import { Icon } from "./components/Icon"; +import { PostList } from "./components/PostList"; +import { Sidebar } from "./components/Sidebar"; + +type View = { kind: "list" } | { kind: "editor"; postId: number | null; key: number }; + +export function App({ platform }: { platform: Platform }) { + const [sites, setSites] = useState([]); + const [siteId, setSiteId] = useState(null); + const [context, setContext] = useState(null); + const [contextError, setContextError] = useState(null); + const [postType, setPostType] = useState(null); + const [view, setView] = useState({ kind: "list" }); + const [report, setReport] = useState(null); + const [adding, setAdding] = useState(false); + const [waitingForBrowser, setWaitingForBrowser] = useState(false); + const [authError, setAuthError] = useState(null); + const [reloadKey, setReloadKey] = useState(0); + + // Refs so native event handlers always see current state. + const state = useRef({ view, report, context, postType }); + state.current = { view, report, context, postType }; + + const refreshSites = useCallback(async () => { + const list = await api.listSites(); + setSites(list); + return list; + }, []); + + useEffect(() => { + void refreshSites().then((list) => list[0] && setSiteId((cur) => cur ?? list[0].id)); + }, [refreshSites]); + + // Load what the signed-in user can do on the selected site. + useEffect(() => { + setContext(null); + setPostType(null); + setContextError(null); + if (!siteId) return; + let cancelled = false; + api + .getSiteContext(siteId) + .then((ctx) => { + if (cancelled) return; + setContext(ctx); + setPostType(ctx.postTypes[0] ?? null); + }) + .catch((e) => !cancelled && setContextError(errorMessage(e))); + return () => { + cancelled = true; + }; + }, [siteId, reloadKey]); + + /** Leaves the editor, asking first if there are unsaved changes. */ + const leaveEditor = useCallback(async (): Promise => { + const { view, report } = state.current; + if (view.kind !== "editor") return true; + if (report?.dirty) { + const ok = await ask("You have unsaved changes. Leave the editor and discard them?", { + title: "Unsaved Changes", + kind: "warning", + okLabel: "Discard Changes", + cancelLabel: "Keep Editing", + }); + if (!ok) return false; + } + await api.closeEditor().catch(() => {}); + setReport(null); + setView({ kind: "list" }); + return true; + }, []); + + const openEditor = useCallback((postId: number | null) => { + setReport(null); + setView({ kind: "editor", postId, key: Date.now() }); + }, []); + + const selectSite = useCallback( + async (id: string) => { + if (await leaveEditor()) setSiteId(id); + }, + [leaveEditor], + ); + + const selectType = useCallback( + async (t: PostTypeAccess) => { + if (await leaveEditor()) setPostType(t); + }, + [leaveEditor], + ); + + const startAddSite = useCallback(async () => { + if (await leaveEditor()) { + setAuthError(null); + setWaitingForBrowser(false); + setAdding(true); + } + }, [leaveEditor]); + + const removeSite = useCallback( + async (site: SiteSummary) => { + const ok = await ask( + `Remove ${site.name} from this app? Its application password will be revoked on the site and deleted from your keychain.`, + { title: "Remove Site", kind: "warning", okLabel: "Remove", cancelLabel: "Cancel" }, + ); + if (!ok || !(await leaveEditor())) return; + try { + await api.removeSite(site.id); + } catch (e) { + await message(errorMessage(e), { title: "Couldn't remove site", kind: "error" }); + } + const list = await refreshSites(); + setSiteId(list[0]?.id ?? null); + }, + [leaveEditor, refreshSites], + ); + + const onSiteAdded = useCallback( + async (site: SiteSummary) => { + setAdding(false); + setWaitingForBrowser(false); + await refreshSites(); + setSiteId(site.id); + }, + [refreshSites], + ); + + // Native events: auth results, editor state, menu actions. + useEffect(() => { + const unlisten = [ + listen("auth://completed", (e) => void onSiteAdded(e.payload)), + listen("auth://failed", (e) => { + setWaitingForBrowser(false); + setAuthError(e.payload); + }), + listen("editor://state", (e) => setReport(e.payload)), + listen("menu://action", (e) => { + const { view, context } = state.current; + const inEditor = view.kind === "editor"; + const findType = (slug: string) => context?.postTypes.find((t) => t.slug === slug && t.canCreate); + switch (e.payload) { + case "new-post": + case "new-page": { + const t = findType(e.payload === "new-post" ? "post" : "page"); + if (!t) return; + void leaveEditor().then((ok) => { + if (!ok) return; + setPostType(t); + openEditor(null); + }); + return; + } + case "save": + if (inEditor) void api.editorCommand("save"); + return; + case "publish": + if (inEditor) void api.editorCommand("publish"); + return; + case "toggle-settings": + if (inEditor) void api.editorCommand("toggleSettings"); + return; + case "device-desktop": + case "device-tablet": + case "device-mobile": + if (inEditor) { + const d = e.payload.split("-")[1]; + void api.editorCommand(d === "desktop" ? "deviceDesktop" : d === "tablet" ? "deviceTablet" : "deviceMobile"); + } + return; + case "close-editor": + void leaveEditor(); + return; + case "add-site": + void startAddSite(); + return; + case "toggle-sidebar": + document.documentElement.classList.toggle("sidebar-hidden"); + window.dispatchEvent(new Event("resize")); + return; + } + }), + ]; + return () => unlisten.forEach((p) => void p.then((f) => f())); + }, [leaveEditor, onSiteAdded, openEditor, startAddSite]); + + return ( +
+ +
+ {sites.length === 0 ? ( + + ) : contextError ? ( +
+
{contextError}
+ +
+ ) : !context || !postType ? ( +
{context ? "Your role can't create or edit content on this site." : "Loading…"}
+ ) : view.kind === "editor" ? ( + void leaveEditor()} + /> + ) : ( + + )} +
+ {adding && ( + { + setAuthError(null); + setWaitingForBrowser(true); + }} + onAdded={onSiteAdded} + onCancel={() => setAdding(false)} + /> + )} +
+ ); +} + +function Welcome({ onAdd, platform }: { onAdd: () => void; platform: Platform }) { + return ( +
+
+ +
+

Welcome to WordPress Remote Editor

+

+ Write and publish on your WordPress sites with the full block editor, including blocks from your plugins. + Your sign-in is kept in the {platform === "macos" ? "macOS Keychain" : platform === "windows" ? "Windows Credential Manager" : "system keyring"}. +

+ +
+ ); +} diff --git a/src/components/AddSiteSheet.tsx b/src/components/AddSiteSheet.tsx new file mode 100644 index 0000000..07a9523 --- /dev/null +++ b/src/components/AddSiteSheet.tsx @@ -0,0 +1,202 @@ +import { useEffect, useRef, useState } from "react"; +import { api, errorMessage, type SiteDiscovery, type SiteSummary } from "../lib/api"; +import { hostOf } from "../lib/format"; +import { Icon } from "./Icon"; + +interface Props { + waitingForBrowser: boolean; + authError: string | null; + onStartedAuthorization: () => void; + onAdded: (site: SiteSummary) => void; + onCancel: () => void; +} + +type Step = "address" | "signin" | "manual"; + +export function AddSiteSheet({ waitingForBrowser, authError, onStartedAuthorization, onAdded, onCancel }: Props) { + const [step, setStep] = useState("address"); + const [url, setUrl] = useState(""); + const [allowLocal, setAllowLocal] = useState(false); + const [discovery, setDiscovery] = useState(null); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const firstField = useRef(null); + + useEffect(() => firstField.current?.focus(), [step]); + useEffect(() => setError(authError), [authError]); + useEffect(() => { + const onKey = (e: KeyboardEvent) => e.key === "Escape" && onCancel(); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onCancel]); + + async function findSite(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); + setError(null); + try { + const d = await api.discoverSite(url, allowLocal); + setDiscovery(d); + setStep(d.supportsApplicationPasswords ? "signin" : "manual"); + if (!d.supportsApplicationPasswords) { + setError("This site doesn't advertise Application Password sign-in. You can still try an application password created under Users → Profile."); + } + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + async function signInWithWordPress() { + setBusy(true); + setError(null); + try { + await api.beginAuthorization(discovery?.siteUrl ?? url, allowLocal); + onStartedAuthorization(); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + async function addManually(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); + setError(null); + try { + const site = await api.addSiteManual(discovery?.siteUrl ?? url, username, password, allowLocal); + setPassword(""); + onAdded(site); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + return ( +
e.target === e.currentTarget && onCancel()}> +
+

Add a WordPress Site

+ + {step === "address" && ( +
+

Enter the address of a site you can sign in to.

+ + + {error &&

{error}

} +
+ + +
+
+ )} + + {step === "signin" && discovery && ( +
+
+ +
+
{discovery.name}
+
{hostOf(discovery.siteUrl)}
+
+
+ {waitingForBrowser ? ( +

+ Approve WordPress Remote Editor in the sign-in window. This sheet updates when you're done. +

+ ) : ( +

+ You'll sign in on your site and approve this app. WordPress creates an Application Password just for this app, which you can revoke from your profile at any time. +

+ )} + {error &&

{error}

} +
+ +
+ + +
+
+
+ )} + + {step === "manual" && ( +
+

+ Create an Application Password in WordPress under Users → Profile → Application Passwords, then paste it here. It's stored only in your system keychain. +

+ + + {error &&

{error}

} +
+ +
+ + +
+
+
+ )} +
+
+ ); +} diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx new file mode 100644 index 0000000..797812e --- /dev/null +++ b/src/components/EditorView.tsx @@ -0,0 +1,145 @@ +import { useLayoutEffect, useRef, useState } from "react"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { api, errorMessage, type Bounds, type EditorReport, type PostTypeAccess, type SiteContext } from "../lib/api"; +import { statusLabel } from "../lib/format"; +import { Icon } from "./Icon"; + +interface Props { + context: SiteContext; + postType: PostTypeAccess; + postId: number | null; + report: EditorReport | null; + onBack: () => void; +} + +type Device = "Desktop" | "Tablet" | "Mobile"; + +function measure(el: HTMLElement): Bounds { + const r = el.getBoundingClientRect(); + return { x: r.left, y: r.top, width: r.width, height: r.height }; +} + +/** + * Native toolbar plus a placeholder region. The Rust core places the site's + * block editor (a separate, isolated webview) exactly over the placeholder. + */ +export function EditorView({ context, postType, postId, report, onBack }: Props) { + const host = useRef(null); + const [error, setError] = useState(null); + const [device, setDevice] = useState("Desktop"); + + useLayoutEffect(() => { + const el = host.current; + if (!el) return; + let cancelled = false; + setError(null); + api.openEditor(context.site.id, postType.slug, postId, measure(el)).catch((e) => { + if (!cancelled) setError(errorMessage(e)); + }); + let frame = 0; + const sync = () => { + cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => host.current && api.setEditorBounds(measure(host.current)).catch(() => {})); + }; + const ro = new ResizeObserver(sync); + ro.observe(el); + window.addEventListener("resize", sync); + return () => { + cancelled = true; + ro.disconnect(); + window.removeEventListener("resize", sync); + cancelAnimationFrame(frame); + }; + }, [context.site.id, postType.slug, postId]); + + const isEditor = report?.kind === "editor"; + const status = report?.status ?? ""; + const canPublish = report?.canPublish ?? postType.canPublish; + const published = status === "publish" || status === "future" || status === "private"; + const primaryLabel = published ? "Update" : canPublish ? "Publish…" : "Submit for Review"; + const title = isEditor ? report?.title || "Untitled" : postId ? "Loading…" : `New ${postType.singularName}`; + + const cmd = (c: Parameters[0]) => api.editorCommand(c).catch((e) => setError(errorMessage(e))); + const switchDevice = (d: Device) => { + setDevice(d); + void cmd(d === "Desktop" ? "deviceDesktop" : d === "Tablet" ? "deviceTablet" : "deviceMobile"); + }; + + return ( +
+
+ +
+ {title} + {isEditor && ( + + {report?.saving ? "Saving…" : report?.dirty ? "Edited" : statusLabel(status)} + + )} +
+
+ {isEditor && ( + <> +
+ + +
+
+ {(["Desktop", "Tablet", "Mobile"] as Device[]).map((d) => ( + + ))} +
+ {report?.link && status === "publish" && ( + + )} + + {!published && ( + + )} + + + )} +
+ + {report?.kind === "login" && ( +
+ Sign in to {context.site.name} once below to open the editor. + {!context.site.bridgeAvailable && " Install the Remote Editor Bridge plugin on the site to skip this step."} +
+ )} + {error && ( +
+ {error} +
+ )} + +
+
Loading the block editor…
+
+
+ ); +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx new file mode 100644 index 0000000..06488e2 --- /dev/null +++ b/src/components/Icon.tsx @@ -0,0 +1,42 @@ +// Small inline icon set (stroke icons, 20px grid) so nothing loads remotely. +const paths: Record = { + plus: "M10 4v12M4 10h12", + back: "M12.5 4.5 7 10l5.5 5.5", + search: "M9 15a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm4.5-1.5L17 17", + post: "M5 3.5h10v13H5zM7.5 7h5M7.5 10h5M7.5 13h3", + page: "M6 2.5h5.5L15 6v11.5H6zM11.5 2.5V6H15", + type: "M4 5h12M4 10h12M4 15h8", + undo: "M7 8H13a3.5 3.5 0 0 1 0 7H9M7 8l3-3M7 8l3 3", + redo: "M13 8H7a3.5 3.5 0 0 0 0 7h4M13 8l-3-3M13 8l-3 3", + settings: "M3.5 4.5h13v11h-13zM12 4.5v11", + desktop: "M3 4h14v9H3zM7.5 16.5h5M10 13v3.5", + tablet: "M5.5 2.5h9v15h-9zM9 15h2", + mobile: "M7 2.5h6v15H7zM9.5 15h1", + external: "M11 3.5h5.5V9M16.5 3.5 9 11M14 12v4.5H3.5V6H8", + trash: "M4 6h12M8 6V4h4v2M5.5 6l.8 10.5h7.4L14.5 6", + more: "M5 10h.01M10 10h.01M15 10h.01", + globe: "M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14ZM3 10h14M10 3c2 2 2.8 4.5 2.8 7S12 15 10 17c-2-2-2.8-4.5-2.8-7S8 5 10 3Z", + lock: "M5.5 9h9v7.5h-9zM7.5 9V6.5a2.5 2.5 0 0 1 5 0V9", + check: "M4.5 10.5 8 14l7.5-8", + refresh: "M15.5 8A6 6 0 0 0 4.6 6.5M4.5 3.5v3h3M4.5 12a6 6 0 0 0 10.9 1.5M15.5 16.5v-3h-3", +}; + +export function Icon({ name, size = 18, title }: { name: keyof typeof paths | string; size?: number; title?: string }) { + return ( + + {title ? {title} : null} + + + ); +} diff --git a/src/components/PostList.tsx b/src/components/PostList.tsx new file mode 100644 index 0000000..dc754f8 --- /dev/null +++ b/src/components/PostList.tsx @@ -0,0 +1,207 @@ +import { useCallback, useEffect, useState } from "react"; +import { ask } from "@tauri-apps/plugin-dialog"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { api, errorMessage, type PostSummary, type PostTypeAccess, type SiteContext, type StatusFilter } from "../lib/api"; +import { relativeTime, statusLabel } from "../lib/format"; +import { Icon } from "./Icon"; + +interface Props { + context: SiteContext; + postType: PostTypeAccess; + onOpen: (postId: number | null) => void; +} + +const FILTERS: { id: StatusFilter; label: string }[] = [ + { id: "any", label: "All" }, + { id: "draft", label: "Drafts" }, + { id: "pending", label: "Pending" }, + { id: "future", label: "Scheduled" }, + { id: "publish", label: "Published" }, +]; + +export function PostList({ context, postType, onOpen }: Props) { + const [filter, setFilter] = useState("any"); + const [mineOnly, setMineOnly] = useState(!postType.canEditOthers); + const [search, setSearch] = useState(""); + const [query, setQuery] = useState(""); + const [items, setItems] = useState([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selected, setSelected] = useState(null); + + useEffect(() => setMineOnly(!postType.canEditOthers), [postType]); + + // Debounce search input. + useEffect(() => { + const t = window.setTimeout(() => setQuery(search), 300); + return () => window.clearTimeout(t); + }, [search]); + + const load = useCallback( + async (nextPage: number) => { + setLoading(true); + setError(null); + try { + const res = await api.listPosts({ + siteId: context.site.id, + restBase: postType.restBase, + status: filter, + search: query, + page: nextPage, + mineOnly, + canEditOthers: postType.canEditOthers, + userId: context.userId, + }); + setItems((prev) => (nextPage === 1 ? res.items : [...prev, ...res.items])); + setPage(res.page); + setTotalPages(res.totalPages); + } catch (e) { + setError(errorMessage(e)); + if (nextPage === 1) setItems([]); + } finally { + setLoading(false); + } + }, + [context.site.id, context.userId, postType, filter, query, mineOnly], + ); + + useEffect(() => { + void load(1); + }, [load]); + + async function trash(p: PostSummary) { + const ok = await ask(`Move “${p.title || "(no title)"}” to the Trash? You can restore it from WordPress.`, { + title: "Move to Trash", + kind: "warning", + okLabel: "Move to Trash", + cancelLabel: "Cancel", + }); + if (!ok) return; + try { + await api.trashPost(context.site.id, postType.restBase, p.id); + setItems((prev) => prev.filter((x) => x.id !== p.id)); + } catch (e) { + setError(errorMessage(e)); + } + } + + return ( +
+
+

{postType.name}

+
+ {FILTERS.map((f) => ( + + ))} +
+
+ + + {postType.canCreate && ( + + )} +
+ + {postType.canEditOthers && ( +
+ +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ {items.map((p) => ( +
setSelected(p.id)} + onDoubleClick={() => p.canEdit && onOpen(p.id)} + onKeyDown={(e) => { + if (e.key === "Enter" && p.canEdit) onOpen(p.id); + }} + > +
+
{p.title || (no title)}
+
+ {statusLabel(p.status)} + {relativeTime(p.modified)} +
+
+
+ {p.link && p.status === "publish" && ( + + )} + {p.canEdit && postType.canDelete && ( + + )} + {p.canEdit && ( + + )} +
+
+ ))} + + {!loading && items.length === 0 && !error && ( +
+ +

No {postType.name.toLowerCase()} here yet.

+ {postType.canCreate && ( + + )} +
+ )} + + {loading &&
Loading…
} + {!loading && page < totalPages && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..eaf67dc --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,97 @@ +import type { PostTypeAccess, SiteContext, SiteSummary } from "../lib/api"; +import { roleLabel } from "../lib/format"; +import { Icon } from "./Icon"; +import { SiteAvatar, UserAvatar } from "./SiteAvatar"; + +interface Props { + sites: SiteSummary[]; + selectedSiteId: string | null; + context: SiteContext | null; + selectedType: string | null; + onSelectSite: (id: string) => void; + onSelectType: (t: PostTypeAccess) => void; + onAddSite: () => void; + onRemoveSite: (site: SiteSummary) => void; +} + +export function Sidebar(props: Props) { + const { sites, selectedSiteId, context, selectedType } = props; + return ( + + ); +} diff --git a/src/components/SiteAvatar.tsx b/src/components/SiteAvatar.tsx new file mode 100644 index 0000000..6b99ed5 --- /dev/null +++ b/src/components/SiteAvatar.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; +import type { SiteSummary } from "../lib/api"; + +/** Site initial in a tinted tile; deterministic colour per site. */ +export function SiteAvatar({ site, size = 22 }: { site: Pick; size?: number }) { + let hash = 0; + for (const ch of site.id) hash = (hash * 31 + ch.charCodeAt(0)) | 0; + const hue = Math.abs(hash) % 360; + return ( + + {(site.name.trim()[0] ?? "W").toUpperCase()} + + ); +} + +export function UserAvatar({ url, name, size = 24 }: { url: string | null; name: string; size?: number }) { + const [failed, setFailed] = useState(false); + if (!url || failed) { + return ( + + {(name.trim()[0] ?? "?").toUpperCase()} + + ); + } + return ( + setFailed(true)} + /> + ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..adfeb2d --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,127 @@ +// Typed wrappers around the Rust core's IPC commands. The shell never talks +// to WordPress directly and never handles passwords after submitting them. +import { invoke } from "@tauri-apps/api/core"; + +export interface SiteSummary { + id: string; + name: string; + url: string; + username: string; + displayName: string; + avatarUrl: string | null; + bridgeAvailable: boolean; +} + +export interface SiteDiscovery { + siteUrl: string; + adminUrl: string; + apiRoot: string; + name: string; + description: string; + supportsApplicationPasswords: boolean; + bridgeAvailable: boolean; +} + +export interface PostTypeAccess { + slug: string; + restBase: string; + name: string; + singularName: string; + hierarchical: boolean; + canCreate: boolean; + canPublish: boolean; + canEditOthers: boolean; + canDelete: boolean; +} + +export interface SiteContext { + site: SiteSummary; + userId: number; + roles: string[]; + postTypes: PostTypeAccess[]; +} + +export interface PostSummary { + id: number; + title: string; + status: string; + modified: string | null; + author: number | null; + link: string | null; + canEdit: boolean; +} + +export interface PostPage { + items: PostSummary[]; + total: number; + totalPages: number; + page: number; +} + +export interface EditorReport { + kind: "editor" | "login" | "page"; + postId?: number | null; + postType?: string | null; + title?: string | null; + status?: string | null; + link?: string | null; + dirty?: boolean | null; + saving?: boolean | null; + canPublish?: boolean | null; + path?: string | null; +} + +export interface Bounds { + x: number; + y: number; + width: number; + height: number; +} + +export type EditorCommand = + | "save" + | "publish" + | "undo" + | "redo" + | "toggleSettings" + | "deviceDesktop" + | "deviceTablet" + | "deviceMobile"; + +export type StatusFilter = "any" | "draft" | "pending" | "future" | "publish" | "private" | "trash"; + +export const api = { + discoverSite: (url: string, allowInsecureLocal = false) => + invoke("discover_site", { url, allowInsecureLocal }), + beginAuthorization: (url: string, allowInsecureLocal = false) => + invoke("begin_authorization", { url, allowInsecureLocal }), + addSiteManual: (url: string, username: string, appPassword: string, allowInsecureLocal = false) => + invoke("add_site_manual", { url, username, appPassword, allowInsecureLocal }), + listSites: () => invoke("list_sites"), + removeSite: (siteId: string) => invoke("remove_site", { siteId }), + getSiteContext: (siteId: string) => invoke("get_site_context", { siteId }), + listPosts: (args: { + siteId: string; + restBase: string; + status: StatusFilter; + search: string; + page: number; + mineOnly: boolean; + canEditOthers: boolean; + userId: number; + }) => invoke("list_posts", { query: args }), + trashPost: (siteId: string, restBase: string, postId: number) => + invoke("trash_post", { siteId, restBase, postId }), + openEditor: (siteId: string, postType: string, postId: number | null, bounds: Bounds) => + invoke("open_editor", { siteId, postType, postId, bounds }), + closeEditor: () => invoke("close_editor"), + setEditorBounds: (bounds: Bounds) => invoke("editor_set_bounds", { bounds }), + editorCommand: (command: EditorCommand) => invoke("editor_command", { command }), +}; + +/** Errors from the core arrive as plain user-facing strings. */ +export function errorMessage(e: unknown): string { + if (typeof e === "string") return e; + if (e instanceof Error) return e.message; + return "Something went wrong."; +} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts new file mode 100644 index 0000000..57d7658 --- /dev/null +++ b/src/lib/format.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { hostOf, relativeTime, roleLabel, statusLabel } from "./format"; + +describe("format helpers", () => { + it("labels WordPress statuses", () => { + expect(statusLabel("publish")).toBe("Published"); + expect(statusLabel("future")).toBe("Scheduled"); + expect(statusLabel("custom")).toBe("Custom"); + expect(statusLabel(null)).toBe(""); + }); + + it("formats relative times", () => { + const now = new Date("2026-10-13T12:00:00"); + expect(relativeTime("2026-10-13T11:58:00", now)).toMatch(/2 minutes ago/); + expect(relativeTime("not a date", now)).toBe(""); + expect(relativeTime(null, now)).toBe(""); + }); + + it("labels roles", () => { + expect(roleLabel(["editor"])).toBe("Editor"); + expect(roleLabel(["shop_manager"])).toBe("shop manager"); + }); + + it("extracts hosts", () => { + expect(hostOf("https://example.com/blog/")).toBe("example.com"); + }); +}); diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..082effa --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,49 @@ +const STATUS_LABELS: Record = { + publish: "Published", + draft: "Draft", + pending: "Pending Review", + future: "Scheduled", + private: "Private", + trash: "Trash", + "auto-draft": "Draft", +}; + +export function statusLabel(status: string | null | undefined): string { + if (!status) return ""; + return STATUS_LABELS[status] ?? status.charAt(0).toUpperCase() + status.slice(1); +} + +/** WordPress returns `modified` as local site time without a zone. */ +export function relativeTime(iso: string | null, now: Date = new Date()): string { + if (!iso) return ""; + const then = new Date(iso); + if (Number.isNaN(then.getTime())) return ""; + const seconds = Math.round((now.getTime() - then.getTime()) / 1000); + const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); + const abs = Math.abs(seconds); + if (abs < 60) return rtf.format(-seconds, "second"); + if (abs < 3600) return rtf.format(-Math.round(seconds / 60), "minute"); + if (abs < 86400) return rtf.format(-Math.round(seconds / 3600), "hour"); + if (abs < 86400 * 7) return rtf.format(-Math.round(seconds / 86400), "day"); + return then.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); +} + +/** Turns WordPress role slugs into readable names. */ +export function roleLabel(roles: string[]): string { + const known: Record = { + administrator: "Administrator", + editor: "Editor", + author: "Author", + contributor: "Contributor", + subscriber: "Subscriber", + }; + return roles.map((r) => known[r] ?? r.replace(/[_-]+/g, " ")).join(", "); +} + +export function hostOf(url: string): string { + try { + return new URL(url).host; + } catch { + return url; + } +} diff --git a/src/lib/platform.ts b/src/lib/platform.ts new file mode 100644 index 0000000..8ef72af --- /dev/null +++ b/src/lib/platform.ts @@ -0,0 +1,20 @@ +export type Platform = "macos" | "windows" | "linux"; + +/** Resolves the host OS so the shell can adopt that platform's conventions. */ +export async function detectPlatform(): Promise { + try { + const os = await import("@tauri-apps/plugin-os"); + const p = os.platform(); + if (p === "macos" || p === "windows") return p; + return "linux"; + } catch { + const ua = navigator.userAgent; + if (/Mac/.test(ua)) return "macos"; + if (/Windows/.test(ua)) return "windows"; + return "linux"; + } +} + +export function modKey(platform: Platform): string { + return platform === "macos" ? "⌘" : "Ctrl+"; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..28e81d0 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import { detectPlatform } from "./lib/platform"; +import "./styles/app.css"; + +void detectPlatform().then((platform) => { + document.documentElement.dataset.platform = platform; + createRoot(document.getElementById("root")!).render( + + + , + ); +}); + +// Native apps don't show a browser context menu on chrome surfaces. +window.addEventListener("contextmenu", (e) => { + const t = e.target as HTMLElement | null; + if (!t?.closest("input, textarea, [contenteditable='true']")) e.preventDefault(); +}); diff --git a/src/styles/app.css b/src/styles/app.css new file mode 100644 index 0000000..0282ed8 --- /dev/null +++ b/src/styles/app.css @@ -0,0 +1,869 @@ +/* + * Platform-adaptive shell styles. `data-platform` on selects macOS, + * Windows (Fluent) or Linux (libadwaita-like) metrics; the OS light/dark + * setting is followed through prefers-color-scheme. + */ + +/* ---- Base tokens (Linux / GNOME defaults) ------------------------------- */ +:root { + --font: "Cantarell", "Inter", "Noto Sans", "Ubuntu", system-ui, sans-serif; + --font-size: 14px; + --accent: #3584e4; + --accent-text: #ffffff; + --text: #1e1e1e; + --text-muted: rgba(0, 0, 0, 0.55); + --window-bg: #fafafa; + --sidebar-bg: #ebebed; + --content-bg: #ffffff; + --toolbar-bg: #ffffff; + --border: rgba(0, 0, 0, 0.1); + --hover: rgba(0, 0, 0, 0.05); + --selected: rgba(0, 0, 0, 0.08); + --selected-text: var(--text); + --control-bg: rgba(0, 0, 0, 0.06); + --control-bg-hover: rgba(0, 0, 0, 0.1); + --field-bg: #ffffff; + --danger: #c01c28; + --success: #26a269; + --warning: #c64600; + --radius: 8px; + --radius-small: 6px; + --control-height: 34px; + --toolbar-height: 47px; + --sidebar-width: 248px; + --titlebar-inset: 0px; + --focus-ring: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent); + --shadow-sheet: 0 12px 40px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(0, 0, 0, 0.06); + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #ffffff; + --text-muted: rgba(255, 255, 255, 0.6); + --window-bg: #242424; + --sidebar-bg: #2e2e32; + --content-bg: #1e1e1e; + --toolbar-bg: #2a2a2a; + --border: rgba(255, 255, 255, 0.1); + --hover: rgba(255, 255, 255, 0.06); + --selected: rgba(255, 255, 255, 0.1); + --control-bg: rgba(255, 255, 255, 0.1); + --control-bg-hover: rgba(255, 255, 255, 0.15); + --field-bg: rgba(255, 255, 255, 0.08); + --danger: #ff7b63; + --success: #8ff0a4; + --warning: #ffa348; + } +} + +/* ---- macOS ------------------------------------------------------------- */ +:root[data-platform="macos"] { + --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; + --font-size: 13px; + --accent: AccentColor; + --accent-text: AccentColorText; + --window-bg: transparent; + --sidebar-bg: transparent; /* native sidebar vibrancy shows through */ + --content-bg: #ffffff; + --toolbar-bg: #ffffff; + --selected: rgba(0, 0, 0, 0.1); + --control-bg: #ffffff; + --control-bg-hover: #f5f5f5; + --radius: 6px; + --radius-small: 5px; + --control-height: 24px; + --toolbar-height: 52px; + --sidebar-width: 232px; + --titlebar-inset: 40px; /* room for the traffic lights */ + --shadow-sheet: 0 20px 60px rgba(0, 0, 0, 0.3), 0 0 0 0.5px rgba(0, 0, 0, 0.2); +} +@supports not (color: AccentColor) { + :root[data-platform="macos"] { + --accent: #007aff; + --accent-text: #ffffff; + } +} +@media (prefers-color-scheme: dark) { + :root[data-platform="macos"] { + --content-bg: #1e1e1e; + --toolbar-bg: #2b2b2b; + --selected: rgba(255, 255, 255, 0.12); + --control-bg: rgba(255, 255, 255, 0.14); + --control-bg-hover: rgba(255, 255, 255, 0.2); + } +} + +/* ---- Windows 11 (Fluent) ----------------------------------------------- */ +:root[data-platform="windows"] { + --font: "Segoe UI Variable Text", "Segoe UI Variable", "Segoe UI", system-ui, sans-serif; + --font-size: 14px; + --accent: AccentColor; + --accent-text: AccentColorText; + --window-bg: transparent; + --sidebar-bg: transparent; /* Mica shows through */ + --content-bg: rgba(255, 255, 255, 0.7); + --toolbar-bg: transparent; + --hover: rgba(0, 0, 0, 0.037); + --selected: rgba(0, 0, 0, 0.06); + --control-bg: rgba(255, 255, 255, 0.7); + --control-bg-hover: rgba(249, 249, 249, 0.5); + --radius: 8px; + --radius-small: 4px; + --control-height: 32px; + --toolbar-height: 52px; + --sidebar-width: 260px; +} +@supports not (color: AccentColor) { + :root[data-platform="windows"] { + --accent: #005fb8; + --accent-text: #ffffff; + } +} +@media (prefers-color-scheme: dark) { + :root[data-platform="windows"] { + --content-bg: rgba(58, 58, 58, 0.3); + --hover: rgba(255, 255, 255, 0.06); + --selected: rgba(255, 255, 255, 0.08); + --control-bg: rgba(255, 255, 255, 0.06); + --control-bg-hover: rgba(255, 255, 255, 0.08); + } +} + +/* ---- Reset ------------------------------------------------------------- */ +*, +*::before, +*::after { + box-sizing: border-box; +} +html, +body, +#root { + height: 100%; + margin: 0; + overflow: hidden; +} +body { + font-family: var(--font); + font-size: var(--font-size); + color: var(--text); + background: var(--window-bg); + -webkit-font-smoothing: antialiased; + user-select: none; + -webkit-user-select: none; + cursor: default; +} +input, +textarea, +.row-title { + user-select: text; + -webkit-user-select: text; +} +button, +input { + font: inherit; + color: inherit; +} +:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} +h1, +h2 { + margin: 0; + font-weight: 600; +} +.muted { + color: var(--text-muted); +} + +/* ---- Layout ------------------------------------------------------------ */ +.app { + display: flex; + height: 100%; +} +.main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + background: var(--content-bg); +} +:root[data-platform="windows"] .main { + border-top-left-radius: 8px; + border-left: 1px solid var(--border); + border-top: 1px solid var(--border); + margin-top: 0; +} +.sidebar-hidden .sidebar, +.app.editor-focus .sidebar { + display: none; +} +/* Leave room for the traffic lights when the sidebar is hidden. */ +:root[data-platform="macos"] .app.editor-focus .toolbar, +:root[data-platform="macos"].sidebar-hidden .toolbar { + padding-left: 84px; +} +.centered { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; + align-items: center; + justify-content: center; + padding: 24px; +} + +/* ---- Sidebar ----------------------------------------------------------- */ +.sidebar { + width: var(--sidebar-width); + flex-shrink: 0; + display: flex; + flex-direction: column; + background: var(--sidebar-bg); + border-right: 1px solid var(--border); +} +:root[data-platform="macos"] .sidebar, +:root[data-platform="windows"] .sidebar { + border-right: none; +} +.sidebar-titlebar { + height: var(--titlebar-inset); + flex-shrink: 0; +} +.sidebar-scroll { + flex: 1; + overflow-y: auto; + padding: 8px 10px; +} +.sidebar-section + .sidebar-section { + margin-top: 14px; +} +.sidebar-section ul { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar-heading { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + padding: 4px 8px; +} +:root[data-platform="linux"] .sidebar-heading { + font-size: 12px; + font-weight: 700; +} +.sidebar-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-height: 28px; + padding: 4px 8px; + border: none; + background: transparent; + border-radius: var(--radius-small); + text-align: left; +} +:root[data-platform="windows"] .sidebar-item { + min-height: 36px; + padding: 0 12px; + position: relative; +} +:root[data-platform="linux"] .sidebar-item { + min-height: 36px; + padding: 0 10px; +} +.sidebar-item:hover { + background: var(--hover); +} +.sidebar-item.selected { + background: var(--selected); + color: var(--selected-text); +} +:root[data-platform="windows"] .sidebar-item.selected::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 3px; + height: 16px; + margin-top: -8px; + border-radius: 2px; + background: var(--accent); +} +.sidebar-item .label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.sidebar-item.subtle { + color: var(--text-muted); +} +.sidebar-item svg { + color: var(--accent); + flex-shrink: 0; +} +.icon-tile { + display: inline-flex; + width: 22px; + height: 22px; + align-items: center; + justify-content: center; + border-radius: 5px; + background: var(--control-bg); +} +.badge { + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + background: var(--control-bg); + color: var(--text-muted); +} +.site-avatar, +.user-avatar.fallback { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 5px; + color: #fff; + font-weight: 600; + flex-shrink: 0; +} +.user-avatar { + border-radius: 50%; + flex-shrink: 0; +} +.user-avatar.fallback { + border-radius: 50%; + background: var(--control-bg-hover); + color: var(--text); +} +.sidebar-footer { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-top: 1px solid var(--border); +} +.sidebar-footer .who { + flex: 1; + min-width: 0; +} +.sidebar-footer .name { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.sidebar-footer .role { + font-size: 11px; + color: var(--text-muted); +} + +/* ---- Toolbar ----------------------------------------------------------- */ +.content { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +.toolbar { + display: flex; + align-items: center; + gap: 8px; + height: var(--toolbar-height); + padding: 0 14px; + background: var(--toolbar-bg); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} +.toolbar-title { + font-size: 15px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 40vw; +} +:root[data-platform="macos"] .toolbar-title { + font-size: 13px; + font-weight: 700; +} +:root[data-platform="windows"] .toolbar-title { + font-size: 20px; + font-weight: 600; +} +.toolbar-title-group { + display: flex; + flex-direction: column; + min-width: 0; + line-height: 1.2; +} +.toolbar-subtitle { + font-size: 11px; + color: var(--text-muted); +} +.toolbar-spacer { + flex: 1; + align-self: stretch; +} +.subbar { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 16px; + border-bottom: 1px solid var(--border); +} + +/* ---- Controls ---------------------------------------------------------- */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: var(--control-height); + padding: 0 14px; + border-radius: var(--radius-small); + border: 1px solid var(--border); + background: var(--control-bg); + white-space: nowrap; +} +.button:hover:not(:disabled) { + background: var(--control-bg-hover); +} +.button:disabled { + opacity: 0.5; +} +.button.primary { + background: var(--accent); + color: var(--accent-text); + border-color: transparent; +} +.button.primary:hover:not(:disabled) { + filter: brightness(1.08); + background: var(--accent); +} +.button.borderless { + border-color: transparent; + background: transparent; + padding: 0 8px; +} +.button.borderless:hover { + background: var(--hover); +} +.button.small { + height: calc(var(--control-height) - 4px); + padding: 0 10px; +} +.button.large { + height: 40px; + padding: 0 22px; + font-size: 15px; +} +:root[data-platform="macos"] .button { + padding: 0 10px; + box-shadow: 0 0.5px 1px rgba(0, 0, 0, 0.12); + border-color: rgba(0, 0, 0, 0.08); +} +:root[data-platform="macos"] .button.large { + height: 32px; + font-size: 13px; +} +:root[data-platform="linux"] .button { + border: none; + font-weight: 600; +} +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--control-height); + height: var(--control-height); + min-width: 28px; + min-height: 28px; + border: none; + border-radius: var(--radius-small); + background: transparent; + color: var(--text-muted); +} +.icon-button:hover { + background: var(--hover); + color: var(--text); +} +.icon-button.danger:hover { + color: var(--danger); +} +.button-group { + display: inline-flex; +} +.link-button { + border: none; + background: none; + color: var(--accent); + padding: 0; +} +.link-button:hover { + text-decoration: underline; +} +.segmented { + display: inline-flex; + padding: 2px; + gap: 2px; + border-radius: var(--radius); + background: var(--control-bg); + border: 1px solid var(--border); +} +.segmented button { + border: none; + background: transparent; + padding: 0 10px; + height: calc(var(--control-height) - 6px); + min-height: 20px; + border-radius: calc(var(--radius) - 2px); + color: var(--text-muted); + display: inline-flex; + align-items: center; +} +.segmented button.active { + background: var(--content-bg); + color: var(--text); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); +} +:root[data-platform="macos"] .segmented button.active { + background: #fff; +} +@media (prefers-color-scheme: dark) { + .segmented button.active, + :root[data-platform="macos"] .segmented button.active { + background: rgba(255, 255, 255, 0.18); + } +} +.segmented.icons button { + padding: 0 6px; +} +.search-field { + display: inline-flex; + align-items: center; + gap: 6px; + height: var(--control-height); + min-height: 26px; + padding: 0 8px; + border-radius: var(--radius-small); + background: var(--field-bg); + border: 1px solid var(--border); + color: var(--text-muted); + width: 200px; +} +.search-field input { + border: none; + outline: none; + background: transparent; + width: 100%; + color: var(--text); +} +.field { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 12px; +} +.field span { + font-weight: 500; +} +.field input { + height: 32px; + padding: 0 10px; + border-radius: var(--radius-small); + border: 1px solid var(--border); + background: var(--field-bg); +} +:root[data-platform="windows"] .field input { + border-bottom: 2px solid var(--text-muted); +} +:root[data-platform="windows"] .field input:focus { + border-bottom-color: var(--accent); + box-shadow: none; +} +.checkbox { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 4px 0 12px; + color: var(--text-muted); +} +.checkbox.small { + margin: 0; + align-items: center; +} +.checkbox input { + accent-color: var(--accent); + margin: 2px 0 0; +} + +/* ---- Banners ----------------------------------------------------------- */ +.banner { + padding: 8px 16px; + font-size: 13px; + border-bottom: 1px solid var(--border); +} +.banner.error { + color: var(--danger); + background: color-mix(in srgb, var(--danger) 9%, transparent); +} +.banner.info { + background: color-mix(in srgb, var(--accent) 10%, transparent); +} +.centered .banner { + border: none; + border-radius: var(--radius); +} + +/* ---- Post list --------------------------------------------------------- */ +.list { + flex: 1; + overflow-y: auto; + padding: 6px 8px 16px; +} +.row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: var(--radius); + outline: none; +} +.row + .row { + margin-top: 1px; +} +.row:hover { + background: var(--hover); +} +.row.selected { + background: var(--selected); +} +:root[data-platform="macos"] .row.selected { + background: var(--accent); + color: var(--accent-text); +} +:root[data-platform="macos"] .row.selected .muted, +:root[data-platform="macos"] .row.selected .status, +:root[data-platform="macos"] .row.selected .icon-button { + color: inherit; + opacity: 0.85; +} +.row:focus-visible { + box-shadow: var(--focus-ring); +} +.row.readonly .row-title { + color: var(--text-muted); +} +.row-main { + flex: 1; + min-width: 0; +} +.row-title { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.row-meta { + display: flex; + gap: 10px; + font-size: 12px; + margin-top: 2px; +} +.row-actions { + display: flex; + align-items: center; + gap: 4px; + opacity: 0; +} +.row:hover .row-actions, +.row.selected .row-actions, +.row:focus-within .row-actions { + opacity: 1; +} +.status { + font-weight: 500; +} +.status-publish { + color: var(--success); +} +.status-draft { + color: var(--text-muted); +} +.status-pending { + color: var(--warning); +} +.status-future { + color: var(--accent); +} +.empty, +.loading, +.load-more { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 48px 16px; + color: var(--text-muted); +} +.load-more { + padding: 16px; +} + +/* ---- Editor ------------------------------------------------------------ */ +.editor-host { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + background: var(--content-bg); +} +.editor-loading { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); +} + +/* ---- Welcome ----------------------------------------------------------- */ +.welcome { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 14px; + padding: 32px; +} +.welcome p { + max-width: 460px; + color: var(--text-muted); + line-height: 1.5; + margin: 0 0 8px; +} +.welcome-icon { + width: 84px; + height: 84px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + background: linear-gradient(180deg, #466cf0, #1d35b8); + box-shadow: 0 8px 24px rgba(29, 53, 184, 0.35); +} +.welcome h1 { + font-size: 22px; +} + +/* ---- Sheet (modal) ----------------------------------------------------- */ +.sheet-backdrop { + position: fixed; + inset: 0; + display: flex; + align-items: flex-start; + justify-content: center; + background: rgba(0, 0, 0, 0.18); + animation: fade 120ms ease-out; +} +:root[data-platform="windows"] .sheet-backdrop, +:root[data-platform="linux"] .sheet-backdrop { + align-items: center; + background: rgba(0, 0, 0, 0.32); +} +.sheet { + width: min(480px, calc(100vw - 32px)); + margin-top: var(--titlebar-inset); + padding: 22px 24px 18px; + border-radius: 12px; + background: var(--content-bg); + box-shadow: var(--shadow-sheet); + animation: drop 160ms ease-out; +} +:root[data-platform="macos"] .sheet { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +:root[data-platform="windows"] .sheet, +:root[data-platform="linux"] .sheet { + margin-top: 0; +} +:root[data-platform="windows"] .sheet { + background: color-mix(in srgb, var(--content-bg) 100%, #f3f3f3); + border-radius: 8px; +} +.sheet h2 { + font-size: 17px; + margin-bottom: 8px; +} +.sheet-intro { + color: var(--text-muted); + line-height: 1.45; + margin: 0 0 14px; +} +.sheet-intro.waiting { + color: var(--text); +} +.sheet-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 8px; +} +.sheet-actions.spread { + justify-content: space-between; + align-items: center; +} +.form-error { + color: var(--danger); + margin: 0 0 8px; + line-height: 1.4; +} +.site-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + margin-bottom: 14px; + border-radius: var(--radius); + background: var(--control-bg); + border: 1px solid var(--border); +} +.site-card svg { + color: var(--accent); +} +.site-card-name { + font-weight: 600; +} +.site-card-host { + color: var(--text-muted); + font-size: 12px; +} + +@keyframes fade { + from { + opacity: 0; + } +} +@keyframes drop { + from { + transform: translateY(-12px); + opacity: 0; + } +} +@media (prefers-reduced-motion: reduce) { + .sheet, + .sheet-backdrop { + animation: none; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6d77e81 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest/globals", "node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f8b47ba --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +const host = process.env.TAURI_DEV_HOST; + +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host ? { protocol: "ws", host, port: 1421 } : undefined, + watch: { ignored: ["**/src-tauri/**"] }, + }, + build: { + target: ["es2022", "safari16"], + sourcemap: false, + }, + test: { + environment: "jsdom", + }, +});