diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f8c3fcc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/src-tauri" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..cbf5f4a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,129 @@ +name: Build Desktop App + +on: + workflow_dispatch: + push: + branches: + - main + - master + paths-ignore: + - "README.md" + - "AGENTS.md" + - "CHANGES_PENDING.md" + - "docs/**" + pull_request: + branches: + - main + - master + paths-ignore: + - "README.md" + - "AGENTS.md" + - "CHANGES_PENDING.md" + - "docs/**" + +permissions: + contents: read + +env: + TAURI_CLI_VERSION: "2.10.0" + +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +jobs: + quick-check: + name: Quick Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install frontend dependencies + run: npm ci + + - name: Frontend build check + run: npm run build + + tauri-build: + if: github.event_name != 'pull_request' + needs: quick-check + name: Tauri (${{ matrix.platform }}) + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + include: + - platform: ubuntu-22.04 + artifact: linux + - platform: windows-latest + artifact: windows + - platform: macos-latest + artifact: macos + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install frontend dependencies + run: npm ci + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust artifacts + uses: swatinem/rust-cache@v2 + with: + workspaces: | + src-tauri -> target + + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + pkg-config \ + libssl-dev \ + libglib2.0-dev \ + libwebkit2gtk-4.1-dev \ + libsoup-3.0-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + || sudo apt-get install -y \ + pkg-config \ + libssl-dev \ + libglib2.0-dev \ + libwebkit2gtk-4.0-dev \ + libgtk-3-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Install Tauri CLI + run: cargo install tauri-cli --locked --version "${{ env.TAURI_CLI_VERSION }}" + + - name: Build app bundle + run: npm run tauri:build + + - name: Upload bundle artifacts + uses: actions/upload-artifact@v7 + with: + name: cdpaint-${{ matrix.artifact }} + path: | + target/release/bundle/** + src-tauri/target/release/bundle/** + if-no-files-found: error diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..54db093 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,51 @@ +name: Deploy Demo to Pages + +on: + push: + branches: ["master"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install frontend dependencies + run: npm ci + + - name: Build frontend + run: npm run build + + - name: Setup Pages + uses: actions/configure-pages@v6 + with: + enablement: true + + - name: Upload site files + uses: actions/upload-pages-artifact@v5 + with: + path: ./dist + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release-draft.yml b/.github/workflows/release-draft.yml new file mode 100644 index 0000000..d11ad88 --- /dev/null +++ b/.github/workflows/release-draft.yml @@ -0,0 +1,205 @@ +name: Draft Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +env: + TAURI_CLI_VERSION: "2.10.0" + +concurrency: + group: draft-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release-gate: + name: Release Gate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Validate CHANGES_PENDING.md + shell: bash + run: | + test -f CHANGES_PENDING.md || { echo "CHANGES_PENDING.md is required for releases."; exit 1; } + + for heading in "## Changes" "## Additions" "## Bug Fixes"; do + grep -q "^${heading}$" CHANGES_PENDING.md || { echo "Missing section: ${heading}"; exit 1; } + done + + changes_count=$(awk '/^## Changes$/{f=1;next}/^## /{f=0}f && /^- /{c++}END{print c+0}' CHANGES_PENDING.md) + additions_count=$(awk '/^## Additions$/{f=1;next}/^## /{f=0}f && /^- /{c++}END{print c+0}' CHANGES_PENDING.md) + bugfix_count=$(awk '/^## Bug Fixes$/{f=1;next}/^## /{f=0}f && /^- /{c++}END{print c+0}' CHANGES_PENDING.md) + + if [ "$changes_count" -eq 0 ] && [ "$additions_count" -eq 0 ] && [ "$bugfix_count" -eq 0 ]; then + echo "CHANGES_PENDING.md has no categorized entries." + exit 1 + fi + + current_tag="${GITHUB_REF_NAME}" + last_tag=$(sed -n 's/^- Last tag release: `\(.*\)`/\1/p' CHANGES_PENDING.md | head -n1) + if [ -n "$last_tag" ] && [ "$last_tag" = "$current_tag" ]; then + echo "CHANGES_PENDING.md baseline still matches current release tag (${current_tag})." + echo "It should describe changes leading up to this tag, not already be reset." + exit 1 + fi + + draft-release: + needs: release-gate + name: Draft (${{ matrix.platform }}) + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + include: + - platform: ubuntu-22.04 + - platform: windows-latest + - platform: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install frontend dependencies + run: npm ci + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust artifacts + uses: swatinem/rust-cache@v2 + with: + workspaces: | + src-tauri -> target + + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + pkg-config \ + libssl-dev \ + libglib2.0-dev \ + libwebkit2gtk-4.1-dev \ + libsoup-3.0-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + || sudo apt-get install -y \ + pkg-config \ + libssl-dev \ + libglib2.0-dev \ + libwebkit2gtk-4.0-dev \ + libgtk-3-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Install Tauri CLI + run: cargo install tauri-cli --locked --version "${{ env.TAURI_CLI_VERSION }}" + + - name: Build and upload draft release assets + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: src-tauri + tagName: ${{ github.ref_name }} + releaseName: "CDPaint ${{ github.ref_name }}" + releaseDraft: true + prerelease: false + includeDebug: false + includeRelease: true + releaseBody: | + Automated draft release for ${{ github.ref_name }}. + Edit this draft before publishing. + + rename-assets: + name: Rename Release Assets + runs-on: ubuntu-latest + needs: draft-release + steps: + - name: Rename assets to readable versioned names + uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const tag = context.ref.replace('refs/tags/', ''); + + async function waitForReleaseByTag(maxAttempts = 30, delayMs = 10000) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const rel = await github.rest.repos.getReleaseByTag({ + owner, + repo, + tag + }); + core.info(`Found release for tag ${tag} on attempt ${attempt}.`); + return rel; + } catch (err) { + const status = err && err.status; + if (status !== 404) throw err; + if (attempt === maxAttempts) throw err; + core.info(`Release for tag ${tag} not ready yet (attempt ${attempt}/${maxAttempts}). Waiting ${delayMs / 1000}s...`); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + throw new Error(`Release for tag ${tag} not found after retries.`); + } + + const rel = await waitForReleaseByTag(); + + const assets = rel.data.assets || []; + + function desiredName(name) { + const lower = name.toLowerCase(); + if (lower.endsWith('.rpm')) return `CDPaint-${tag}-Linux-rpm-x64.rpm`; + if (lower.endsWith('.appimage')) return `CDPaint-${tag}-Linux-AppImage-x64.AppImage`; + if (lower.endsWith('.deb')) return `CDPaint-${tag}-Linux-deb-x64.deb`; + if (lower.endsWith('-setup.exe') || lower.endsWith('.exe')) return `CDPaint-${tag}-Windows-Setup-x64.exe`; + if (lower.endsWith('.msi')) return `CDPaint-${tag}-Windows-Installer-x64.msi`; + if (lower.endsWith('.dmg')) { + if (lower.includes('aarch64') || lower.includes('arm64')) return `CDPaint-${tag}-macOS-dmg-AppleSilicon.dmg`; + return `CDPaint-${tag}-macOS-dmg-x64.dmg`; + } + if (lower.endsWith('.app.tar.gz')) { + if (lower.includes('aarch64') || lower.includes('arm64')) return `CDPaint-${tag}-macOS-app-archive-AppleSilicon.tar.gz`; + return `CDPaint-${tag}-macOS-app-archive-x64.tar.gz`; + } + return null; + } + + const occupied = new Set(assets.map(a => a.name)); + + for (const asset of assets) { + const target = desiredName(asset.name); + if (!target) continue; + if (asset.name === target) continue; + if (occupied.has(target)) { + core.warning(`Skipping rename for ${asset.name}; target already exists: ${target}`); + continue; + } + await github.rest.repos.updateReleaseAsset({ + owner, + repo, + asset_id: asset.id, + name: target + }); + core.info(`Renamed ${asset.name} -> ${target}`); + occupied.delete(asset.name); + occupied.add(target); + } diff --git a/.gitignore b/.gitignore index 2714985..19f0c64 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,18 @@ src-tauri/target *.njsproj *.sln *.sw? + +# Local AI tooling (kept on disk, not part of the repo) +.kilo +AGENTS.md +opencode.json +.opencode + +# Local backups +src-tauri/icons-backup-* + +# Environment files +*.env + +# Local security audit notes +src/CDPaint_Security_Audit.txt diff --git a/CHANGES_PENDING.md b/CHANGES_PENDING.md new file mode 100644 index 0000000..69ea7ff --- /dev/null +++ b/CHANGES_PENDING.md @@ -0,0 +1,16 @@ +## Changes +- Reworked the paint engine into modular ES modules (paint-engine, layer-system, gradient-engine, smart-shape, freehand-path-engine) +- Replaced the single-file standalone bundle with a modular src/ layout +- Rewrote the README and clarified the LICENSE with third-party asset notices + +## Additions +- Project browser panel for opening and saving sprite assets with exact palette and index preservation +- Sprite editor with palette panel and PAL support +- Krita brush engine integration with David Revoy brush presets +- Seeded RNG for repeatable brush jitter and noise patterns + +## Bug Fixes +- Fixed UTF-8 mojibake corruption in source files +- Fixed freehand tool preserving prior strokes and ribbon width behavior + +- Last tag release: `v1.1.5` \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/Cargo.lock similarity index 78% rename from src-tauri/Cargo.lock rename to Cargo.lock index 2373e3c..f01b5aa 100644 --- a/src-tauri/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -43,9 +43,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "async-broadcast" @@ -73,9 +82,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -140,14 +149,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "async-signal" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -175,7 +184,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -209,9 +218,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -225,6 +234,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[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" @@ -233,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -273,9 +297,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -284,19 +308,28 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" 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.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -312,9 +345,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -325,7 +358,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -346,9 +379,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" dependencies = [ "serde_core", ] @@ -383,14 +416,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.11+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.55" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "shlex", @@ -398,15 +431,18 @@ dependencies = [ [[package]] name = "cdpaint" -version = "0.1.0" +version = "1.1.6" dependencies = [ "serde", "serde_json", "tauri", "tauri-build", - "tauri-plugin-fs", + "tauri-plugin-dialog", "tauri-plugin-opener", "tauri-plugin-single-instance", + "tauri-plugin-updater", + "url", + "uuid", ] [[package]] @@ -444,9 +480,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -473,12 +509,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.18.1" @@ -507,11 +537,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "core-foundation", "core-graphics-types", "foreign-types", @@ -524,7 +554,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "core-foundation", "libc", ] @@ -549,18 +579,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-common" @@ -574,19 +604,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.29.6" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", + "phf", "smallvec", - "syn 1.0.109", ] [[package]] @@ -596,24 +622,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.114", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -621,50 +653,78 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 2.0.118", +] + +[[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 = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" -version = "0.99.20" +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 = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -698,31 +758,27 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", + "block2", + "libc", "objc2", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -745,7 +801,22 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", ] [[package]] @@ -772,6 +843,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -786,14 +872,14 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.11+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "vswhom", "winreg", ] @@ -828,7 +914,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -839,9 +925,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -881,9 +967,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fdeflate" @@ -904,6 +990,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -926,6 +1022,12 @@ 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" @@ -944,7 +1046,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -962,36 +1064,26 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1000,9 +1092,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1019,32 +1111,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1053,19 +1145,9 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gdk" version = "0.18.2" @@ -1177,36 +1259,36 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.2.17" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "r-efi 5.3.0", + "wasip2", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasip2", + "r-efi 6.0.0", ] [[package]] @@ -1247,7 +1329,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -1275,7 +1357,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1354,7 +1436,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1365,9 +1447,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1395,21 +1477,19 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "html5ever" -version = "0.29.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "mac", "markup5ever", - "match_token", ] [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1446,9 +1526,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1459,12 +1539,26 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1519,17 +1613,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1537,9 +1632,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1550,9 +1645,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1564,15 +1659,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1584,15 +1679,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1622,9 +1717,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1643,12 +1738,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1664,19 +1759,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is-docker" @@ -1699,9 +1784,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -1735,26 +1820,79 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" -version = "0.3.0" +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 = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1786,29 +1924,11 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.13.0", - "selectors", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libappindicator" version = "0.9.0" @@ -1835,9 +1955,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] [[package]] name = "libloading" @@ -1851,25 +1980,24 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags 2.10.0", "libc", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1882,52 +2010,26 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "mac" -version = "0.1.1" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "markup5ever" -version = "0.14.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", ] -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -1944,6 +2046,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1956,20 +2064,20 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] [[package]] name = "muda" -version = "0.17.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", @@ -1980,10 +2088,10 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1992,8 +2100,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", - "jni-sys", + "bitflags 2.13.0", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2001,19 +2109,13 @@ dependencies = [ "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", + "jni-sys 0.3.1", ] [[package]] @@ -2022,17 +2124,11 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -2045,9 +2141,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2055,21 +2151,21 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -2081,19 +2177,11 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", "objc2-foundation", - "objc2-quartz-core", ] [[package]] @@ -2102,7 +2190,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2", "objc2-foundation", ] @@ -2113,7 +2201,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.10.0", "objc2", "objc2-foundation", ] @@ -2124,7 +2211,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "dispatch2", "objc2", ] @@ -2135,7 +2222,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -2153,28 +2240,25 @@ dependencies = [ ] [[package]] -name = "objc2-core-text" +name = "objc2-core-location" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "bitflags 2.10.0", "objc2", - "objc2-core-foundation", - "objc2-core-graphics", + "objc2-foundation", ] [[package]] -name = "objc2-core-video" +name = "objc2-core-text" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-io-surface", ] [[package]] @@ -2198,7 +2282,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -2211,19 +2295,21 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", ] [[package]] -name = "objc2-javascript-core" +name = "objc2-osa-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ + "bitflags 2.13.0", "objc2", - "objc2-core-foundation", + "objc2-app-kit", + "objc2-foundation", ] [[package]] @@ -2232,32 +2318,40 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-foundation", ] [[package]] -name = "objc2-security" +name = "objc2-ui-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", + "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-ui-kit" +name = "objc2-user-notifications" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags 2.10.0", "objc2", - "objc2-core-foundation", "objc2-foundation", ] @@ -2267,34 +2361,37 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2", "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", - "objc2-javascript-core", - "objc2-security", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.3" +version = "5.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" dependencies = [ "dunce", "is-wsl", "libc", - "pathdiff", ] +[[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" @@ -2311,6 +2408,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2365,12 +2476,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2379,155 +2484,68 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "phf_macros", + "phf_shared", + "serde", ] [[package]] name = "phf_codegen" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", ] [[package]] name = "phf_generator" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", + "fastrand", + "phf_shared", ] [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.114", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", + "syn 2.0.118", ] [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -2536,18 +2554,18 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "quick-xml", "serde", "time", @@ -2566,6 +2584,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -2582,9 +2613,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2595,15 +2626,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "precomputed-hash" version = "0.1.1" @@ -2632,11 +2654,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -2663,12 +2685,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.106" @@ -2680,18 +2696,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -2703,85 +2719,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "raw-window-handle" @@ -2795,7 +2736,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -2826,14 +2767,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -2854,15 +2795,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -2872,15 +2813,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2892,6 +2838,50 @@ dependencies = [ "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" @@ -2902,23 +2892,96 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "1.1.3" +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +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", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "same-file" @@ -2929,6 +2992,15 @@ 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" @@ -2977,7 +3049,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -2986,29 +3058,53 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "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.24.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.0", "cssparser", "derive_more", - "fxhash", "log", - "phf 0.8.0", - "phf_codegen 0.8.0", + "new_debug_unreachable", + "phf", + "phf_codegen", "precomputed-hash", + "rustc-hash", "servo_arc", "smallvec", ] [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -3053,7 +3149,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3064,14 +3160,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3088,7 +3184,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3102,24 +3198,25 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] [[package]] name = "serde_with" -version = "3.16.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -3130,14 +3227,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3159,16 +3256,15 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "servo_arc" -version = "0.2.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "nodrop", "stable_deref_trait", ] @@ -3185,9 +3281,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -3201,21 +3297,31 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "siphasher" -version = "0.3.11" +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3225,18 +3331,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3295,25 +3401,24 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.11.3", + "phf_shared", "precomputed-hash", - "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -3324,6 +3429,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3342,15 +3453,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.114" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -3374,7 +3484,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3392,35 +3502,35 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2", "core-foundation", "core-graphics", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", - "lazy_static", + "jni 0.21.1", "libc", "log", "ndk", - "ndk-context", "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -3438,7 +3548,18 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", ] [[package]] @@ -3449,9 +3570,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.2" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" dependencies = [ "anyhow", "bytes", @@ -3464,7 +3585,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3500,9 +3621,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.5" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -3516,29 +3637,28 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.11+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.4" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", "serde", "serde_json", "sha2", - "syn 2.0.114", + "syn 2.0.118", "tauri-utils", "thiserror 2.0.18", "time", @@ -3549,23 +3669,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.4" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.5.3" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -3574,19 +3694,38 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.11+spec-1.1.0", "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + [[package]] name = "tauri-plugin-fs" -version = "2.4.5" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" dependencies = [ "anyhow", "dunce", "glob", + "log", + "objc2-foundation", "percent-encoding", "schemars 0.8.22", "serde", @@ -3596,15 +3735,15 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.18", - "toml 0.9.11+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-opener" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" dependencies = [ "dunce", "glob", @@ -3624,9 +3763,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc61e4822b8f74d68278e09161d3e3fdd1b14b9eb781e24edccaabf10c420e8c" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" dependencies = [ "serde", "serde_json", @@ -3637,17 +3776,50 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" -version = "2.10.0" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3664,17 +3836,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", - "objc2-foundation", "once_cell", "percent-encoding", "raw-window-handle", @@ -3691,24 +3862,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever", "http", "infer", "json-patch", - "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf", + "plist", "proc-macro2", "quote", "regex", @@ -3720,7 +3891,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 0.9.11+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -3729,23 +3900,23 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 0.9.11+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", ] [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3753,13 +3924,11 @@ dependencies = [ [[package]] name = "tendril" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ - "futf", - "mac", - "utf-8", + "new_debug_unreachable", ] [[package]] @@ -3788,7 +3957,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3799,17 +3968,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -3819,15 +3987,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -3835,19 +4003,34 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3857,6 +4040,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3884,17 +4077,32 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde_core", - "serde_spanned 1.0.4", + "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.14", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", ] [[package]] @@ -3915,13 +4123,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -3932,7 +4149,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -3941,30 +4158,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 0.7.14", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 0.7.14", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -3983,20 +4200,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4030,7 +4247,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4044,9 +4261,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs", @@ -4058,10 +4275,10 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4078,19 +4295,19 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4136,15 +4353,21 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +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 = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" @@ -4171,12 +4394,6 @@ dependencies = [ "url", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4185,11 +4402,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.20.0" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -4246,12 +4463,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4260,18 +4471,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4282,23 +4493,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4306,22 +4513,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -4341,14 +4548,26 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -4393,6 +4612,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4415,7 +4643,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4542,7 +4770,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4553,7 +4781,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4623,6 +4851,15 @@ 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" @@ -4865,9 +5102,15 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -4884,36 +5127,35 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wry" -version = "0.54.1" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed1a195b0375491dd15a7066a10251be217ce743cf4bbbbdcf5391d6473bee0" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2", "cookie", "crossbeam-channel", "dirs", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", - "jni", - "kuchikiki", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -4960,11 +5202,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4973,21 +5225,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "synstructure", ] [[package]] name = "zbus" -version = "5.13.2" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" dependencies = [ "async-broadcast", "async-executor", @@ -5012,7 +5264,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.14", + "winnow 1.0.3", "zbus_macros", "zbus_names", "zvariant", @@ -5020,14 +5272,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.13.2" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "zbus_names", "zvariant", "zvariant_utils", @@ -5035,61 +5287,47 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" dependencies = [ "serde", - "winnow 0.7.14", + "winnow 1.0.3", "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -5098,9 +5336,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -5109,57 +5347,69 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", ] [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "5.9.2" +version = "5.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.14", + "winnow 1.0.3", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.9.2" +version = "5.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.114", - "winnow 0.7.14", + "syn 2.0.118", + "winnow 1.0.3", ] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bb8bf48 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] +resolver = "2" +members = ["src-tauri"] + +[profile.release] +lto = true +strip = "symbols" +codegen-units = 1 +panic = "abort" +opt-level = "z" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11a5c94 --- /dev/null +++ b/LICENSE @@ -0,0 +1,44 @@ +MIT License + +Copyright (c) 2026 FrCynda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Additional Notice (Third-Party Assets) +--------------------------------------- +The MIT license above covers the original code in this repository. The +following bundled assets are NOT covered by the MIT grant and retain their +respective ownership: + +- Legacy Microsoft Paint UI elements embedded as Base64/SVG strings in the + source. These are replicas of Microsoft's interface, used here as a + non-commercial educational tribute, and remain the intellectual property + of Microsoft Corporation. + +- Brush tip images in src/brushes/ are from David Revoy's Krita brush packs, + licensed under Creative Commons Attribution 4.0 (CC BY 4.0) to David Revoy, + www.davidrevoy.com. Some brushes were modified locally; attribution is + provided here and in the README. + +- Pokemon-related fan assets (src/assets/PokemonProject.png, + src/assets/normal.pal) are reference material for the sprite editor and are + not part of the MIT grant; all rights remain with their respective owners. + +Redistributors and forkers are responsible for complying with any third-party +intellectual property rights that apply to these excluded assets. \ No newline at end of file diff --git a/README.md b/README.md index 9b41b9b..55a91b9 100644 --- a/README.md +++ b/README.md @@ -4,151 +4,95 @@ [![Node.js](https://img.shields.io/badge/Node.js-LTS-green.svg)](https://nodejs.org/) [![Rust](https://img.shields.io/badge/Rust-Ready-orange.svg)](https://www.rust-lang.org/) -A high-performance, WebGL-accelerated painting application designed for pixel artists and retro game developers. While it maintains a modern Windows aesthetic, it introduces useful image processing, active bit-depth enforcement, and specialized export tools for hardware-constrained environments like the Game Boy Advance (GBA). - -CDPaint Interface - -# Table of Contents - -- [🚀 Key Features](#-key-features) -- [🛠 Technical Stack](#-technical-stack) -- [🏗 Architecture](#-architecture) -- [💻 Build & Run Guide](#-build--run-guide) -- [🔧 Troubleshooting](#-troubleshooting) - ---- - -# 🚀 Key Features - -## 1. Active Canvas-Wide Color Modes -Unlike standard editors that operate in a 24-bit space, this app allows you to lock the entire drawing experience to specific hardware limits: -* **Active Quantization**: Every stroke you draw is automatically converted to the nearest valid color in your selected mode. -* **15bpp (RGB555)**: Constrains R, G, and B to 32 levels each (GBA native). -* **16bpp (RGB565)**: Constrains R and B to 32 levels and G to 64 levels. -* **8bpp (Indexed)**: Restricts the entire canvas to a 256-color palette. -* **Live Mode Switching**: Instantly convert existing artwork between bit-depths using GPU-accelerated quantization shaders. - -## 2. GBA & Retro Export Studio -A specialized workflow for generating game-ready assets: -* **Palette Index Reordering**: Drag-and-drop palette swatches to ensure specific colors (like transparency) are assigned to the correct indices (e.g., Slot 0). -* **Automated Sprite Splitting**: Automatically detects and splits 64x128 or 128x64 canvases into separate 64x64 files for "Front/Back" game sprites. -* **JASC-PAL Support**: Full import/export support for `.pal` files used in game decompilation projects and map editors like Porymap. -* **Hardware Preview**: Preview exactly how your image will look on 15-bit hardware before exporting. - -## 3. Image Adjustments -* **Precision Hue/Saturation**: A channel-based HSL adjustment tool (Master, R, Y, G, C, B, M) with a Split View slider to compare changes in real-time. -* **Decrease Color Depth**: Advanced quantization menu featuring Floyd-Steinberg dithering, OKLab color space accuracy, and seeded RNG for repeatable noise patterns. -* **Smart Edge Cleaner**: A GPU-accelerated filter designed to remove "stray" pixels from edges while protecting user-defined primary colors. - -## 4. Advanced Selection Engine -* **Magic Wand Tool**: Features a distance-based tolerance slider. Drag away from your click point to live-expand the selection. -* **Boolean Operations**: Shift (Add), Ctrl (Subtract), and Alt (Intersect) modifiers for all selection tools. -* **Lasso & Polyline Select**: Free-form and point-to-point selection paths for non-rectangular objects. -* **Transparent Selection**: Keyed transparency for floating selections and pasted content. - -## 5. Power-User Workflow -* **Custom Hotkeys**: Fully rebindable keyboard shortcuts for every tool and action. -* **Tiled Mode**: Live tiling for seamless pattern design. Draw in the center tile and see your strokes replicated across a 3×3 grid. -* **Free Canvas Mode**: Toggle between an "Anchored" view and a "Free" floating canvas that can be moved and manipulated within the viewport. -* **Hover Preview**: A dedicated 20x magnification window showing the exact pixel grid under your cursor. - ---- - -# 🛠 Technical Stack - -* **Rendering**: Dual-layer HTML5 Canvas API with WebGL Fragment Shaders for strokes, transforms, and quantization. -* **Processing**: Web Workers for heavy lifting (K-means color clustering and HSL adjustments) to keep the UI thread responsive. -* **Architecture**: Vanilla JavaScript with a custom "modern Windows" UI component library. -* **Platform**: Optimized for standalone use or as a Tauri desktop application. - ---- - -# 🏗 Architecture - -**Frontend:** -* Source files live in `src/` -* Dev server is a small Node HTTP server at `http://localhost:1420` (`scripts/dev.js`) -* Build step copies `src/` to `dist/` (`scripts/build.js`) - -**Tauri:** -* Config is in `src-tauri/tauri.conf.json` -* Tauri uses the dev server in development (`beforeDevCommand`: `npm run dev`) -* Tauri uses `dist/` for production builds (`beforeBuildCommand`: `npm run build`) - ---- - -# 💻 Build & Run Guide - -Welcome! This guide is written specifically for complete beginners. You do not need to be a programmer or have any special coding software installed to build this app. -Simply follow these instructions one by one. - -## 1. Install Prerequisites (One-Time Setup) - -You only need to install three standard tools to make this work. Download them using the official links below: - -1. **Node.js** (Runs the visual interface) - * **Download:** Get the **LTS** version from [nodejs.org](https://nodejs.org/en/download) - * **Setup:** Click through the standard installation, accepting all the defaults. -2. **Rust** (Runs the app's background engine) - * **Download:** Get the installer from [rust-lang.org](https://www.rust-lang.org/tools/install) - * **Setup:** When the black window pops up during installation, simply press `1` and hit **Enter** to accept the default installation. -3. **Visual Studio Build Tools** (Helps Windows read the code) - * **Download:** Get it from [visualstudio.microsoft.com](https://visualstudio.microsoft.com/visual-cpp-build-tools/) - * **Setup:** During installation, you will see a screen with checkboxes. You **must** check the box that says **Desktop development with C++**. - -> **IMPORTANT:** Once all three are installed, **restart your computer** so Windows registers the new tools. - -## 2. Open Command Prompt (The Easy Way) - -Instead of navigating through complex computer menus, use this standard Windows shortcut to open your command prompt exactly where it needs to be: - -1. Open your **CDPaint** folder wherever you saved or extracted it (for example, in your Downloads or Documents folder) using the standard Windows File Explorer. -2. Click directly on the long address bar at the very top of the folder window (the bar that shows your current folder path). -3. Delete whatever text is there, type `cmd`, and press **Enter**. -4. A black Command Prompt window will pop up. Because you opened it this way, it automatically knows to look inside your CDPaint folder. Keep this window open! - -## 3. Install Dependencies - -Now we just need to tell the computer to download the final pieces the app needs to run. In that black window you just opened, simply type the following and press **Enter**: - -`npm install` - -*Note: You will see a lot of text scrolling by and some progress bars. Don't worry, it's just doing its job! When the text stops moving and you see a blinking cursor again, it is completely done.* - -## 4. Run the App (Test Mode) - -To launch the app so you can see it and test it out, type this into the same black window and press **Enter**: - -`npm run tauri dev` - -* **What happens:** The window will process some more text, and after a few moments, your CDPaint application will automatically pop open! -* **To stop it:** Close the CDPaint window like you would close any normal app. Then, click on your black Command Prompt window and press the `Ctrl` and `C` keys on your keyboard at the same time to safely shut down the background process. - -## 5. Build the Final App - -When you are completely finished testing and want to create a final, clickable application file (like an `.exe` installer) that you can easily open later or share with friends, run this command: - -`npm run tauri:build` - -* **What happens:** This process takes a few minutes to pack everything tightly together. Once it finishes, open your CDPaint folder in File Explorer. You will find your final, ready-to-use application hidden inside this specific folder path: `src-tauri/target/release/bundle/` - ---- - -# 🔧 Troubleshooting - -* **"Command not found" error:** Your computer hasn't recognized the installations from Step 1. Make sure you fully restarted your computer after installing them. -* **White or Blank app window:** You likely skipped Step 3. Close the app, run `npm install` in your command prompt, and try running it again. - -## White/blank window in dev -Check if the dev server is running properly: -1. Open `http://localhost:1420` in your web browser. -2. If it fails to load, manually run `npm run dev` in your terminal and check the output for any specific errors. - ---- - -# Legal & Licensing - -* The project logic is MIT licensed. -* The embedded Microsoft Paint-style icons and baked-in Base64/SVG asset strings are property of Microsoft Corporation and are excluded from the MIT license for this repository. -* This repository is a non-commercial Fair Use educational tribute/reconstruction. -* Warning to forkers: anyone forking or redistributing this repository assumes all risk and responsibility for redistributing embedded Microsoft-owned assets. +A painting app for pixel artists and retro game developers, built around the +constraints of real hardware. It keeps the familiar Windows Paint-style +workflow, but adds bit-depth enforcement, palette tools, and export helpers +aimed at Game Boy Advance (GBA) development. + +**Try the live browser demo:** https://frcynda.github.io/CDPaint/ + +The demo runs best in Chromium-based browsers. Gecko-based browsers +(Firefox, LibreWolf, Waterfox, etc.) are noticeably slower. + +CDPaint interface + +## Features + +- **Bit-depth locked painting** - Lock the whole canvas to a hardware color + mode: 15bpp (RGB555), 16bpp (RGB565), or 8bpp indexed with a 256-color + palette. Every stroke is quantized to the active mode, and you can switch + modes live to convert existing artwork. +- **GBA export tools** - Drag-and-drop palette index reordering (so + transparency lands on the right slot), automatic splitting of 64x128 / + 128x64 canvases into 64x64 front/back sprites, JASC-PAL import/export for + decomp projects and Porymap, and a hardware preview of how the image will + look on 15-bit output. +- **Image adjustments** - Channel-based HSL tuning (Master, R, Y, G, C, B, M) + with a split-view comparison, color depth reduction with Floyd-Steinberg + dithering and OKLab accuracy, and an edge cleaner that removes stray pixels + while protecting your key colors. +- **Selection tools** - Magic wand with a distance-based tolerance slider + (drag to live-expand), lasso and polyline selection, Shift/Ctrl/Alt boolean + operations, and keyed transparency for floating selections. +- **Power-user workflow** - Fully rebindable hotkeys, tiled mode for seamless + patterns, a free-floating canvas mode, and a 20x hover preview showing the + exact pixel grid under the cursor. +- **Sprite editor** - Project browser for opening and saving sprite assets + with exact palette and index preservation, plus a palette panel with PAL + support. + +## Tech stack + +- Vanilla JavaScript (no frameworks), Canvas 2D with WebGL fragment shaders + for strokes, transforms, and quantization +- Web Workers for heavy tasks (color clustering, HSL adjustments) +- Tauri 2 for the desktop shell (Rust backend for OS integration only) + +## Project layout + +``` +src/ Frontend (HTML, CSS, JS modules) +src-tauri/ Tauri/Rust shell, config, icons +scripts/ Dev server, build, bundle and smoke-test tooling +tools/ Brush/palette helper scripts +test/ Algorithm tests (wand, reference implementations) +``` + +## Building and running + +Prerequisites: Node.js LTS, Rust, and on Windows the Visual Studio Build +Tools with "Desktop development with C++". + +```bash +npm install +npm run dev # dev server at http://localhost:1420 +npm run tauri:build # desktop installer (NSIS/MSI, DMG, or AppImage/deb/rpm) +npm test # smoke tests +``` + +Installers land in `src-tauri/target/release/bundle/`. + +## Troubleshooting + +- **"Command not found"** - The OS prerequisites aren't on PATH yet. + Re-check the install guide for your OS and restart. +- **Installer folder is empty** - The build failed earlier. Scroll up in the + terminal, fix the first error, and run `npm run tauri:build` again. +- **Windows SmartScreen warning** - Unsigned builds show this; click + "More info" then "Run anyway". +- **macOS says the app is damaged** - System Settings -> Privacy & Security, + find the blocked app, click "Open Anyway". +- **Linux AppImage won't open** - `chmod +x path/to/CDPaint.AppImage` first. + +## Credits + +Brush tip assets in `src/brushes/` come from David Revoy's Krita brush packs, +licensed under Creative Commons Attribution 4.0 (CC BY 4.0) to David Revoy, +[www.davidrevoy.com](https://www.davidrevoy.com). Some brushes were modified +locally for this project. + +## License + +The code is MIT licensed. Some bundled assets are not covered by the MIT +grant - see [LICENSE](LICENSE) for the full notice (Microsoft Paint UI +replicas, David Revoy's brushes, and Pokemon-related reference assets). \ No newline at end of file diff --git a/npm b/npm deleted file mode 100644 index e69de29..0000000 diff --git a/package-lock.json b/package-lock.json index 96e98db..2806635 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,232 +1,952 @@ { - "name": "cdpaint", - "version": "0.1.0", + "name": "CDPaint", + "version": "1.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cdpaint", - "version": "0.1.0", + "name": "CDPaint", + "version": "1.1.6", + "dependencies": { + "canvas": "^3.2.3", + "perfect-freehand": "^1.2.3" + }, "devDependencies": { - "@tauri-apps/cli": "^2" + "esbuild": "^0.28.1" } }, - "node_modules/@tauri-apps/cli": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz", - "integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.10.0", - "@tauri-apps/cli-darwin-x64": "2.10.0", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", - "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", - "@tauri-apps/cli-linux-arm64-musl": "2.10.0", - "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", - "@tauri-apps/cli-linux-x64-gnu": "2.10.0", - "@tauri-apps/cli-linux-x64-musl": "2.10.0", - "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", - "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", - "@tauri-apps/cli-win32-x64-msvc": "2.10.0" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz", - "integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==", + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz", - "integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz", - "integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz", - "integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz", - "integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ - "arm64" + "ia32" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz", - "integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz", - "integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ - "x64" + "s390x" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz", - "integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz", - "integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz", - "integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz", - "integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==", + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 OR MIT", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">=18" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/perfect-freehand": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.3.tgz", + "integrity": "sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 4f663b9..29ef4ed 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,23 @@ { + "type": "module", "name": "CDPaint", - "version": "1.0.0", + "version": "1.1.6", "description": "", "main": "index.js", "scripts": { "tauri": "cd src-tauri && cargo tauri", "tauri:build": "cd src-tauri && cargo tauri build", - "dev": "node scripts/dev.js", - "build": "node scripts/build.js", - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "node scripts/dev.cjs", + "build": "node scripts/build.cjs", + "test": "node scripts/smoke-test.mjs", + "test:wand": "node test/wand-algorithms.test.mjs" }, - "private": true + "private": true, + "dependencies": { + "canvas": "^3.2.3", + "perfect-freehand": "^1.2.3" + }, + "devDependencies": { + "esbuild": "^0.28.1" + } } diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..ac2bbc3 Binary files /dev/null and b/screenshot.png differ diff --git a/scripts/build-pf-bundle.js b/scripts/build-pf-bundle.js new file mode 100644 index 0000000..96fe1d8 --- /dev/null +++ b/scripts/build-pf-bundle.js @@ -0,0 +1,22 @@ +// Build script: wraps perfect-freehand CJS into a browser-friendly IIFE bundle. +// Run: node scripts/build-pf-bundle.js +const fs = require('fs'); +const path = require('path'); + +const cjsPath = path.join(__dirname, '..', 'node_modules', 'perfect-freehand', 'dist', 'cjs', 'index.js'); +const outPath = path.join(__dirname, '..', 'src', 'js', 'perfect-freehand-bundle.js'); + +const src = fs.readFileSync(cjsPath, 'utf8'); + +const bundle = `/* Perfect Freehand v1.2.3 bundled for CDpaint — DO NOT EDIT */ +(function(){ + var exports = {}, module = { exports: exports }; + ${src} + window.getStroke = exports.getStroke || exports.default; + window.getStrokePoints = exports.getStrokePoints; + window.getStrokeOutlinePoints = exports.getStrokeOutlinePoints; +})(); +`; + +fs.writeFileSync(outPath, bundle, 'utf8'); +console.log('Wrote ' + outPath); diff --git a/scripts/build.cjs b/scripts/build.cjs new file mode 100644 index 0000000..4a3a7e6 --- /dev/null +++ b/scripts/build.cjs @@ -0,0 +1,57 @@ +const fs = require('fs'); +const path = require('path'); +const esbuild = require('esbuild'); + +const srcDir = path.resolve(__dirname, '..', 'src'); +const distDir = path.resolve(__dirname, '..', 'dist'); + +function copyRecursive(src, dest) { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + for (const item of fs.readdirSync(src)) { + copyRecursive(path.join(src, item), path.join(dest, item)); + } + } else { + fs.copyFileSync(src, dest); + } +} + +function collectFiles(dir, pattern) { + const results = []; + for (const item of fs.readdirSync(dir)) { + const full = path.join(dir, item); + const stat = fs.statSync(full); + if (stat.isDirectory()) { + results.push(...collectFiles(full, pattern)); + } else if (pattern.test(full)) { + results.push(full); + } + } + return results; +} + +async function main() { + if (fs.existsSync(distDir)) { + fs.rmSync(distDir, { recursive: true, force: true }); + } + copyRecursive(srcDir, distDir); + + const jsFiles = collectFiles(distDir, /\.js$/); + for (const file of jsFiles) { + await esbuild.build({ + entryPoints: [file], + outfile: file, + allowOverwrite: true, + minify: true, + sourcemap: false, + }); + } + + console.log(`Built frontend into ${distDir} (${jsFiles.length} JS files minified)`); +} + +main().catch(err => { + console.error('Build failed:', err); + process.exit(1); +}); diff --git a/scripts/bundle-standalone.js b/scripts/bundle-standalone.js new file mode 100644 index 0000000..d2078d6 --- /dev/null +++ b/scripts/bundle-standalone.js @@ -0,0 +1,310 @@ +/** + * Bundle CDPaint into a single self-contained HTML file. + * + * Inlines all JS, CSS, and images as data URIs so the file works + * when opened from any location (no relative path dependencies). + * + * Usage: node scripts/bundle-standalone.js + * Output: ~/Desktop/cdpaint.html + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +// ── Configuration ────────────────────────────────────────────────────────── +const SRC = path.resolve(__dirname, '..', 'src'); +const OUT = path.resolve( + process.env.USERPROFILE || process.env.HOME || __dirname, + 'Desktop', + 'cdpaint.html' +); + +const MIME_TYPES = { + '.png' : 'image/png', + '.svg' : 'image/svg+xml', + '.gif' : 'image/gif', + '.jpg' : 'image/jpeg', + '.jpeg' : 'image/jpeg', + '.webp' : 'image/webp', + '.ico' : 'image/x-icon', + '.bmp' : 'image/bmp', +}; + +// ── Helpers ──────────────────────────────────────────────────────────────── + +function mimeOf(filePath) { + return MIME_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream'; +} + +function isBinary(mime) { + return !mime.startsWith('text/') && mime !== 'application/json'; +} + +const dataUriCache = new Map(); + +function fileToDataUri(absPath) { + if (dataUriCache.has(absPath)) return dataUriCache.get(absPath); + const buf = fs.readFileSync(absPath); + const mime = mimeOf(absPath); + let uri; + if (mime === 'image/svg+xml') { + // Inline SVG as UTF-8 data URI (smaller than base64 and preserves + // readability in the output). + const svg = buf.toString('utf8').replace(/^\uFEFF/, ''); + uri = `data:image/svg+xml,${encodeURIComponent(svg)}`; + } else { + uri = `data:${mime};base64,${buf.toString('base64')}`; + } + dataUriCache.set(absPath, uri); + return uri; +} + +function readFileSafe(absPath) { + try { + let text = fs.readFileSync(absPath, 'utf8'); + if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1); // strip BOM + return text; + } catch (e) { + return null; + } +} + +function replaceAll(str, search, replacement) { + return str.split(search).join(replacement); +} + +// ── Asset index ──────────────────────────────────────────────────────────── +// Build two lookup maps: +// 1. absolute path → data URI +// 2. filename only → [absolute path, ...] (for ambiguous short-name resolution) + +const absToUri = new Map(); // full absolute path → data URI +const nameToAbs = new Map(); // base filename → [absolute paths...] + +function indexDirectory(dirPath) { + let entries; + try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return; } + for (const e of entries) { + const full = path.join(dirPath, e.name); + if (e.isDirectory()) { + indexDirectory(full); + } else { + const uri = fileToDataUri(full); + absToUri.set(full, uri); + const base = e.name.toLowerCase(); + if (!nameToAbs.has(base)) nameToAbs.set(base, []); + nameToAbs.get(base).push(full); + } + } +} + +// Index assets/ + app-icon.png at root +const assetRoot = path.join(SRC, 'assets'); +if (fs.existsSync(assetRoot)) indexDirectory(assetRoot); +const rootIcon = path.join(SRC, 'app-icon.png'); +if (fs.existsSync(rootIcon)) { + absToUri.set(rootIcon, fileToDataUri(rootIcon)); + nameToAbs.set('app-icon.png', [rootIcon]); +} + +// ── Resolve a relative reference to a data URI ───────────────────────────── +// Tries, in order: +// a) ./assets/foo.png → SRC/assets/foo.png +// b) assets/foo.png → SRC/assets/foo.png +// c) assets/foo.png → SRC/foo.png +// d) foo.png → SRC/assets/foo.png (if unique) +// e) ./foo.png → SRC/foo.png + +function resolveToUri(ref) { + const clean = ref.replace(/^\.\//, '').replace(/^\//, '').replace(/\\/g, '/'); + + // Direct path under SRC + let abs = path.resolve(SRC, clean); + if (absToUri.has(abs)) return absToUri.get(abs); + + // Prepend assets/ + abs = path.resolve(SRC, 'assets', clean); + if (absToUri.has(abs)) return absToUri.get(abs); + + // Try full path under SRC (for ./app-icon.png) + abs = path.resolve(SRC, clean); + if (absToUri.has(abs)) return absToUri.get(abs); + + // Short name — unique match in the asset index + const base = path.basename(clean).toLowerCase(); + if (nameToAbs.has(base)) { + const candidates = nameToAbs.get(base); + if (candidates.length === 1) return absToUri.get(candidates[0]); + // Multiple files with the same name — try matching the full relative path + for (const c of candidates) { + const rel = path.relative(SRC, c).replace(/\\/g, '/'); + if (rel === clean || rel === path.join('assets', clean).replace(/\\/g, '/')) { + return absToUri.get(c); + } + } + } + + return null; +} + +// ── Collect all unique asset references from a blob of text ──────────────── +// Returns an array of { raw, inner } where `raw` is the original quoted +// or backtick string and `inner` is the path inside the quotes. + +function findAssetRefs(text) { + const seen = new Set(); + const refs = []; + + function add(raw, inner) { + if (seen.has(raw)) return; + seen.add(raw); + refs.push({ raw, inner: inner.replace(/^\.\//, '') }); + } + + // Single/double quoted: 'assets/...', "assets/...", './assets/...' + const qRe = /['"]((?:\.\/)?(?:assets|app-icon)[^'"]*\.(?:png|svg|gif|jpg|jpeg|webp|ico|bmp))['"]/gi; + let m; + while ((m = qRe.exec(text)) !== null) add(m[0], m[1]); + + // Backtick template literals with simple known paths: `assets/foo.png` + const btRe = /`((?:\.\/)?(?:assets|app-icon)[^`]*\.(?:png|svg|gif|jpg|jpeg|webp|ico|bmp))`/gi; + while ((m = btRe.exec(text)) !== null) add(m[0], m[1]); + + // JS strings where the full path is concatenated from known parts: + // e.g. 'assets/' + name + '.png' — only if the literal part includes 'assets/' + // We look for: 'assets/FILENAME.EXT' where FILENAME.EXT is literal + // This is already handled by the first pattern. For dynamic parts like + // 'assets/' + var + '.png' we can't resolve statically — skip them. + + // CSS url() references (unused in this project, but keep for robustness) + const cssRe = /url\(["']?((?:\.\/)?(?:assets|app-icon)[^"')]+\.[a-z]+)["']?\)/gi; + while ((m = cssRe.exec(text)) !== null) { + const raw = m[0]; + const inner = m[1]; + if (!seen.has(raw)) { seen.add(raw); refs.push({ raw, inner }); } + } + + return refs; +} + +// ── Main ─────────────────────────────────────────────────────────────────── + +let html = readFileSafe(path.join(SRC, 'index.html')); +if (!html) { + console.error('FATAL: src/index.html not found at', path.join(SRC, 'index.html')); + process.exit(1); +} + +const warnings = []; +let cssInlined = 0, jsInlined = 0, imgInlined = 0; + +// ── Step 1: Inline CSS ───────────────────────────────────────────────────── +html = html.replace( + //gi, + (match, href) => { + const abs = path.resolve(SRC, href); + const css = readFileSafe(abs); + if (css === null) { + warnings.push(`CSS file not found: ${abs}`); + return match; + } + cssInlined++; + return ``; + } +); + +// ── Step 2: Inline JS files ──────────────────────────────────────────────── +html = html.replace( + /<\/script>/gi, + (match, src) => { + const abs = path.resolve(SRC, src); + const code = readFileSafe(abs); + if (code === null) { + warnings.push(`JS file not found: ${abs}`); + return match; + } + jsInlined++; + return ``; + } +); + +// ── Step 3: Convert local asset references to data URIs ──────────────────── +const refs = findAssetRefs(html); +const unresolved = []; + +for (const { raw, inner } of refs) { + const uri = resolveToUri(inner); + if (uri) { + // Replace the original (quoted) reference with the data URI (always + // double-quoted so it works in both HTML and JS contexts). + const replacement = `"${uri}"`; + html = replaceAll(html, raw, replacement); + imgInlined++; + } else { + unresolved.push(inner); + } +} + +// ── Step 4: Remove Tauri window controls ─────────────────────────────────── +html = html.replace( + /]*>.*?<\/button>\s*/gi, + '' +); + +// ── Step 5: Post-build verification ──────────────────────────────────────── +// Scan for any remaining path patterns that look like un-inlined local files. +const leftoverRe = /['"`]((?:\.\/)?(?:assets|app-icon)[^'"`]+\.[a-z]+)['"`]/gi; +const leftovers = []; +let lm; +while ((lm = leftoverRe.exec(html)) !== null) { + const p = lm[1]; + // Skip data: URIs + if (p.startsWith('data:')) continue; + // Skip external URLs + if (p.startsWith('http://') || p.startsWith('https://')) continue; + leftovers.push(p); +} + +// Deduplicate +const uniqueLeftovers = [...new Set(leftovers)]; +if (uniqueLeftovers.length > 0) { + warnings.push( + `${uniqueLeftovers.length} local asset path(s) were NOT inlined:\n` + + uniqueLeftovers.map(s => ` ${s}`).join('\n') + ); +} + +// ── Write output ─────────────────────────────────────────────────────────── +const outDir = path.dirname(OUT); +try { fs.mkdirSync(outDir, { recursive: true }); } catch {} + +fs.writeFileSync(OUT, html, 'utf8'); +const sizeKB = (Buffer.byteLength(html, 'utf8') / 1024).toFixed(1); + +// ── Report ───────────────────────────────────────────────────────────────── +console.log(''); +console.log('── Bundle complete ────────────────────────'); +console.log(` Output : ${OUT}`); +console.log(` Size : ${sizeKB} KB`); +console.log(` CSS : ${cssInlined} file(s) inlined`); +console.log(` JS : ${jsInlined} file(s) inlined`); +console.log(` Images : ${imgInlined} reference(s) → data URI`); +console.log('───────────────────────────────────────────'); + +if (unresolved.length > 0) { + console.warn(`\n⚠ ${unresolved.length} asset(s) could not be resolved (file missing?):`); + for (const r of unresolved) console.warn(` ${r}`); +} + +if (warnings.length > 0) { + for (const w of warnings) console.warn(`⚠ ${w}`); +} + +if (uniqueLeftovers.length > 0) { + console.error('\n✖ Output still contains un-inlined local paths.'); + process.exit(1); +} + +console.log('✓ Output verified — no remaining local file references.\n'); diff --git a/scripts/dev.cjs b/scripts/dev.cjs new file mode 100644 index 0000000..241f9d0 --- /dev/null +++ b/scripts/dev.cjs @@ -0,0 +1,109 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..', 'src'); +const port = process.env.PORT || 1420; +const host = process.env.HOST || '127.0.0.1'; + +/* ── Live-reload injection ───────────────────────────────────────────── */ +const RELOAD_SCRIPT = ` + +`; + +function injectReloadScript(data) { + return data.toString('utf8').replace('', RELOAD_SCRIPT + ''); +} + +/* ── MTime scanner (stat only, no content read) ──────────────────────── */ +function scanMtime(dir) { + let max = 0; + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + const sub = scanMtime(full); + if (sub > max) max = sub; + } else if (entry.isFile() && /\.(js|css|html?)$/i.test(entry.name)) { + try { + const m = fs.statSync(full).mtimeMs; + if (m > max) max = m; + } catch (_) {} + } + } + } catch (_) {} + return max; +} + +/* ── Helpers ─────────────────────────────────────────────────────────── */ +function contentType(file) { + if (file.endsWith('.html')) return 'text/html'; + if (file.endsWith('.js')) return 'application/javascript'; + if (file.endsWith('.css')) return 'text/css'; + if (file.endsWith('.png')) return 'image/png'; + if (file.endsWith('.jpg') || file.endsWith('.jpeg')) return 'image/jpeg'; + if (file.endsWith('.svg')) return 'image/svg+xml'; + return 'application/octet-stream'; +} + +function decodeUrlPath(urlPath) { + try { + return decodeURIComponent(urlPath); + } catch (_) { + return null; + } +} + +function resolveWithinRoot(urlPath) { + const decoded = decodeUrlPath(urlPath); + if (!decoded) return null; + const normalized = decoded.replace(/^\/+/, ''); + const resolved = path.resolve(root, normalized); + const relative = path.relative(root, resolved); + if (relative.startsWith('..') || path.isAbsolute(relative)) return null; + return resolved; +} + +/* ── HTTP server ─────────────────────────────────────────────────────── */ +const server = http.createServer((req, res) => { + const urlPath = (req.url || '/').split('?')[0]; + + /* MTime endpoint for client-side polling */ + if (urlPath === '/__mtime') { + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Content-Type', 'text/plain'); + res.end(String(scanMtime(root))); + return; + } + + let filePath = urlPath; + if (filePath === '/') filePath = '/index.html'; + const resolved = resolveWithinRoot(filePath); + if (!resolved) { + res.statusCode = 403; + res.end('Forbidden'); + return; + } + fs.readFile(resolved, (err, data) => { + if (err) { + res.statusCode = 404; + res.end('Not found'); + return; + } + res.setHeader('Content-Type', contentType(resolved)); + res.end(filePath.endsWith('.html') ? injectReloadScript(data) : data); + }); +}); + +server.listen(port, host, () => { + console.log(`Dev server running at http://${host}:${port}`); +}); diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs new file mode 100644 index 0000000..a4dbd61 --- /dev/null +++ b/scripts/smoke-test.mjs @@ -0,0 +1,23 @@ +import { readdirSync } from 'fs'; +import { execSync } from 'child_process'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const dir = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'js'); +const files = readdirSync(dir).filter(f => f.endsWith('.js')); + +let failed = 0; +for (const f of files) { + try { + execSync(`node --check "${join(dir, f)}"`, { stdio: 'pipe' }); + } catch { + console.error(`FAIL: ${f}`); + failed++; + } +} + +if (failed) { + console.error(`\n${failed} file(s) failed syntax check.`); + process.exit(1); +} +console.log(`OK (${files.length} files)`); diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index b21bd68..6f36833 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -5,3 +5,6 @@ # Generated by Tauri # will have schema files for capabilities auto-completion /gen/schemas + +# Updater signing keys (private key must never be committed) +/keys/ diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8725ec0..bc3baac 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cdpaint" -version = "0.1.0" +version = "1.1.6" description = "A Tauri App" authors = ["you"] edition = "2021" @@ -18,10 +18,12 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2.2", features = [] } tauri-plugin-opener = "2" tauri-plugin-dialog = "2" +tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -tauri-plugin-single-instance = "2.4.0" -tauri-plugin-fs = "2.4.5" +tauri-plugin-single-instance = "2.4.2" +url = "2" +uuid = { version = "1", features = ["v4"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 3463a6e..3422eef 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -12,17 +12,6 @@ "core:window:allow-toggle-maximize", "core:window:allow-close", "core:window:allow-start-dragging", - "core:window:allow-set-resizable", - "fs:allow-read", - "fs:allow-read-dir", - "fs:allow-read-file", - "fs:allow-write", - "fs:allow-write-file", - "fs:allow-write-text-file", - "fs:allow-create", - { - "identifier": "fs:scope", - "allow": ["**"] - } + "core:window:allow-set-resizable" ] } diff --git a/src-tauri/cargo-wrapper.cmd b/src-tauri/cargo-wrapper.cmd new file mode 100644 index 0000000..38ab9e0 --- /dev/null +++ b/src-tauri/cargo-wrapper.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +set SCRIPT_DIR=%~dp0 +cargo --manifest-path "%SCRIPT_DIR%Cargo.toml" %* diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index e268419..fa69528 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index 675bb91..ed097db 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index b35af1f..53e1713 100644 Binary files a/src-tauri/icons/32x32.png 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..b40817a 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 index 92095c7..7ca38ee 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index 888c785..e1c93dd 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 525acd7..2aaf4bb 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index dc7ca12..72c397c 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 1c63f29..fc9b768 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index e92f10c..aa0139e 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index 537c7f1..adbd91c 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index c66ccee..1a8611a 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index 908f1a9..57d9877 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 7f5bd96..fda32e6 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..0ace21e Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..6ee2b4a Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..239f17d Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..3183793 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ce7b470 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..17e1c08 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..88a1b11 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..7ffcd3f Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..ef803b6 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..172c584 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..45b8022 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..fcd4d74 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..dd5a938 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..0faefe7 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c7b249d Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 2c4e084..d96a1cd 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 30fb035..b072a0d 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index f4b96e9..fdef260 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..4000343 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..8e24d62 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..8e24d62 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..ae5e37e Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..f67a46c Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..2ba11dc Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..2ba11dc Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..2935f6f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..8e24d62 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..30f86bf Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..30f86bf Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..730c5f5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..b914039 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..730c5f5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..e12fa0d Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..0ed56bc Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..7c152fc Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..2fcb496 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/splash.png b/src-tauri/splash.png new file mode 100644 index 0000000..bccdc47 Binary files /dev/null and b/src-tauri/splash.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 13f3af6..f537645 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,37 +1,369 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Mutex; -use std::thread; -use std::time::Duration; use tauri::{Emitter, Manager}; +use tauri_plugin_dialog::DialogExt; +use tauri_plugin_updater::UpdaterExt; +use serde::{Deserialize, Serialize}; +use url::Url; + +const MIN_WINDOW_WIDTH: u32 = 400; +const MIN_WINDOW_HEIGHT: u32 = 400; #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } +#[tauri::command] +fn get_app_version(app: tauri::AppHandle) -> String { + app.package_info().version.to_string() +} + +#[derive(Debug, Clone, Serialize)] +struct UpdateCheckResponse { + current_version: String, + available: bool, + version: Option, + notes: Option, + date: Option, + download_url: Option, +} + +#[tauri::command] +async fn updater_check(app: tauri::AppHandle) -> Result { + let current_version = app.package_info().version.to_string(); + let updater = app.updater().map_err(|e| e.to_string())?; + let Some(update) = updater.check().await.map_err(|e| e.to_string())? else { + return Ok(UpdateCheckResponse { + current_version, + available: false, + version: None, + notes: None, + date: None, + download_url: None, + }); + }; + + Ok(UpdateCheckResponse { + current_version, + available: true, + version: Some(update.version), + notes: update.body, + date: update.date.map(|d| d.to_string()), + download_url: Some(update.download_url.to_string()), + }) +} + +#[tauri::command] +async fn updater_download_and_install(app: tauri::AppHandle) -> Result { + let updater = app.updater().map_err(|e| e.to_string())?; + let Some(update) = updater.check().await.map_err(|e| e.to_string())? else { + return Ok(false); + }; + update + .download_and_install(|_chunk_len, _total| {}, || {}) + .await + .map_err(|e| e.to_string())?; + Ok(true) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedWindowState { + x: i32, + y: i32, + width: u32, + height: u32, + maximized: bool, +} + +fn window_state_path(app: &tauri::AppHandle) -> Option { + let dir = app.path().app_config_dir().ok()?; + Some(dir.join("window-state.json")) +} + +fn read_saved_window_state(app: &tauri::AppHandle) -> Option { + let path = window_state_path(app)?; + let raw = std::fs::read(path).ok()?; + serde_json::from_slice::(&raw).ok() +} + +fn write_saved_window_state(app: &tauri::AppHandle, state: &SavedWindowState) -> Result<(), String> { + let path = window_state_path(app).ok_or_else(|| "no config path".to_string())?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| format!("create config dir failed: {}", e))?; + } + let bytes = serde_json::to_vec_pretty(state).map_err(|e| format!("serialize window state failed: {}", e))?; + std::fs::write(path, bytes).map_err(|e| format!("write window state failed: {}", e)) +} + +fn current_window_state(window: &tauri::WebviewWindow) -> Option { + let pos = window.outer_position().ok()?; + let size = window.outer_size().ok()?; + let maximized = window.is_maximized().ok().unwrap_or(false); + Some(SavedWindowState { + x: pos.x, + y: pos.y, + width: size.width, + height: size.height, + maximized, + }) +} + +fn minimum_window_size() -> tauri::Size { + tauri::Size::Physical(tauri::PhysicalSize::new( + MIN_WINDOW_WIDTH, + MIN_WINDOW_HEIGHT, + )) +} + +fn enforce_min_window_size(window: &tauri::WebviewWindow) { + let _ = window.set_min_size(Some(minimum_window_size())); +} + +/// Returns true if the point (x, y) falls within any currently connected monitor. +fn position_on_visible_monitor(window: &tauri::WebviewWindow, x: i32, y: i32) -> bool { + let monitors = match window.available_monitors() { + Ok(m) => m, + // If we can't enumerate monitors, trust the saved value rather than + // risk moving a perfectly good window. + Err(_) => return true, + }; + if monitors.is_empty() { + return true; + } + monitors.iter().any(|m| { + let pos = m.position(); + let size = m.size(); + x >= pos.x + && y >= pos.y + && x < pos.x + size.width as i32 + && y < pos.y + size.height as i32 + }) +} + +/// Move the window to the centre of the primary (or first available) monitor. +fn center_on_primary_monitor(window: &tauri::WebviewWindow) { + let monitor = window + .primary_monitor() + .ok() + .flatten() + .or_else(|| window.available_monitors().ok().and_then(|m| m.into_iter().next())); + let monitor = match monitor { + Some(m) => m, + None => { + let _ = window.center(); + return; + } + }; + let pos = monitor.position(); + let size = monitor.size(); + let win = window + .outer_size() + .unwrap_or(tauri::PhysicalSize::new(1200u32, 800u32)); + let x = pos.x + ((size.width as i32 - win.width as i32) / 2).max(0); + let y = pos.y + ((size.height as i32 - win.height as i32) / 2).max(0); + let _ = window.set_position(tauri::Position::Physical(tauri::PhysicalPosition::new(x, y))); +} + +fn apply_startup_window_state( + window: &tauri::WebviewWindow, + saved: Option<&SavedWindowState>, + default_maximized: bool, +) -> bool { + if let Some(state) = saved { + // A position saved while a now-disconnected monitor was attached would + // otherwise be reapplied verbatim, placing the window off-screen + // (invisible, but the process keeps running). Keep the window on a + // visible monitor before restoring it. + if state.maximized { + if !position_on_visible_monitor(window, state.x, state.y) { + center_on_primary_monitor(window); + } + // Apply maximized state after window creation to avoid a visible + // resize frame on Windows when using frameless windows. + let _ = window.maximize(); + return true; + } + if state.width > 0 && state.height > 0 { + let width = state.width.max(MIN_WINDOW_WIDTH); + let height = state.height.max(MIN_WINDOW_HEIGHT); + let _ = window.set_size(tauri::Size::Physical(tauri::PhysicalSize::new( + width, + height, + ))); + } + if position_on_visible_monitor(window, state.x, state.y) { + let _ = window.set_position(tauri::Position::Physical(tauri::PhysicalPosition::new( + state.x, state.y, + ))); + } else { + center_on_primary_monitor(window); + } + return false; + } + + if default_maximized { + let _ = window.maximize(); + return true; + } + + false +} + +fn install_maximized_resizable_guard(window: &tauri::WebviewWindow, _startup_maximized: bool) { + // Keep maximized windows resizable so custom frameless title-bar interactions + // (drag-to-restore, minimize/maximize buttons) stay functional on Windows. + let _ = window.set_resizable(true); +} + +fn install_window_state_persistence(app: &tauri::AppHandle, window: &tauri::WebviewWindow) { + let saved = read_saved_window_state(app); + let should_restore_maximized = apply_startup_window_state(window, saved.as_ref(), true); + install_maximized_resizable_guard(window, should_restore_maximized); + + let initial = saved + .filter(|s| !s.maximized) + .or_else(|| current_window_state(window)) + .unwrap_or(SavedWindowState { + x: 0, + y: 0, + width: 1200, + height: 800, + maximized: false, + }); + let last_normal = std::sync::Arc::new(Mutex::new(initial)); + let last_maximized = std::sync::Arc::new(Mutex::new(should_restore_maximized)); + + let app_handle = app.clone(); + let win = window.clone(); + let last_normal_state = last_normal.clone(); + let last_maximized_state = last_maximized.clone(); + window.on_window_event(move |event| match event { + tauri::WindowEvent::Moved(pos) => { + let is_max = win.is_maximized().ok().unwrap_or(false); + if let Ok(mut max_flag) = last_maximized_state.lock() { + *max_flag = is_max; + } + if is_max { + return; + } + if let Ok(mut state) = last_normal_state.lock() { + state.x = pos.x; + state.y = pos.y; + } + } + tauri::WindowEvent::Resized(size) => { + let is_max = win.is_maximized().ok().unwrap_or(false); + if let Ok(mut max_flag) = last_maximized_state.lock() { + *max_flag = is_max; + } + if is_max { + return; + } + if let Ok(mut state) = last_normal_state.lock() { + state.width = size.width; + state.height = size.height; + } + } + tauri::WindowEvent::Focused(_) => { + let is_max = win.is_maximized().ok().unwrap_or(false); + if let Ok(mut max_flag) = last_maximized_state.lock() { + *max_flag = is_max; + } + } + tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed => { + let maximized = win.is_maximized().ok().unwrap_or_else(|| { + last_maximized_state + .lock() + .ok() + .map(|v| *v) + .unwrap_or(false) + }); + let mut out = if let Ok(state) = last_normal_state.lock() { + state.clone() + } else { + return; + }; + if !maximized { + if let Some(cur) = current_window_state(&win) { + out.x = cur.x; + out.y = cur.y; + out.width = cur.width; + out.height = cur.height; + } + } + out.maximized = maximized; + let _ = write_saved_window_state(&app_handle, &out); + } + _ => {} + }); +} + +fn file_url_to_path(input: &str) -> Option { + let url = Url::parse(input).ok()?; + if url.scheme() != "file" { + return None; + } + + if let Ok(path) = url.to_file_path() { + return Some(path.to_string_lossy().to_string()); + } + + #[cfg(windows)] + { + // Fallback for non-standard "file://C:/..." style inputs where + // url::Url::to_file_path() couldn't parse the path. + // Normalize to file:///C:/... and re-parse through the url crate. + let raw = input.trim_start_matches("file://"); + let bytes = raw.as_bytes(); + let looks_like_drive = bytes.len() >= 3 + && bytes[0] == b'/' + && bytes[1].is_ascii_alphabetic() + && bytes[2] == b':'; + if looks_like_drive { + if let Ok(url2) = Url::parse(&format!("file:///{}", &raw[1..])) { + if let Ok(path) = url2.to_file_path() { + return Some(path.to_string_lossy().to_string()); + } + } + } + None + } + + #[cfg(not(windows))] + { + None + } +} + +fn normalize_launch_arg(arg: &str) -> Option { + let trimmed = arg.trim().trim_matches('"'); + if trimmed.is_empty() { + return None; + } + + let path = if trimmed.starts_with("file://") { + file_url_to_path(trimmed)? + } else { + trimmed.to_string() + }; + + Some(normalize_device_path(&path)) +} + fn first_file_path(args: I) -> Option where I: IntoIterator, { let mut candidates: Vec = Vec::new(); for arg in args { - let mut s = arg.trim().trim_matches('"').to_string(); - if s.is_empty() { - continue; + if let Some(s) = normalize_launch_arg(&arg) { + candidates.push(s); } - if s.starts_with("file://") { - s = s.trim_start_matches("file://").to_string(); - if s.starts_with('/') { - s = s.trim_start_matches('/').to_string(); - } - s = s.replace('/', "\\"); - } - candidates.push(s); } - // Prefer existing image files. + // Accept only existing image files passed on launch. for s in &candidates { if is_current_exe(s) { continue; @@ -41,28 +373,6 @@ where return Some(normalize_candidate(s)); } } - // Then any existing non-exe file. - for s in &candidates { - if is_current_exe(s) { - continue; - } - let p = Path::new(s); - if p.is_file() { - let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase(); - if ext != "exe" { - return Some(normalize_candidate(s)); - } - } - } - // Fallback: if it looks like an image path, accept it even if it doesn't exist yet. - for s in &candidates { - if is_current_exe(s) { - continue; - } - if is_image_path(s) { - return Some(s.to_string()); - } - } None } @@ -77,19 +387,452 @@ fn is_current_exe(path: &str) -> bool { } struct PendingFiles(Mutex>); -static WINDOW_COUNTER: AtomicU64 = AtomicU64::new(1); #[tauri::command] fn get_pending_file(window: tauri::Window, state: tauri::State<'_, PendingFiles>) -> Option { - let mut guard = state.0.lock().unwrap(); + let mut guard = state.0.lock().ok()?; let label = window.label().to_string(); guard.remove(&label) } fn is_image_path(path: &str) -> bool { let p = Path::new(path); - let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase(); - matches!(ext.as_str(), "png" | "jpg" | "jpeg" | "bmp" | "gif" | "webp") + is_image_extension(p.extension().and_then(|e| e.to_str())) +} + +fn is_image_extension(ext: Option<&str>) -> bool { + matches!( + ext.unwrap_or("").to_ascii_lowercase().as_str(), + "png" | "jpg" | "jpeg" | "bmp" | "gif" | "webp" | "ora" + ) +} + +fn normalize_device_path(path: &str) -> String { + let mut s = path.trim().to_string(); + if s.starts_with(r"\\?\UNC\") { + s = format!(r"\\{}", &s[r"\\?\UNC\".len()..]); + } else if s.starts_with(r"\\?\") { + s = s[r"\\?\".len()..].to_string(); + } + s +} + +fn normalize_to_absolute_path(path: &str) -> Result { + let trimmed = path.trim(); + let normalized = if trimmed.starts_with("file://") { + file_url_to_path(trimmed).ok_or_else(|| "invalid file URL".to_string())? + } else { + normalize_device_path(trimmed) + }; + let p = PathBuf::from(normalized); + if !p.is_absolute() { + return Err("path must be absolute".into()); + } + // Resolve `.` and `..` components logically so the OS cannot be tricked into + // writing/reading outside the directory implied by the (possibly malicious) string. + // `..` at the root is dropped, so the path can never escape the absolute root. + Ok(resolve_path_dots(&p)) +} + +/// Logically resolve `.` and `..` in an absolute path without touching the filesystem. +/// A `..` that would climb above the root is simply dropped, preventing traversal escape. +fn resolve_path_dots(path: &Path) -> PathBuf { + use std::path::Component; + let mut stack: Vec = Vec::new(); + for comp in path.components() { + match comp { + Component::ParentDir => { + if let Some(Component::Normal(_)) = stack.last() { + stack.pop(); + } + } + Component::CurDir => {} + other => stack.push(other), + } + } + let mut out = PathBuf::new(); + for c in stack { + out.push(c.as_os_str()); + } + out +} + +fn is_allowed_write_extension(ext: Option<&str>) -> bool { + matches!( + ext.unwrap_or("").to_ascii_lowercase().as_str(), + "png" | "jpg" | "jpeg" | "bmp" | "gif" | "webp" | "pal" + ) +} + +#[tauri::command] +fn read_image_file(path: String) -> Result, String> { + let p = normalize_to_absolute_path(&path)?; + if !p.is_file() { + return Err("path is not an existing file".into()); + } + if !is_image_extension(p.extension().and_then(|e| e.to_str())) { + return Err("only image files can be read".into()); + } + std::fs::read(&p).map_err(|e| format!("read failed: {}", e)) +} + +#[tauri::command] +fn write_allowed_file(path: String, data: Vec) -> Result<(), String> { + let p = normalize_to_absolute_path(&path)?; + if !is_allowed_write_extension(p.extension().and_then(|e| e.to_str())) { + return Err("file extension not allowed".into()); + } + if let Some(parent) = p.parent() { + if !parent.exists() { + return Err("target directory does not exist".into()); + } + } else { + return Err("invalid target path".into()); + } + std::fs::write(&p, data).map_err(|e| format!("write failed: {}", e)) +} + +#[derive(Debug, Clone, Serialize)] +struct ProjectNode { + name: String, + path: String, + kind: String, + ext: Option, + size: u64, + children: Vec, +} + +const MAX_SCAN_DEPTH: usize = 12; +const SCAN_DIR_DENYLIST: [&str; 6] = [".git", "node_modules", "target", ".vscode", ".idea", "dist"]; + +fn scan_dir(dir: &Path, depth: usize, out: &mut ProjectNode) -> std::io::Result<()> { + let mut dir_entries: Vec<(String, PathBuf)> = Vec::new(); + let mut file_entries: Vec<(String, PathBuf, u64)> = Vec::new(); + let mut entries = std::fs::read_dir(dir)?; + while let Some(entry) = entries.next() { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + let p = entry.path(); + let meta = match std::fs::symlink_metadata(&p) { + Ok(m) => m, + Err(_) => continue, + }; + if meta.is_symlink() { + continue; + } + let fname = entry.file_name().to_string_lossy().to_string(); + if meta.is_dir() { + if depth >= MAX_SCAN_DEPTH { + continue; + } + if SCAN_DIR_DENYLIST.iter().any(|d| d.eq_ignore_ascii_case(&fname)) { + continue; + } + dir_entries.push((fname, p)); + } else if meta.is_file() { + let ext = p + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_ascii_lowercase()); + if matches!(ext.as_deref(), Some("png") | Some("pal")) { + file_entries.push((fname, p, meta.len())); + } + } + } + for (fname, p) in dir_entries { + let mut node = ProjectNode { + name: fname, + path: p.to_string_lossy().to_string(), + kind: "dir".to_string(), + ext: None, + size: 0, + children: Vec::new(), + }; + if scan_dir(&p, depth + 1, &mut node).is_ok() { + if !node.children.is_empty() { + out.children.push(node); + } + } + } + for (fname, p, size) in file_entries { + out.children.push(ProjectNode { + name: fname, + path: p.to_string_lossy().to_string(), + kind: "file".to_string(), + ext: p + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_ascii_lowercase()), + size, + children: Vec::new(), + }); + } + out.children.sort_by(|a, b| { + if a.kind != b.kind { + return a.kind.cmp(&b.kind); + } + a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase()) + }); + Ok(()) +} + +#[tauri::command] +fn scan_project(path: String) -> Result { + let p = normalize_to_absolute_path(&path)?; + if !p.is_dir() { + return Err("path is not an existing directory".into()); + } + let mut root = ProjectNode { + name: p + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| p.to_string_lossy().to_string()), + path: p.to_string_lossy().to_string(), + kind: "dir".to_string(), + ext: None, + size: 0, + children: Vec::new(), + }; + scan_dir(&p, 0, &mut root).map_err(|e| format!("scan failed: {}", e))?; + Ok(root) +} + +#[tauri::command] +fn read_text_file(path: String) -> Result { + let p = normalize_to_absolute_path(&path)?; + if !p.is_file() { + return Err("path is not an existing file".into()); + } + let ext = p + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_ascii_lowercase()); + if !matches!(ext.as_deref(), Some("pal") | Some("txt")) { + return Err("only .pal and .txt files can be read".into()); + } + std::fs::read_to_string(&p).map_err(|e| format!("read failed: {}", e)) +} + +#[derive(Debug, Deserialize)] +struct ExportFilePayload { + name: String, + data: Vec, +} + +fn resolve_dialog_file_path(file_path: tauri_plugin_dialog::FilePath) -> Result { + if let Ok(path) = file_path.clone().into_path() { + if path.is_absolute() { + return Ok(path); + } + } + + // Some backends may serialize the picked path as a URL string. + let raw = file_path.to_string(); + normalize_to_absolute_path(&raw) +} + +fn write_export_files_to_directory(dir: &Path, files: Vec) -> Result<(), String> { + if !dir.is_dir() { + return Err("target directory does not exist".into()); + } + for file in files { + let name = file.name.trim(); + if name.is_empty() { + return Err("file name is empty".into()); + } + if name.contains('\\') || name.contains('/') || name.contains(':') { + return Err("file name must not contain path separators".into()); + } + let out = dir.join(name); + if !is_allowed_write_extension(out.extension().and_then(|e| e.to_str())) { + return Err(format!("file extension not allowed: {}", name)); + } + std::fs::write(&out, file.data).map_err(|e| format!("write failed ({}): {}", name, e))?; + } + Ok(()) +} + +#[tauri::command] +fn write_export_files(directory: String, files: Vec) -> Result<(), String> { + let dir = normalize_to_absolute_path(&directory)?; + write_export_files_to_directory(&dir, files) +} + +#[tauri::command] +async fn write_export_files_with_dialog(app: tauri::AppHandle, files: Vec) -> Result { + let folder = tauri::async_runtime::spawn_blocking(move || { + app.dialog() + .file() + .set_title("Choose Export Folder") + .blocking_pick_folder() + }) + .await + .map_err(|e| e.to_string())?; + let Some(folder) = folder else { + return Ok(false); + }; + let dir = resolve_dialog_file_path(folder)?; + write_export_files_to_directory(&dir, files)?; + Ok(true) +} + +#[tauri::command] +async fn write_export_files_with_save_dialog( + app: tauri::AppHandle, + files: Vec, + suggested_name: Option, +) -> Result { + let mut dialog = app.dialog().file().set_title("Save Export Location"); + if let Some(name) = suggested_name { + let trimmed = name.trim(); + if !trimmed.is_empty() { + dialog = dialog.set_file_name(trimmed.to_string()); + } + } + + let selection = tauri::async_runtime::spawn_blocking(move || dialog.blocking_save_file()) + .await + .map_err(|e| e.to_string())?; + let Some(selection) = selection else { + return Ok(false); + }; + let selected_path = resolve_dialog_file_path(selection)?; + let dir = if selected_path.is_dir() { + selected_path + } else { + selected_path + .parent() + .map(Path::to_path_buf) + .ok_or_else(|| "selected path has no parent directory".to_string())? + }; + write_export_files_to_directory(&dir, files)?; + Ok(true) +} + +#[tauri::command] +async fn write_export_file_with_save_dialog( + app: tauri::AppHandle, + file: ExportFilePayload, + suggested_name: Option, + default_directory: Option, +) -> Result, String> { + let mut dialog = app.dialog().file().set_title("Save Export File"); + + if let Some(dir) = default_directory { + let trimmed = dir.trim(); + if !trimmed.is_empty() { + let p = normalize_to_absolute_path(trimmed)?; + if p.is_dir() { + dialog = dialog.set_directory(&p); + } + } + } + + let mut suggested = suggested_name + .unwrap_or_else(|| file.name.clone()) + .trim() + .to_string(); + if suggested.is_empty() { + suggested = file.name.trim().to_string(); + } + if !suggested.is_empty() { + dialog = dialog.set_file_name(suggested); + } + + let selection = tauri::async_runtime::spawn_blocking(move || dialog.blocking_save_file()) + .await + .map_err(|e| e.to_string())?; + let Some(selection) = selection else { + return Ok(None); + }; + + let selected_path = resolve_dialog_file_path(selection)?; + let save_path = if selected_path.is_dir() { + let base_name = file.name.trim(); + if base_name.is_empty() { + return Err("file name is empty".into()); + } + selected_path.join(base_name) + } else { + selected_path + }; + + let parent = save_path + .parent() + .map(Path::to_path_buf) + .ok_or_else(|| "selected path has no parent directory".to_string())?; + if !parent.is_dir() { + return Err("target directory does not exist".into()); + } + if !is_allowed_write_extension(save_path.extension().and_then(|e| e.to_str())) { + return Err("file extension not allowed".into()); + } + + std::fs::write(&save_path, file.data).map_err(|e| format!("write failed: {}", e))?; + Ok(Some(normalize_device_path(&parent.to_string_lossy()))) +} + +#[tauri::command] +fn show_current_window(window: tauri::Window) -> Result<(), String> { + window.show().map_err(|e| format!("show failed: {}", e)) +} + +fn center_window_on_monitor(window: &tauri::Window) -> Result<(), String> { + if let Some(monitor) = window + .current_monitor() + .map_err(|e| format!("current_monitor failed: {}", e))? + { + let monitor_pos = monitor.position(); + let monitor_size = monitor.size(); + let win_size = window + .outer_size() + .map_err(|e| format!("outer_size failed: {}", e))?; + let x = monitor_pos.x + ((monitor_size.width as i32 - win_size.width as i32) / 2).max(0); + let y = monitor_pos.y + ((monitor_size.height as i32 - win_size.height as i32) / 2).max(0); + window + .set_position(tauri::Position::Physical(tauri::PhysicalPosition::new(x, y))) + .map_err(|e| format!("set_position failed: {}", e))?; + return Ok(()); + } + window.center().map_err(|e| format!("center failed: {}", e)) +} + +#[tauri::command] +fn toggle_current_window_fullscreen(window: tauri::Window) -> Result { + let is_fullscreen = window + .is_fullscreen() + .map_err(|e| format!("is_fullscreen failed: {}", e))?; + if is_fullscreen { + window + .set_fullscreen(false) + .map_err(|e| format!("set_fullscreen failed: {}", e))?; + center_window_on_monitor(&window)?; + return Ok(false); + } + center_window_on_monitor(&window)?; + window + .set_fullscreen(true) + .map_err(|e| format!("set_fullscreen failed: {}", e))?; + Ok(true) +} + +#[tauri::command] +async fn pick_export_folder(app: tauri::AppHandle) -> Result, String> { + let folder = tauri::async_runtime::spawn_blocking(move || { + app.dialog() + .file() + .set_title("Choose Export Folder") + .blocking_pick_folder() + }) + .await + .map_err(|e| e.to_string())?; + let Some(folder) = folder else { + return Ok(None); + }; + let path = resolve_dialog_file_path(folder)?; + Ok(Some(normalize_device_path(&path.to_string_lossy()))) } fn normalize_candidate(path: &str) -> String { @@ -102,15 +845,7 @@ fn normalize_candidate(path: &str) -> String { } fn next_window_label() -> String { - let n = WINDOW_COUNTER.fetch_add(1, Ordering::Relaxed); - format!( - "file-{}-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(), - n - ) + format!("file-{}", uuid::Uuid::new_v4()) } fn collect_image_paths(args: I) -> Vec @@ -119,27 +854,49 @@ where { let mut out = Vec::new(); for arg in args { - let mut s = arg.trim().trim_matches('"').to_string(); - if s.is_empty() { - continue; - } - if s.starts_with("file://") { - s = s.trim_start_matches("file://").to_string(); - if s.starts_with('/') { - s = s.trim_start_matches('/').to_string(); - } - s = s.replace('/', "\\"); - } + let Some(s) = normalize_launch_arg(&arg) else { continue }; if is_current_exe(&s) { continue; } - if is_image_path(&s) { + if is_image_path(&s) && Path::new(&s).is_file() { out.push(normalize_candidate(&s)); } } out } +fn spawn_additional_window( + app_handle: &tauri::AppHandle, + saved_window_state: Option<&SavedWindowState>, + pending_path: Option, +) { + let label = next_window_label(); + if let Some(path) = pending_path { + if let Ok(mut guard) = app_handle.state::().0.lock() { + guard.insert(label.clone(), path); + } + } + if let Ok(window) = tauri::WebviewWindowBuilder::new( + app_handle, + label, + tauri::WebviewUrl::App("index.html".into()), + ) + .title("cdpaint") + .visible(false) + .decorations(false) + .shadow(true) + .resizable(true) + .inner_size(1920.0, 1057.0) + .min_inner_size(MIN_WINDOW_WIDTH as f64, MIN_WINDOW_HEIGHT as f64) + .build() + { + enforce_min_window_size(&window); + let should_restore_maximized = + apply_startup_window_state(&window, saved_window_state, true); + install_maximized_resizable_guard(&window, should_restore_maximized); + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -147,55 +904,91 @@ pub fn run() { .manage(PendingFiles(Mutex::new(HashMap::new()))) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| { let paths = collect_image_paths(argv.into_iter()); - if paths.is_empty() { - return; - } let app_handle = app.clone(); tauri::async_runtime::spawn(async move { + let saved_window_state = read_saved_window_state(&app_handle); + if paths.is_empty() { + // Relaunch from desktop/taskbar shortcut without file args: + // open a fresh blank window in the running instance. + spawn_additional_window(&app_handle, saved_window_state.as_ref(), None); + return; + } for path in paths { let windows = app_handle.webview_windows(); if windows.is_empty() { let _ = app_handle.emit("open-file", path); continue; } - let label = next_window_label(); - if let Ok(mut guard) = app_handle.state::().0.lock() { - guard.insert(label.clone(), path); - } - let _ = tauri::WebviewWindowBuilder::new( - &app_handle, - label, - tauri::WebviewUrl::App("index.html".into()), - ) - .title("cdpaint") - .decorations(false) - .shadow(true) - .resizable(true) - .inner_size(1920.0, 1057.0) - .maximized(true) - .build(); + spawn_additional_window(&app_handle, saved_window_state.as_ref(), Some(path)); } }); })) - .invoke_handler(tauri::generate_handler![greet, get_pending_file]) + .invoke_handler(tauri::generate_handler![ + greet, + get_app_version, + updater_check, + updater_download_and_install, + get_pending_file, + read_image_file, + write_allowed_file, + write_export_files, + write_export_files_with_dialog, + write_export_files_with_save_dialog, + write_export_file_with_save_dialog, + show_current_window, + toggle_current_window_fullscreen, + pick_export_folder, + scan_project, + read_text_file + ]) .setup(|app| { + if let Some(main_window) = app.get_webview_window("main") { + enforce_min_window_size(&main_window); + install_window_state_persistence(&app.handle().clone(), &main_window); + } let args: Vec = std::env::args().collect(); if let Some(path) = first_file_path(args.into_iter().skip(1)) { - let handle = app.handle().clone(); - thread::spawn(move || { - thread::sleep(Duration::from_millis(500)); - if let Some(window) = handle.get_webview_window("main") { - let _ = window.emit("open-file", path); - } else { - let _ = handle.emit("open-file", path); - } - }); + // Queue the startup file immediately so the frontend can pull it + // on first boot without waiting for an artificial delay. + if let Ok(mut guard) = app.state::().0.lock() { + guard.insert("main".to_string(), path); + } } Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_path_dots_removes_dot_and_dotdot() { + let p = Path::new(r"C:\Users\me\Pictures\..\..\evil.png"); + let resolved = resolve_path_dots(p); + assert_eq!(resolved, Path::new(r"C:\Users\evil.png")); + } + + #[test] + fn resolve_path_dots_cannot_climb_above_root() { + // All `..` that would climb above the drive root are dropped, so the + // path can never escape the absolute root via traversal. + let p = Path::new(r"C:\a\b\c\..\..\..\..\evil.png"); + let resolved = resolve_path_dots(p); + assert_eq!(resolved, Path::new(r"C:\evil.png")); + assert!(resolved.is_absolute()); + assert!(!resolved.to_string_lossy().contains("..")); + } + + #[test] + fn normalize_to_absolute_path_blocks_traversal() { + let p = normalize_to_absolute_path(r"C:\Users\me\Pictures\..\..\evil.png") + .expect("should normalize"); + assert_eq!(p, Path::new(r"C:\Users\evil.png")); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f86ba5a..1584e76 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "cdpaint", - "version": "0.1.0", + "version": "1.1.6", "identifier": "com.frenc.cdpaint", "build": { "beforeDevCommand": "npm run dev", @@ -14,15 +14,20 @@ "windows": [ { "decorations": false, + "devtools": true, + "dragDropEnabled": false, "fullscreen": false, "height": 800, + "shadow": true, "resizable": true, "title": "CDPaint", + "visible": false, "width": 1200 } ], "security": { - "csp": null + "dangerousDisableAssetCspModification": true, + "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' http://ipc.localhost https://github.com; worker-src 'self' blob:" } }, "bundle": { @@ -67,5 +72,16 @@ "mimeType": "image/*" } ] + }, + "plugins": { + "updater": { + "endpoints": [ + "https://github.com/FrCynda/CDPaint/releases/latest/download/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEMzQjJGRTVGRkE4RjUyN0MKUldSOFVvLzZYLzZ5dzVqZnhxaVhVNmhyeDdoWldKRGdXSG5sT3Rnd0p2cVBJMWlyemQ4NEY2T1gK", + "windows": { + "installMode": "passive" + } + } } } diff --git a/src/app-icon.png b/src/app-icon.png new file mode 100644 index 0000000..53e1713 Binary files /dev/null and b/src/app-icon.png differ diff --git a/src/assets/Free.png b/src/assets/Free.png new file mode 100644 index 0000000..9394f7e Binary files /dev/null and b/src/assets/Free.png differ diff --git a/src/assets/Freehand.png b/src/assets/Freehand.png new file mode 100644 index 0000000..3037c82 Binary files /dev/null and b/src/assets/Freehand.png differ diff --git a/src/assets/Gradient.png b/src/assets/Gradient.png new file mode 100644 index 0000000..7011364 Binary files /dev/null and b/src/assets/Gradient.png differ diff --git a/src/assets/GradientHandle.png b/src/assets/GradientHandle.png new file mode 100644 index 0000000..31ee970 Binary files /dev/null and b/src/assets/GradientHandle.png differ diff --git a/src/assets/Paintbrush.png b/src/assets/Paintbrush.png new file mode 100644 index 0000000..85b42f6 Binary files /dev/null and b/src/assets/Paintbrush.png differ diff --git "a/src/assets/Pok\303\251Project.png" "b/src/assets/Pok\303\251Project.png" new file mode 100644 index 0000000..79db274 Binary files /dev/null and "b/src/assets/Pok\303\251Project.png" differ diff --git a/src/assets/ResizeUI.png b/src/assets/ResizeUI.png new file mode 100644 index 0000000..c58fb9e Binary files /dev/null and b/src/assets/ResizeUI.png differ diff --git a/src/assets/SkewUI.png b/src/assets/SkewUI.png new file mode 100644 index 0000000..2c43c0b Binary files /dev/null and b/src/assets/SkewUI.png differ diff --git a/src/assets/anchor.png b/src/assets/anchor.png new file mode 100644 index 0000000..56cd1a0 Binary files /dev/null and b/src/assets/anchor.png differ diff --git a/src/assets/crop.png b/src/assets/crop.png new file mode 100644 index 0000000..2b43227 Binary files /dev/null and b/src/assets/crop.png differ diff --git a/src/assets/flip-horizontal.png b/src/assets/flip-horizontal.png new file mode 100644 index 0000000..5b28258 Binary files /dev/null and b/src/assets/flip-horizontal.png differ diff --git a/src/assets/flip-vertical.png b/src/assets/flip-vertical.png new file mode 100644 index 0000000..56b654c Binary files /dev/null and b/src/assets/flip-vertical.png differ diff --git a/src/assets/fluent-emoji/artist_palette_flat.svg b/src/assets/fluent-emoji/artist_palette_flat.svg new file mode 100644 index 0000000..a97e454 --- /dev/null +++ b/src/assets/fluent-emoji/artist_palette_flat.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/fluent-emoji/check_mark_button_flat.svg b/src/assets/fluent-emoji/check_mark_button_flat.svg new file mode 100644 index 0000000..62a07a5 --- /dev/null +++ b/src/assets/fluent-emoji/check_mark_button_flat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/fluent-emoji/floppy_disk_flat.svg b/src/assets/fluent-emoji/floppy_disk_flat.svg new file mode 100644 index 0000000..061a2d7 --- /dev/null +++ b/src/assets/fluent-emoji/floppy_disk_flat.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/fluent-emoji/globe_with_meridians_flat.svg b/src/assets/fluent-emoji/globe_with_meridians_flat.svg new file mode 100644 index 0000000..14f1efc --- /dev/null +++ b/src/assets/fluent-emoji/globe_with_meridians_flat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/fluent-emoji/inbox_tray_flat.svg b/src/assets/fluent-emoji/inbox_tray_flat.svg new file mode 100644 index 0000000..e0c319e --- /dev/null +++ b/src/assets/fluent-emoji/inbox_tray_flat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/fluent-emoji/magic_wand_flat.svg b/src/assets/fluent-emoji/magic_wand_flat.svg new file mode 100644 index 0000000..33adb48 --- /dev/null +++ b/src/assets/fluent-emoji/magic_wand_flat.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/fluent-emoji/memo_flat.svg b/src/assets/fluent-emoji/memo_flat.svg new file mode 100644 index 0000000..dc13966 --- /dev/null +++ b/src/assets/fluent-emoji/memo_flat.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/fluent-emoji/open_file_folder_flat.svg b/src/assets/fluent-emoji/open_file_folder_flat.svg new file mode 100644 index 0000000..46ef362 --- /dev/null +++ b/src/assets/fluent-emoji/open_file_folder_flat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/fluent-emoji/outbox_tray_flat.svg b/src/assets/fluent-emoji/outbox_tray_flat.svg new file mode 100644 index 0000000..e4e0228 --- /dev/null +++ b/src/assets/fluent-emoji/outbox_tray_flat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/fluent-emoji/page_facing_up_flat.svg b/src/assets/fluent-emoji/page_facing_up_flat.svg new file mode 100644 index 0000000..6fd1e7b --- /dev/null +++ b/src/assets/fluent-emoji/page_facing_up_flat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/handle.png b/src/assets/handle.png new file mode 100644 index 0000000..0604f40 Binary files /dev/null and b/src/assets/handle.png differ diff --git a/src/assets/layers.png b/src/assets/layers.png new file mode 100644 index 0000000..f8cbd81 Binary files /dev/null and b/src/assets/layers.png differ diff --git a/src/assets/mode-icons/15bpp.png b/src/assets/mode-icons/15bpp.png new file mode 100644 index 0000000..1002cee Binary files /dev/null and b/src/assets/mode-icons/15bpp.png differ diff --git a/src/assets/mode-icons/16bpp.png b/src/assets/mode-icons/16bpp.png new file mode 100644 index 0000000..1a0c6ae Binary files /dev/null and b/src/assets/mode-icons/16bpp.png differ diff --git a/src/assets/mode-icons/1bpp.png b/src/assets/mode-icons/1bpp.png new file mode 100644 index 0000000..4165684 Binary files /dev/null and b/src/assets/mode-icons/1bpp.png differ diff --git a/src/assets/mode-icons/24bpp.png b/src/assets/mode-icons/24bpp.png new file mode 100644 index 0000000..99bae8e Binary files /dev/null and b/src/assets/mode-icons/24bpp.png differ diff --git a/src/assets/mode-icons/4bpp.png b/src/assets/mode-icons/4bpp.png new file mode 100644 index 0000000..61811c2 Binary files /dev/null and b/src/assets/mode-icons/4bpp.png differ diff --git a/src/assets/mode-icons/8bpp.png b/src/assets/mode-icons/8bpp.png new file mode 100644 index 0000000..98ad997 Binary files /dev/null and b/src/assets/mode-icons/8bpp.png differ diff --git a/src/assets/normal.pal b/src/assets/normal.pal new file mode 100644 index 0000000..a9c99df --- /dev/null +++ b/src/assets/normal.pal @@ -0,0 +1,19 @@ +JASC-PAL +0100 +16 +239 25 33 +0 0 0 +247 140 58 +214 90 49 +247 206 58 +255 255 255 +148 74 58 +74 82 82 +148 148 173 +123 123 132 +214 197 222 +156 156 189 +0 0 0 +0 0 0 +0 0 0 +0 0 0 diff --git a/src/assets/resize.png b/src/assets/resize.png new file mode 100644 index 0000000..1694495 Binary files /dev/null and b/src/assets/resize.png differ diff --git a/src/assets/ribbon-icons/clipboard.png b/src/assets/ribbon-icons/clipboard.png new file mode 100644 index 0000000..129d298 Binary files /dev/null and b/src/assets/ribbon-icons/clipboard.png differ diff --git a/src/assets/rotate selection.png b/src/assets/rotate selection.png new file mode 100644 index 0000000..32baa01 Binary files /dev/null and b/src/assets/rotate selection.png differ diff --git a/src/assets/rotate-180.png b/src/assets/rotate-180.png new file mode 100644 index 0000000..2c3cf6b Binary files /dev/null and b/src/assets/rotate-180.png differ diff --git a/src/assets/rotate-left.png b/src/assets/rotate-left.png new file mode 100644 index 0000000..eb070eb Binary files /dev/null and b/src/assets/rotate-left.png differ diff --git a/src/assets/rotate-right.png b/src/assets/rotate-right.png new file mode 100644 index 0000000..f39ab85 Binary files /dev/null and b/src/assets/rotate-right.png differ diff --git a/src/assets/rotate.png b/src/assets/rotate.png new file mode 100644 index 0000000..f39ab85 Binary files /dev/null and b/src/assets/rotate.png differ diff --git a/src/assets/titlebar-icons/redo-disabled.png b/src/assets/titlebar-icons/redo-disabled.png new file mode 100644 index 0000000..422620d Binary files /dev/null and b/src/assets/titlebar-icons/redo-disabled.png differ diff --git a/src/assets/titlebar-icons/redo.png b/src/assets/titlebar-icons/redo.png new file mode 100644 index 0000000..81b91aa Binary files /dev/null and b/src/assets/titlebar-icons/redo.png differ diff --git a/src/assets/titlebar-icons/undo-disabled.png b/src/assets/titlebar-icons/undo-disabled.png new file mode 100644 index 0000000..232f880 Binary files /dev/null and b/src/assets/titlebar-icons/undo-disabled.png differ diff --git a/src/assets/titlebar-icons/undo.png b/src/assets/titlebar-icons/undo.png new file mode 100644 index 0000000..4172d22 Binary files /dev/null and b/src/assets/titlebar-icons/undo.png differ diff --git a/src/assets/toolbar-icons/circle.png b/src/assets/toolbar-icons/circle.png new file mode 100644 index 0000000..8a113b6 Binary files /dev/null and b/src/assets/toolbar-icons/circle.png differ diff --git a/src/assets/toolbar-icons/curve.png b/src/assets/toolbar-icons/curve.png new file mode 100644 index 0000000..e04e191 Binary files /dev/null and b/src/assets/toolbar-icons/curve.png differ diff --git a/src/assets/toolbar-icons/eraser.png b/src/assets/toolbar-icons/eraser.png new file mode 100644 index 0000000..8886e58 Binary files /dev/null and b/src/assets/toolbar-icons/eraser.png differ diff --git a/src/assets/toolbar-icons/fill.png b/src/assets/toolbar-icons/fill.png new file mode 100644 index 0000000..b3bcca7 Binary files /dev/null and b/src/assets/toolbar-icons/fill.png differ diff --git a/src/assets/toolbar-icons/line.png b/src/assets/toolbar-icons/line.png new file mode 100644 index 0000000..0c9d326 Binary files /dev/null and b/src/assets/toolbar-icons/line.png differ diff --git a/src/assets/toolbar-icons/original/circle.png b/src/assets/toolbar-icons/original/circle.png new file mode 100644 index 0000000..0cfef0e Binary files /dev/null and b/src/assets/toolbar-icons/original/circle.png differ diff --git a/src/assets/toolbar-icons/original/curve.png b/src/assets/toolbar-icons/original/curve.png new file mode 100644 index 0000000..fa26e41 Binary files /dev/null and b/src/assets/toolbar-icons/original/curve.png differ diff --git a/src/assets/toolbar-icons/original/eraser.png b/src/assets/toolbar-icons/original/eraser.png new file mode 100644 index 0000000..61ea201 Binary files /dev/null and b/src/assets/toolbar-icons/original/eraser.png differ diff --git a/src/assets/toolbar-icons/original/fill.png b/src/assets/toolbar-icons/original/fill.png new file mode 100644 index 0000000..954683b Binary files /dev/null and b/src/assets/toolbar-icons/original/fill.png differ diff --git a/src/assets/toolbar-icons/original/line.png b/src/assets/toolbar-icons/original/line.png new file mode 100644 index 0000000..4a2f5e1 Binary files /dev/null and b/src/assets/toolbar-icons/original/line.png differ diff --git a/src/assets/toolbar-icons/original/pencil-smart.png b/src/assets/toolbar-icons/original/pencil-smart.png new file mode 100644 index 0000000..59d53a7 Binary files /dev/null and b/src/assets/toolbar-icons/original/pencil-smart.png differ diff --git a/src/assets/toolbar-icons/original/pencil.png b/src/assets/toolbar-icons/original/pencil.png new file mode 100644 index 0000000..5841e4d Binary files /dev/null and b/src/assets/toolbar-icons/original/pencil.png differ diff --git a/src/assets/toolbar-icons/original/picker.png b/src/assets/toolbar-icons/original/picker.png new file mode 100644 index 0000000..3caab14 Binary files /dev/null and b/src/assets/toolbar-icons/original/picker.png differ diff --git a/src/assets/toolbar-icons/original/poly.png b/src/assets/toolbar-icons/original/poly.png new file mode 100644 index 0000000..2f8946b Binary files /dev/null and b/src/assets/toolbar-icons/original/poly.png differ diff --git a/src/assets/toolbar-icons/original/rect.png b/src/assets/toolbar-icons/original/rect.png new file mode 100644 index 0000000..7bf0d2b Binary files /dev/null and b/src/assets/toolbar-icons/original/rect.png differ diff --git a/src/assets/toolbar-icons/original/tri.png b/src/assets/toolbar-icons/original/tri.png new file mode 100644 index 0000000..39d9a4c Binary files /dev/null and b/src/assets/toolbar-icons/original/tri.png differ diff --git a/src/assets/toolbar-icons/original/wand-contig.png b/src/assets/toolbar-icons/original/wand-contig.png new file mode 100644 index 0000000..4ce0f0b Binary files /dev/null and b/src/assets/toolbar-icons/original/wand-contig.png differ diff --git a/src/assets/toolbar-icons/original/wand-global.png b/src/assets/toolbar-icons/original/wand-global.png new file mode 100644 index 0000000..ae5f3ab Binary files /dev/null and b/src/assets/toolbar-icons/original/wand-global.png differ diff --git a/src/assets/toolbar-icons/original/zoom.png b/src/assets/toolbar-icons/original/zoom.png new file mode 100644 index 0000000..8510f73 Binary files /dev/null and b/src/assets/toolbar-icons/original/zoom.png differ diff --git a/src/assets/toolbar-icons/pencil-smart.png b/src/assets/toolbar-icons/pencil-smart.png new file mode 100644 index 0000000..6264ca4 Binary files /dev/null and b/src/assets/toolbar-icons/pencil-smart.png differ diff --git a/src/assets/toolbar-icons/pencil.png b/src/assets/toolbar-icons/pencil.png new file mode 100644 index 0000000..06e9a82 Binary files /dev/null and b/src/assets/toolbar-icons/pencil.png differ diff --git a/src/assets/toolbar-icons/picker.png b/src/assets/toolbar-icons/picker.png new file mode 100644 index 0000000..ac066e2 Binary files /dev/null and b/src/assets/toolbar-icons/picker.png differ diff --git a/src/assets/toolbar-icons/poly.png b/src/assets/toolbar-icons/poly.png new file mode 100644 index 0000000..c120191 Binary files /dev/null and b/src/assets/toolbar-icons/poly.png differ diff --git a/src/assets/toolbar-icons/rect.png b/src/assets/toolbar-icons/rect.png new file mode 100644 index 0000000..bc8308b Binary files /dev/null and b/src/assets/toolbar-icons/rect.png differ diff --git a/src/assets/toolbar-icons/tri.png b/src/assets/toolbar-icons/tri.png new file mode 100644 index 0000000..0b4ac03 Binary files /dev/null and b/src/assets/toolbar-icons/tri.png differ diff --git a/src/assets/toolbar-icons/wand-contig.png b/src/assets/toolbar-icons/wand-contig.png new file mode 100644 index 0000000..a1da5c7 Binary files /dev/null and b/src/assets/toolbar-icons/wand-contig.png differ diff --git a/src/assets/toolbar-icons/wand-global.png b/src/assets/toolbar-icons/wand-global.png new file mode 100644 index 0000000..4a917b5 Binary files /dev/null and b/src/assets/toolbar-icons/wand-global.png differ diff --git a/src/assets/toolbar-icons/zoom.png b/src/assets/toolbar-icons/zoom.png new file mode 100644 index 0000000..2793d3c Binary files /dev/null and b/src/assets/toolbar-icons/zoom.png differ diff --git a/src/brushes/3_rake.png b/src/brushes/3_rake.png new file mode 100644 index 0000000..3ab6138 Binary files /dev/null and b/src/brushes/3_rake.png differ diff --git a/src/brushes/abominable_snowman.png b/src/brushes/abominable_snowman.png new file mode 100644 index 0000000..359dc30 Binary files /dev/null and b/src/brushes/abominable_snowman.png differ diff --git a/src/brushes/bristle.png b/src/brushes/bristle.png new file mode 100644 index 0000000..a29cf2a Binary files /dev/null and b/src/brushes/bristle.png differ diff --git a/src/brushes/bristles_circle_medium.png b/src/brushes/bristles_circle_medium.png new file mode 100644 index 0000000..87f7bd3 Binary files /dev/null and b/src/brushes/bristles_circle_medium.png differ diff --git a/src/brushes/bristles_compact_mini.png b/src/brushes/bristles_compact_mini.png new file mode 100644 index 0000000..2281f6f Binary files /dev/null and b/src/brushes/bristles_compact_mini.png differ diff --git a/src/brushes/bristles_grouped.png b/src/brushes/bristles_grouped.png new file mode 100644 index 0000000..61be7f2 Binary files /dev/null and b/src/brushes/bristles_grouped.png differ diff --git a/src/brushes/chalk.png b/src/brushes/chalk.png new file mode 100644 index 0000000..7ece9e1 Binary files /dev/null and b/src/brushes/chalk.png differ diff --git a/src/brushes/chalk_chisel_losange_202210A.png b/src/brushes/chalk_chisel_losange_202210A.png new file mode 100644 index 0000000..5edf747 Binary files /dev/null and b/src/brushes/chalk_chisel_losange_202210A.png differ diff --git a/src/brushes/chalk_chisel_random.png b/src/brushes/chalk_chisel_random.png new file mode 100644 index 0000000..b17e901 Binary files /dev/null and b/src/brushes/chalk_chisel_random.png differ diff --git a/src/brushes/chalk_chisel_random_small.png b/src/brushes/chalk_chisel_random_small.png new file mode 100644 index 0000000..a6d6f37 Binary files /dev/null and b/src/brushes/chalk_chisel_random_small.png differ diff --git a/src/brushes/chalk_round_hard.png b/src/brushes/chalk_round_hard.png new file mode 100644 index 0000000..5e2d743 Binary files /dev/null and b/src/brushes/chalk_round_hard.png differ diff --git a/src/brushes/chalk_sparse.png b/src/brushes/chalk_sparse.png new file mode 100644 index 0000000..3331a0d Binary files /dev/null and b/src/brushes/chalk_sparse.png differ diff --git a/src/brushes/chisel_streaks.png b/src/brushes/chisel_streaks.png new file mode 100644 index 0000000..f322374 Binary files /dev/null and b/src/brushes/chisel_streaks.png differ diff --git a/src/brushes/deevad-202210C_compact-fix.png b/src/brushes/deevad-202210C_compact-fix.png new file mode 100644 index 0000000..dbd7eb4 Binary files /dev/null and b/src/brushes/deevad-202210C_compact-fix.png differ diff --git a/src/brushes/deevad-painterly-brush-tip_2023C.png b/src/brushes/deevad-painterly-brush-tip_2023C.png new file mode 100644 index 0000000..a5b8f9b Binary files /dev/null and b/src/brushes/deevad-painterly-brush-tip_2023C.png differ diff --git a/src/brushes/flat-tip-dirty.png b/src/brushes/flat-tip-dirty.png new file mode 100644 index 0000000..869e7fe Binary files /dev/null and b/src/brushes/flat-tip-dirty.png differ diff --git a/src/brushes/impressionism_brush.png b/src/brushes/impressionism_brush.png new file mode 100644 index 0000000..64e6b7e Binary files /dev/null and b/src/brushes/impressionism_brush.png differ diff --git a/src/brushes/random-debris.png b/src/brushes/random-debris.png new file mode 100644 index 0000000..b47c137 Binary files /dev/null and b/src/brushes/random-debris.png differ diff --git a/src/brushes/rock_pitted-fixed-B.png b/src/brushes/rock_pitted-fixed-B.png new file mode 100644 index 0000000..756e547 Binary files /dev/null and b/src/brushes/rock_pitted-fixed-B.png differ diff --git a/src/brushes/rock_pitted-fixed.png b/src/brushes/rock_pitted-fixed.png new file mode 100644 index 0000000..cbd190f Binary files /dev/null and b/src/brushes/rock_pitted-fixed.png differ diff --git a/src/brushes/scratches_rough.png b/src/brushes/scratches_rough.png new file mode 100644 index 0000000..21e0aac Binary files /dev/null and b/src/brushes/scratches_rough.png differ diff --git a/src/brushes/scribbles.png b/src/brushes/scribbles.png new file mode 100644 index 0000000..5b9720f Binary files /dev/null and b/src/brushes/scribbles.png differ diff --git a/src/brushes/splat_dots.png b/src/brushes/splat_dots.png new file mode 100644 index 0000000..85c85f0 Binary files /dev/null and b/src/brushes/splat_dots.png differ diff --git a/src/brushes/splats_large.png b/src/brushes/splats_large.png new file mode 100644 index 0000000..b8548bf Binary files /dev/null and b/src/brushes/splats_large.png differ diff --git a/src/brushes/square_rough_lightgrey.png b/src/brushes/square_rough_lightgrey.png new file mode 100644 index 0000000..8a92bf3 Binary files /dev/null and b/src/brushes/square_rough_lightgrey.png differ diff --git a/src/brushes/vegetal_stylised.png b/src/brushes/vegetal_stylised.png new file mode 100644 index 0000000..42c7263 Binary files /dev/null and b/src/brushes/vegetal_stylised.png differ diff --git a/src/css/styles.css b/src/css/styles.css new file mode 100644 index 0000000..c4d220f --- /dev/null +++ b/src/css/styles.css @@ -0,0 +1,5829 @@ +/* --- CSS STYLES --- */ + /* Startup loading bar — visible immediately, faded out by JS */ + #startup-loader { + position: fixed; top: 0; left: 0; width: 100%; height: 3px; z-index: 99999; + pointer-events: none; + } + #startup-loader .loader-bar { + height: 100%; width: 0%; + background: linear-gradient(90deg, #0078d7, #60cdff); + animation: loader-fill 1.8s ease-in-out forwards; + } + @keyframes loader-fill { + 0% { width: 0%; } + 30% { width: 35%; } + 60% { width: 65%; } + 100% { width: 88%; } + } + body.app-ready #startup-loader { + opacity: 0; + transition: opacity 0.25s ease; + } + /* CSS custom properties for the entire app. Centralised here so themes only need to override this block. */ + :root { + --bg-ribbon: #f5f6f7; + --border-ribbon: #dadbdc; + --win-blue: #0078d7; + --app-text-color: #111111; + --app-bg: #ffffff; + --titlebar-bg: #ffffff; + --titlebar-action-icon: #8a8f98; + --titlebar-save-icon: #7b3ff2; + --titlebar-save-pixel-primary: #9c75ca; + --titlebar-save-pixel-shadow: #81679b; + --titlebar-save-pixel-sheet: #e6eaec; + --titlebar-save-pixel-sheet-shadow: #cccad5; + --titlebar-save-pixel-white: #ffffff; + --titlebar-save-pixel-accent: #835ac7; + --titlebar-save-pixel-accent-soft: #a191b3; + --titlebar-save-pixel-mid: #98999c; + --titlebar-undo-redo-enabled: #1b6fe5; + --titlebar-action-outline-hover: #9fc4ea; + --titlebar-action-hover-bg: #e5e5e5; + --titlebar-separator: #c8c8c8; + --titlebar-filename: #111111; + --titlebar-control-icon: #111111; + --titlebar-control-close-hover: #e81123; + --titlebar-control-close-hover-icon: #ffffff; + --tab-row-bg: #ffffff; + --tab-file-text: #ffffff; + --section-divider: #e0e0e0; + --section-title-color: #666666; + --btn-hover-bg: #eaf4ff; + --btn-hover-border: #9fc4ea; + --btn-hover-shadow: rgba(0,120,215,0.15); + --btn-active-bg: #cce8ff; + --btn-active-border: #66a7e8; + --btn-active-shadow: rgba(0,120,215,0.3); + --dropdown-bg: #ffffff; + --dropdown-border: #a0a0a0; + --dropdown-shadow: rgba(0,0,0,0.2); + --dropdown-item-hover-bg: #e8e8e8; + --dropdown-arrow-color: #666666; + --dropdown-separator: #e0e0e0; + --btn-small-bg: #f9f9f9; + --btn-small-border: #cccccc; + --threshold-label-color: #666666; + --threshold-values-color: #444444; + --toggle-icon-color: #0078d7; + --toggle-icon-color-dark: #ffffff; + --toggle-status-color: #666666; + --hotkeys-icon-color: #0078d7; + --hotkeys-icon-color-dark: #ffffff; + --anchor-toggle-icon-color: #0078d7; + --anchor-toggle-icon-color-dark: #ffffff; + --theme-mode-toggle-icon-color: #0078d7; + --theme-mode-toggle-icon-color-dark: #ffffff; + --select-menu-header-color: #5f5f5f; + --select-menu-header-bg: #f2f2f2; + --select-menu-icon-all-fill: #e6f0fa; + --select-menu-icon-all-fill-disabled: #f0f0f0; + --select-menu-icon-disabled-stroke: #a0a0a0; + --select-menu-icon-delete-stroke: #c00000; + --theme-colors-window-bg: #ffffff; + --theme-colors-titlebar-bg-start: #fbfbfb; + --theme-colors-titlebar-bg-end: #f1f1f1; + --theme-colors-titlebar-border: #d8d8d8; + --theme-colors-top-note-color: #5a5a5a; + --theme-colors-result-count-color: #666666; + --theme-colors-list-bg: #ffffff; + --theme-colors-editor-panel-bg-start: #fcfcfc; + --theme-colors-editor-panel-bg-end: #f5f5f5; + --theme-colors-editor-panel-border: #d3d3d3; + --theme-colors-editor-title-color: #444444; + --theme-colors-editor-key-color: #333333; + --theme-colors-editor-input-bg: #ffffff; + --theme-colors-editor-input-border: #bdbdbd; + --theme-colors-editor-swatch-border: #777777; + --theme-colors-editor-hint-color: #666666; + --theme-colors-mini-toggle-color: #444444; + --palette-mini-swatch-border: #a0a0a0; + --palette-mini-swatch-inner: #ffffff; + --palette-mini-swatch-hover-border: #0078d7; + --palette-mini-swatch-hover-inner: #bcdcff; + --view-center-icon-color: #0078d7; + --view-edgeclean-icon-color: #007bff; + --view-theme-icon-color: #0078d7; + --select-icon-free-color: #1a6aab; + --select-icon-poly-main-color: #1a6aab; + --select-icon-poly-accent-color: #4e8aba; + --bg-hover: #cce8ff; + --border-hover: #99d1ff; + --bg-selected: #e1f0ff; + --border-selected: #7da2ce; + --bg-canvas: #cce3f3; + --shape-icon-color: #000000; + --zoom: 1; + --zoom-inv: 1; + --sel-rotate-icon-color: #ffffff; + } + + /* Global box-model reset and font defaults. Disabling user-select everywhere prevents text highlighting during canvas drag operations. */ + * { box-sizing: border-box; user-select: none; } + + body { + margin: 0; + height: 100vh; + display: flex; + flex-direction: column; + font-family: 'Segoe UI', sans-serif; + font-size: 12px; + overflow: hidden; + background: var(--app-bg); + color: var(--app-text-color); + } + + input { user-select: auto !important; cursor: pointer; } + input[type="checkbox"] { accent-color: var(--win-blue); } + + /* LAYOUT */ + /* Layout for the title bar, tab row, and ribbon. Heights are fixed so the canvas area can fill the remaining viewport precisely. */ + + #title-bar { height: 21px; display: flex; align-items: center; padding: 0 10px 0 10px; background: var(--titlebar-bg); z-index: 101; } + #title-bar .title-left { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; } + #title-bar .app-icon { width: 16px; height: 16px; image-rendering: pixelated; } + #title-bar .title-action { + width: 28px; + height: 24px; + border: 0; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + cursor: pointer; + } + #title-bar .title-action svg { + width: 13px; + height: 13px; + fill: var(--titlebar-action-icon); + stroke: none; + } + #title-bar .title-action img { + width: 13px; + height: 13px; + min-width: 13px; + min-height: 13px; + max-width: 13px; + max-height: 13px; + display: block; + image-rendering: pixelated; + } + #title-bar .title-action.icon-undo img, + #title-bar .title-action.icon-redo img { + width: 16px; + height: 16px; + min-width: 16px; + min-height: 16px; + max-width: 16px; + max-height: 16px; + } + #title-bar .title-action.icon-save svg { + width: 14px; + height: 14px; + fill: var(--titlebar-save-icon); + stroke: var(--titlebar-save-icon); + stroke-width: 0; + } + #title-bar .title-action.icon-save svg rect, + #title-bar .title-action.icon-save svg circle { + fill: rgba(255,255,255,0.35); + } + #title-bar .title-action.icon-undo.is-enabled svg, + #title-bar .title-action.icon-redo.is-enabled svg { + fill: var(--titlebar-undo-redo-enabled); + } + #title-bar .title-action.icon-undo { + margin-left: -9px; + margin-right: -9px; + } + #title-bar .title-action.icon-save, + #title-bar .title-action.icon-undo, + #title-bar .title-action.icon-redo { + border: 1px solid transparent; + border-radius: 2px; + box-sizing: border-box; + position: relative; + } + #title-bar .title-action.icon-save::before, + #title-bar .title-action.icon-undo::before, + #title-bar .title-action.icon-redo::before { + content: ""; + position: absolute; + width: 20px; + height: 20px; + box-sizing: border-box; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 1px solid transparent; + background: transparent; + box-shadow: none; + border-radius: 0; + pointer-events: none; + } + #title-bar .title-action.icon-save:hover, + #title-bar .title-action.icon-undo:hover, + #title-bar .title-action.icon-redo:hover { + background: transparent; + border-color: transparent; + box-shadow: none; + } + #title-bar .title-action.icon-save:hover::before, + #title-bar .title-action.icon-undo:hover::before, + #title-bar .title-action.icon-redo:hover::before { + background: transparent; + border-color: var(--titlebar-action-outline-hover); + box-shadow: none; + } + #title-bar .title-action.icon-undo:disabled:hover::before, + #title-bar .title-action.icon-redo:disabled:hover::before { + border-color: transparent; + box-shadow: none; + background: transparent; + } + #title-bar .title-action:disabled { + opacity: 0.5; + cursor: default; + } + /* Undo/Redo provide dedicated grey icons, so don't double-dim via opacity. */ + #title-bar .title-action.icon-undo:disabled, + #title-bar .title-action.icon-redo:disabled { + opacity: 1; + } + #title-bar .title-action:hover { background: var(--titlebar-action-hover-bg); } + #title-bar .title-sep { + width: 1px; + height: 18px; + background: var(--titlebar-separator); + margin: 0 2px; + } + #title-bar .title-filename { + font-weight: 600; + color: var(--titlebar-filename); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 240px; + } + #title-bar .title-controls { display: flex; align-items: stretch; height: 100%; margin-right: -10px; } + #title-bar .title-btn { + width: 46px; + border: 0; + background: transparent; + cursor: pointer; + position: relative; + } + #title-bar .title-btn::before { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 14px; + line-height: 1; + color: var(--titlebar-control-icon); + content: ""; + } + #title-bar #title-minimize::before { content: "_"; top: 27%; } + #title-bar #title-maximize { + position: relative; + overflow: visible; + } + #title-bar #title-maximize::before { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 10px; + border: 1.5px solid var(--titlebar-control-icon); + box-sizing: border-box; + } + #title-bar #title-maximize::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 10px; + box-sizing: border-box; + pointer-events: none; + } + #title-bar #title-maximize.is-maximized::before { + background: none; + border: 0; + width: 8px; + height: 8px; + transform: translate(calc(-50% + 3px), calc(-50% - 3px)); + border-top: 1.5px solid var(--titlebar-control-icon); + border-right: 1.5px solid var(--titlebar-control-icon); + } + #title-bar #title-maximize.is-maximized::after { + border: 1.5px solid var(--titlebar-control-icon); + width: 8px; + height: 8px; + } + #title-bar #title-close::before { content: "✕"; } + #title-bar .title-btn:hover { background: var(--titlebar-action-hover-bg); } + #title-bar .title-btn.close:hover { background: var(--titlebar-control-close-hover); } + #title-bar .title-btn.close:hover::before { color: var(--titlebar-control-close-hover-icon); } + .tab-row { display: flex; height: 24px; background: var(--tab-row-bg); border-bottom: 1px solid var(--border-ribbon); z-index: 101; } + .tab { padding: 4px 20px; cursor: pointer; border: 1px solid transparent; margin: 0 2px; position: relative; top: 1px; } + .tab:not(.file):not(.active):hover { background: var(--btn-hover-bg); border-color: var(--btn-hover-border); } + .tab.active { background: var(--bg-ribbon); border: 1px solid var(--border-ribbon); border-bottom: 1px solid var(--bg-ribbon); z-index: 102; } + .tab.file { background: var(--win-blue); color: var(--tab-file-text); } + + + .ribbon { height: 100px; min-height: 100px; max-height: 100px; background: var(--bg-ribbon); border-bottom: 1px solid var(--border-ribbon); display: flex; padding: 5px; gap: 5px; position: relative; z-index: 100; overflow: hidden; } + + + + .section { display: flex; flex-direction: column; align-items: center; justify-content: space-between; border-right: 1px solid var(--section-divider); padding: 0 8px; height: 100%; min-width: max-content; flex-shrink: 0; } + .colors-section { align-items: flex-start; } + .ribbon-draggable { transition: transform 160ms ease; } + .ribbon-drag-handle { cursor: grab; } + .ribbon-dragging { opacity: 0.6; cursor: grabbing; } + .section-title { color: var(--section-title-color); font-size: 11px; margin-bottom: 2px; text-align: center; width: 100%; } + .section-title { -webkit-user-drag: element; } + .section-hidden { display: none !important; } + .tools-grid { display: grid; gap: 2px; } + .shapes-grid { display: grid; gap: 2px; } + .tool-grid-slot { width: 24px; height: 24px; display:flex; align-items:center; justify-content:center; } + .tool-slot-empty { pointer-events: none; } + /* Tool customizer */ + .customizer-tab { font-size:11px; cursor:pointer; } + .customizer-tab.active { background:var(--btn-active-bg); border-color:var(--btn-active-border); font-weight:bold; } + .tool-drawer { display:flex; flex-wrap:wrap; gap:2px; padding:4px; background:var(--bg-panel); border:1px solid var(--border-color); border-radius:3px; min-height:40px; align-content:flex-start; } + .customizer-drawer-item { width:28px; height:28px; display:flex; align-items:center; justify-content:center; cursor:grab; border:1px solid transparent; border-radius:2px; } + .customizer-drawer-item:hover { background:var(--btn-hover-bg); border-color:var(--btn-hover-border); } + .tool-grid-preview { display:grid; grid-template-columns:repeat(20,28px); gap:2px; padding:4px; background:var(--bg-panel); border:1px solid var(--border-color); border-radius:3px; min-height:94px; } + .customizer-slot { width:28px; height:28px; display:flex; align-items:center; justify-content:center; border:1px solid var(--border-color); border-radius:2px; cursor:grab; } + .customizer-slot-empty { background:rgba(128,128,128,0.08); border-style:dashed !important; } + .customizer-slot-filled { background:var(--bg-panel); } + .customizer-slot-filled:hover { background:var(--btn-hover-bg); border-color:var(--btn-hover-border); } + .customizer-over { border-color:var(--accent-color) !important; background:rgba(0,120,215,0.1) !important; } + .customizer-dragging { opacity:0.4; } + .customizer-tab-row { display:flex; gap:0; border-bottom:1px solid var(--border-color); } + .customizer-tab-row .customizer-tab { flex:1; border-radius:0; padding:4px 8px; } + .customizer-body { display:flex; gap:12px; } + .customizer-drawer-col { width:130px; flex-shrink:0; } + .customizer-grid-col { flex:1; min-width:0; } + .customizer-label { font-size:11px; color:var(--section-title-color); margin-bottom:6px; } + .customizer-hint { font-size:10px; color:var(--dim-text); margin-top:6px; } + + /* BUTTONS */ + .btn { border: 1px solid transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; } + .btn-text { justify-content: flex-start; gap: 4px; width: 72px; padding: 2px 4px; text-align: left; } + .btn-text .btn-label { flex: 1; text-align: left; } + .btn-text .btn-caret { margin-left: auto; } + .btn:hover, .btn-large:hover, .split-btn-bottom:hover { background: var(--btn-hover-bg); border-color: var(--btn-hover-border); box-shadow: inset 0 0 2px var(--btn-hover-shadow); } + /* Theme selection buttons: active state, no grey backdrop */ + .btn-large.theme-mode-btn { background: transparent; } + .btn-large.theme-mode-btn.is-on { + background: var(--btn-active-bg); + border-color: var(--btn-active-border); + box-shadow: inset 0 0 3px var(--btn-active-shadow); + } + .btn-large.theme-mode-btn:hover:not(.is-on) { background: var(--btn-hover-bg); border-color: var(--btn-hover-border); box-shadow: inset 0 0 2px var(--btn-hover-shadow); } + .btn-large.edit-colors-btn { position: relative; } + .btn-large.edit-colors-btn { width: 48px; height: 66px; padding: 4px; } + .btn-large.edit-colors-btn::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 10px; + bottom: 1px; + background: transparent; + border-radius: 2px; + pointer-events: none; + z-index: -1; + } + .btn-large.edit-colors-btn { z-index: 0; } + .btn-large.edit-colors-btn:hover::before { + background: var(--btn-hover-bg); + box-shadow: inset 0 0 2px var(--btn-hover-shadow); + } + + .swap-colors-btn { position: relative; z-index: 1; pointer-events: auto; } + + .btn.active, .btn-large.active, .split-btn-container.active { + background: var(--btn-active-bg); + border-color: var(--btn-active-border); + box-shadow: inset 0 0 3px var(--btn-active-shadow); + } + + /* Resize and Skew modal (Win32 style) */ + /* Per-element position fine-tuning for the resize/skew modal. These are pixel nudges to align Win32-style form controls. */ + #modal-resize .window { + background-color: #f0f0f0; + width: 264px; + height: 396px; + box-shadow: 0 0 15px rgba(0,0,0,0.4); + display: flex; + flex-direction: column; + border: 1px solid #999; + } + #modal-resize .title-bar { + background-color: white; + height: 28px; + display: flex; + align-items: center; + justify-content: flex-start; + padding-left: 8px; + font-size: 11px; + } + #modal-resize .close-btn { + width: 40px; + height: 28px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + font-size: 13px; + color: #111; + margin-left: auto; + } + #modal-resize .close-btn:hover { + background-color: #e81123; + color: white; + } + #modal-resize .content { padding: 6px 8px 0px 8px; flex: 1; display: flex; flex-direction: column; gap: 6px; } + #modal-resize fieldset { + border: 1px solid #dfdfdf; + margin: 0; + padding: 12px 8px 16px 8px; + margin-bottom: 6px; + border-radius: 2px; + flex: 1; + } + #modal-resize legend { + font-size: 11px; + color: #000; + padding: 0 4px; + margin-left: -5px; + } + #modal-resize .row { display: flex; align-items: center; margin-bottom: 10px; } + #modal-resize .row:last-child { margin-bottom: 0; } + /* Skew uses manual nudge hacks in the floating window. */ + #modal-resize .sk-h-row { margin-top: -80px; } + #modal-resize .sk-v-row { margin-top: 20px; } + #modal-resize .sk-h-row, + #modal-resize .sk-v-row { margin-left: 13px; } + /* Resize fieldset: icon in column 1, fields in column 2. */ + #modal-resize .rz-fieldset { + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 10px; + row-gap: -6px; + align-items: center; + overflow: visible; + flex: 0 0 auto; + padding-bottom: 0; + } + #modal-resize .rz-fieldset > .rz-icon-row { + grid-column: 1; + grid-row: 1 / -1; + align-self: center; + justify-self: center; + } + #modal-resize .rz-fieldset > .row:not(.rz-icon-row), + #modal-resize .rz-fieldset > .checkbox-row { + grid-column: 2; + } + #modal-resize .rz-dual { display: flex; align-items: center; gap: 2px; } + #modal-resize .rz-h-dual { transform: translateY(-9px); } + #modal-resize .rz-h-dual { gap: 8px; } + #modal-resize .rz-h-dual .rz-pair { display: flex; flex-direction: column; align-items: center; } + #modal-resize .rz-h-dual .rz-unit { margin: 0 0 1px 0; } + #modal-resize .rz-dual input.rz-num { width: 47px; } + #modal-resize .rz-unit { font-size: 11px; color: #000; margin: 0 4px 0 1px; } + /* Floating-window only: nudge the resize icon down. */ + #modal-resize .resize-icon-img { transform: translateY(26px); } + #modal-resize .rz-h-row { transform: translateY(6px); } + #modal-resize .rz-v-row { transform: translateY(-6px); } + #modal-resize .rz-ratio-row { transform: translate(-80px, -20px); } + #modal-resize #rz-v-px { transform: translateX(6px); } + #modal-resize .col-icon { width: 48px; display: flex; justify-content: center; align-items: center; padding-top: 1px; } + #modal-resize .col-icon svg { width: auto; height: auto; display: block; } + #modal-resize .icon-stack-h { display: flex; flex-direction: column; align-items: center; gap: 2px; } + #modal-resize .icon-stack-v { display: flex; flex-direction: row; align-items: center; gap: 2px; } + #modal-resize .resize-icon-img { width: auto; height: auto; display: block; image-rendering: pixelated; } + #modal-resize .skew-icon-img { width: auto; height: auto; display: block; image-rendering: pixelated; } + #modal-resize .rz-hidden { display: none; } + #modal-resize .col-label { width: 62px; color: #000; font-size: 11px; } + #modal-resize .col-input { flex-grow: 1; } + #modal-resize .radio-container { display: flex; gap: 8px; } + #modal-resize .radio-label { display: flex; align-items: center; gap: 4px; cursor: default; font-size: 11px; } + #modal-resize input[type="radio"] { margin: 0; width: 14px; height: 14px; accent-color: var(--toggle-icon-color); transform: translateY(-1px); } + #modal-resize input[type="text"] { + width: 64px; + height: 20px; + box-sizing: border-box; + padding: 0 6px; + border: none; + border-bottom: 1px solid #7a7a7a; + background: #ffffff; + font-family: inherit; + font-size: 11px; + outline: none; + user-select: text; + } + #modal-resize input[type="text"]:hover { border-bottom-color: #3c3c3c; } + #modal-resize input[type="text"]:focus { border-bottom-color: var(--toggle-icon-color); } + #modal-resize .checkbox-row { margin-top: 14px; display: flex; align-items: center; } + #modal-resize .checkbox-indent { margin-left: 30px; display: flex; align-items: center; gap: 6px; font-size: 11px; } + #modal-resize input[type="checkbox"] { margin: 0; width: 12px; height: 12px; accent-color: var(--toggle-icon-color); border: 1px solid #777; } + #modal-resize .footer { + background-color: #f0f0f0; + padding: 8px 10px 10px 10px; + display: flex; + justify-content: flex-end; + gap: 6px; + } + #modal-resize .btn { + width: 64px; + height: 22px; + background-color: #ffffff; + border: 1px solid #adadad; + font-family: inherit; + font-size: 11px; + cursor: pointer; + border-radius: 2px; + outline: none; + } + #modal-resize .btn:hover { background-color: #e5f1fb; border-color: #0078d7; } + #modal-resize .btn:active { background-color: #cce4f7; border-color: #005499; } + #modal-resize .btn:focus-visible { box-shadow: 0 0 0 2px #0078d7; } + #modal-resize .btn-ok { background-color: #ffffff; border: 2px solid #0078d7; } + #modal-resize .btn-ok:hover { background-color: #e5f1fb; } + #modal-resize .btn-ok:active { background-color: #cce4f7; border-color: #005499; } + #modal-resize svg { overflow: visible; } + #modal-resize .shape-stroke { fill: none; stroke: black; stroke-width: 1px; shape-rendering: crispEdges; } + #modal-resize .red-arrow { fill: #ed1c24; } + #modal-props .window { + width: 620px; + max-width: calc(100vw - 34px); + max-height: calc(100vh - 48px); + background-color: #f2f2f2; + border: 1px solid #a8a8a8; + box-shadow: 0 8px 22px rgba(0,0,0,0.22); + position: absolute; + display: flex; + flex-direction: column; + font-family: "Segoe UI", "MS Sans Serif", sans-serif; + font-size: 12px; + } + #modal-props .title-bar { + height: 28px; + background: #ffffff; + border-bottom: 1px solid #d0d0d0; + display: flex; + align-items: center; + padding: 0 10px; + font-size: 12px; + color: #111; + } + #modal-props .close-btn { + margin-left: auto; + width: 32px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + } + #modal-props .close-btn:hover { background: #e81123; color: #fff; } + #modal-props .content { + padding: 10px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + } + #modal-props .props-edit-panel { + background: #ffffff; + border: 1px solid #d0d0d0; + padding: 10px; + display: flex; + flex-direction: column; + gap: 8px; + } + #modal-props .props-edit-grid { + display: grid; + grid-template-columns: 100px 140px 100px 140px; + gap: 8px; + align-items: center; + } + #modal-props .props-edit-grid label { color: #333; font-size: 12px; } + #modal-props .props-edit-grid input { + height: 24px; + border: 1px solid #bcbcbc; + padding: 0 6px; + font-size: 12px; + background: #fff; + } + #modal-props .props-note { font-size: 11px; color: #616161; } + #modal-props .props-info-grid { + display: grid; + grid-template-columns: 1fr; + gap: 8px; + } + #modal-props .props-card { + background: #ffffff; + border: 1px solid #d0d0d0; + padding: 8px 10px; + min-height: 0; + } + #modal-props .props-card h4 { + margin: 0 0 6px 0; + font-size: 12px; + color: #2f2f2f; + font-weight: 600; + } + #modal-props .props-kv { + display: grid; + grid-template-columns: 170px 1fr; + gap: 4px 8px; + align-items: baseline; + } + #modal-props .props-k { color: #5e5e5e; white-space: nowrap; } + #modal-props .props-v { + color: #1f1f1f; + font-family: Consolas, "Courier New", monospace; + overflow-wrap: anywhere; + } + #modal-props .footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 0 10px 10px 10px; + } + #modal-props .footer .btn { + height: 24px; + padding: 0 14px; + border: 1px solid #adadad; + background-color: #e1e1e1; + font-family: inherit; + font-size: 12px; + cursor: pointer; + border-radius: 2px; + } + #modal-props .footer .btn:hover { background-color: #e5f1fb; border-color: var(--toggle-icon-color); } + #modal-props .footer .btn-ok { background: #fff; border: 1px solid #0078d7; box-shadow: inset 0 0 0 1px #0078d7; } + .btn * { pointer-events: none; } + + .btn-large { display: flex; flex-direction: column; align-items: center; min-width: 44px; padding: 4px; height: 100%; justify-content: center; border: 1px solid transparent; cursor: pointer; position: relative; background: transparent; } + .btn-large .ribbon-icon { width: auto; height: auto; max-width: none; max-height: none; display: block; image-rendering: pixelated; image-rendering: crisp-edges; } + .select-btn-wrap:hover { background: transparent; border-color: transparent; box-shadow: none; } + + .split-btn-container { display: flex; flex-direction: column; height: 100%; border: 1px solid transparent; } + .split-btn-top { flex: 1; display: flex; align-items: center; justify-content: center; cursor: pointer; } + + .split-btn-bottom { height: 18px; display: flex; align-items: center; justify-content: center; font-size: 10px; cursor: pointer; gap: 2px; } + .split-btn-container.select-split { + border: 1px solid transparent; + border-radius: 2px; + overflow: hidden; + } + .split-btn-container.select-split.selection-highlight { + border-color: #66a7e8; + } + .split-btn-container.select-split.selection-highlight .split-btn-top, + .split-btn-container.select-split.selection-highlight .split-btn-bottom.select-dropdown { + background: #cce8ff; + } + .split-btn-container.select-split .split-btn-bottom.select-dropdown { + margin: 0; + border: 0; + border-top: 1px solid transparent; + border-radius: 0; + background: transparent; + height: 24px; + flex-direction: column; + line-height: 1; + } + .split-btn-container.select-split .split-btn-bottom.select-dropdown.selection-highlight { + border-top-color: #66a7e8; + } + .split-btn-container.select-split .split-btn-bottom.select-dropdown span:first-child { + font-size: 11px; + } + .split-btn-container.select-split .split-btn-bottom.select-dropdown .caret { + display: block; + margin-top: -3px; + line-height: 1; + } + .rotate-caret { + display: inline-flex; + margin-top: -1px; + } + .split-btn-container.select-split:hover { + border-color: #9fc4ea; + background: var(--btn-hover-bg); + box-shadow: inset 0 0 2px var(--btn-hover-shadow); + } + .split-btn-container.select-split:hover .split-btn-bottom.select-dropdown { + border-top-color: #9fc4ea; + background: var(--btn-hover-bg); + box-shadow: inset 0 0 2px var(--btn-hover-shadow); + } + .btn-icon { width: 24px; height: 24px; } + .btn-icon img.toolbar-icon { + width: 24px; + height: 24px; + display: block; + margin: 0; + image-rendering: pixelated; + image-rendering: crisp-edges; + } + /* Allow icons with non-24x24 native sizes (ex: Triangle 18x26) to render without clipping. */ + [data-tool="tri"] img.toolbar-icon, + [data-tool-id="tri"] img.toolbar-icon { + width: 24px; + height: 26px; + transform: translateY(0px); + } + [data-tool="gradient"] img.toolbar-icon, + [data-tool-id="gradient"] img.toolbar-icon { + width: 15px; + height: 17px; + } + [data-tool="freehand"] img.toolbar-icon, + [data-tool-id="freehand"] img.toolbar-icon { + width: 20px; + height: 14px; + } + [data-tool="paintbrush"] img.toolbar-icon, + [data-tool-id="paintbrush"] img.toolbar-icon { + width: 18px; + height: 17px; + } + #anchor-rotate-toggle-btn img.toolbar-icon, + [data-tool-id="anchor-toggle"] img.toolbar-icon, + [data-tool-id="layers-toggle"] img.toolbar-icon, + [data-tool-id="pokeproject"] img.toolbar-icon { + width: auto; + height: auto; + max-width: 24px; + max-height: 24px; + } + /* Per-icon pixel nudges for visual alignment. */ + [data-tool="eraser"] img.toolbar-icon, + [data-tool-id="eraser"] img.toolbar-icon { transform: translateY(1px); } + [data-tool="picker"] img.toolbar-icon, + [data-tool-id="picker"] img.toolbar-icon { transform: translate(1px, 1px); } + #wand-tool-btn .wand-icon-contig img.toolbar-icon, + [data-tool-id="wand"] .wand-icon-contig img.toolbar-icon { transform: translateX(2px); } + .btn-icon .pencil-icon { + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + } + .btn-icon .pencil-icon img.toolbar-icon { + margin: 0; + } + .select-icon { display: none; } + .select-icon.show { display: block; } + img.select-icon { + image-rendering: pixelated; + image-rendering: crisp-edges; + } + + /* MENUS */ + + .dropdown-menu { position: absolute; background: var(--dropdown-bg); border: 1px solid var(--dropdown-border); box-shadow: 2px 2px 5px var(--dropdown-shadow); display: none; flex-direction: column; min-width: 150px; padding: 2px; z-index: 10001; } + + .dd-item { padding: 5px 5px 5px 5px; cursor: pointer; display: flex; align-items: center; gap: 8px; position: relative; } + .dd-item:hover { background: var(--dropdown-item-hover-bg); } + .pixel-perfect { + image-rendering: pixelated; + image-rendering: crisp-edges; + -ms-interpolation-mode: nearest-neighbor; + } + .dd-icon { margin-right: 3px; } + .submenu { position: absolute; left: 100%; top: -1px; display: none; flex-direction: column; background: var(--dropdown-bg); border: 1px solid var(--dropdown-border); box-shadow: 2px 2px 5px var(--dropdown-shadow); min-width: 150px; padding: 2px; z-index: 10002; } + .dd-item:hover > .submenu { display: flex; } + .dd-arrow { margin-left: auto; font-size: 10px; color: var(--dropdown-arrow-color); } + .dd-sep { height: 1px; background: var(--dropdown-separator); margin: 2px 0; } + .dd-check { width: 16px; visibility: hidden; } + .checked .dd-check { visibility: visible; } + + /* File menu template (left actions + right recent files) */ + #file-menu.file-menu-template { + position: absolute; + width: 337px; + min-width: 337px; + padding: 0; + border: 1px solid #b9c1d1; + box-shadow: 2px 2px 5px rgba(0,0,0,0.1); + overflow: visible; + flex-direction: row; + } + #file-menu.file-menu-template.recent-collapsed { + width: 216px; + min-width: 216px; + } + #file-menu .file-menu-left { + flex: 1 1 auto; + width: auto; + min-width: 0; + display: flex; + flex-direction: column; + border-right: 1px solid #e0e4ea; + background: #ffffff; + } + #file-menu .file-menu-item { + width: 100%; + max-width: 100%; + height: 44px; + display: flex; + align-items: center; + gap: 10px; + padding: 0 10px; + cursor: pointer; + color: #333333; + border: 1px solid transparent; + } + #file-menu .file-menu-item:hover { + background-color: #e5f3fb; + border-color: #d8eaf9; + } + #file-menu .file-menu-icon { + width: 32px; + height: 32px; + text-align: center; + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + transform: translateZ(0); + } + #file-menu .file-menu-icon img { + width: 32px; + height: 32px; + display: block; + image-rendering: -webkit-optimize-contrast; + } + #file-menu .file-menu-label { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + #file-menu .file-menu-separator { + height: 1px; + background-color: #e0e4ea; + margin: 2px 0; + } + #file-menu .file-menu-right { + flex: 0 0 121px; + width: 121px; + margin-left: auto; + background-color: #f5f6f7; + display: flex; + flex-direction: column; + min-width: 0; + } + #file-menu.file-menu-template.recent-collapsed .file-menu-right { + display: none; + } + #file-menu.file-menu-template.recent-collapsed .file-menu-left { + flex: 1 1 auto; + width: auto; + border-right: none; + } + #file-menu .file-menu-recent-toggle { + position: absolute; + right: -1px; + top: -1px; + bottom: auto; + width: 14px; + height: 16px; + border: 1px solid #c3c9d1; + border-right: none; + border-radius: 1px 0 0 1px; + background: #f4f5f7; + color: #5b6168; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + font-size: 9px; + line-height: 1; + z-index: 2; + } + #file-menu .file-menu-recent-toggle:hover { + background: #eceff3; + color: #39414a; + } + #file-menu.file-menu-template.recent-collapsed .file-menu-recent-toggle { + right: -13px; + top: -1px; + border-right: 1px solid #c3c9d1; + border-left: none; + border-radius: 0 1px 1px 0; + } + #file-menu .file-menu-recent-header { + padding: 8px 12px; + border-bottom: 1px solid #e0e4ea; + color: #555555; + font-weight: 600; + } + #file-menu .file-menu-recent-list { + list-style: none; + margin: 0; + padding: 0; + overflow: auto; + flex: 1; + min-height: 0; + } + #file-menu .file-menu-recent-item { + padding: 4px 12px; + cursor: default; + display: flex; + gap: 8px; + min-height: 24px; + align-items: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + #file-menu .file-menu-recent-item span:last-child { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + #file-menu .file-menu-recent-item:hover { + background-color: #e5f3fb; + } + #file-menu .file-menu-recent-item.is-empty { + font-size: 11px; + color: #6a6a6a; + } + #file-menu .file-menu-recent-number { + text-decoration: underline; + width: 12px; + flex-shrink: 0; + color: #2f2f2f; + } + body.dark-mode #file-menu.file-menu-template { + border-color: #4c5666; + box-shadow: 2px 2px 6px rgba(0,0,0,0.4); + } + body.dark-mode #file-menu .file-menu-left { + border-right-color: #465161; + background: #1f2731; + } + body.dark-mode #file-menu .file-menu-item { + color: #e6edf5; + } + body.dark-mode #file-menu .file-menu-item:hover { + background-color: #2a3c52; + border-color: #355474; + } + body.dark-mode #file-menu .file-menu-separator { + background-color: #465161; + } + body.dark-mode #file-menu .file-menu-right { + background-color: #25303b; + } + body.dark-mode #file-menu .file-menu-recent-toggle { + background: #2a3037; + border-color: #465161; + border-right: none; + color: #c9d2db; + } + body.dark-mode #file-menu .file-menu-recent-toggle:hover { + background: #353d47; + color: #eef4fb; + } + body.dark-mode #file-menu.file-menu-template.recent-collapsed .file-menu-recent-toggle { + border-right: 1px solid #465161; + border-left: none; + } + body.dark-mode #file-menu .file-menu-recent-header { + color: #d7e1ea; + border-bottom-color: #465161; + } + body.dark-mode #file-menu .file-menu-recent-item { + color: #d7e1ea; + } + body.dark-mode #file-menu .file-menu-recent-item:hover { + background-color: #2a3c52; + } + + .btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; width: 140px; max-height: 90px; overflow-y: auto; } + + .btn-small { border: 1px solid var(--btn-small-border); background: var(--btn-small-bg); padding: 2px 4px; font-size: 10px; text-align: center; cursor: pointer; } + .btn-small.active { background: var(--bg-selected); border-color: var(--win-blue); font-weight: bold; } + #btn-grid-modes { width: 100px; gap: 1px; max-height: none; overflow: visible; } + #btn-grid-modes .btn-small { width: 48px; height: 20px; line-height: 20px; padding: 0; display: flex; align-items: center; justify-content: center; } + + /* ══════════════════════════════════════════════════════════ + FREEHAND BRUSH + PAINT BRUSH SIDEBARS — Fluent-style flyout + ══════════════════════════════════════════════════════════ */ + + /* Shared panel: acrylic-tinted background, flat square edge, + Windows 10-style flyout — no rounding, no drop shadow. */ + #freehand-sidebar, #paintbrush-sidebar, #gradient-sidebar { + position: fixed; top: 145px; bottom: 24px; left: -296px; + width: 280px; z-index: 9988; + background: #f5f6f7; + border-right: 1px solid var(--border-ribbon, #dadbdc); + border-radius: 0; + display: flex; flex-direction: column; + transition: left .24s cubic-bezier(.16,1,.3,1); + pointer-events: auto; + } + #freehand-sidebar *, #paintbrush-sidebar *, #gradient-sidebar * { pointer-events: auto; } + #freehand-sidebar.open, #paintbrush-sidebar.open, #gradient-sidebar.open { left: 0; } + + #freehand-sidebar-header, #paintbrush-sidebar-header, #gradient-sidebar-header { + display: flex; align-items: center; justify-content: space-between; + padding: 6px 6px 6px 10px; + font-size: 12px; font-weight: 600; + color: var(--app-text-color); + border-bottom: 1px solid color-mix(in srgb, var(--section-divider, #e0e0e0) 80%, transparent); + flex-shrink: 0; + } + #freehand-sidebar-header > span, #paintbrush-sidebar-header > span, #gradient-sidebar-header > span { flex: 1; } + #freehand-collapse-btn, #paintbrush-collapse-btn, #gradient-collapse-btn { + background: none; border: none; cursor: pointer; font-size: 11px; + color: var(--section-title-color); padding: 0; line-height: 1; + width: 24px; height: 24px; border-radius: 2px; + display: flex; align-items: center; justify-content: center; + margin-left: auto; + transition: transform .15s cubic-bezier(.16,1,.3,1), background .12s ease; + } + #freehand-collapse-btn:hover, #paintbrush-collapse-btn:hover, #gradient-collapse-btn:hover { + background: color-mix(in srgb, var(--app-text-color, #111) 8%, transparent); + } + #freehand-collapse-btn:active, #paintbrush-collapse-btn:active, #gradient-collapse-btn:active { + background: color-mix(in srgb, var(--app-text-color, #111) 14%, transparent); + } + #freehand-collapse-btn.collapsed, #paintbrush-collapse-btn.collapsed, #gradient-collapse-btn.collapsed { transform: rotate(-90deg); } + + #freehand-sidebar-body, #paintbrush-sidebar-body, #gradient-sidebar-body { + flex: 1; overflow-y: auto; padding: 10px 12px 6px; + min-height: 0; + scrollbar-width: none; + } + #freehand-sidebar-body::-webkit-scrollbar, #paintbrush-sidebar-body::-webkit-scrollbar, #gradient-sidebar-body::-webkit-scrollbar { width: 0; display: none; } + #gradient-options { padding-top: 8px; } + + .fh-row { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--threshold-label-color); padding: 0; margin-bottom: 2px; } + .fh-row label { min-width: 80px; text-align: right; white-space: nowrap; } + .fh-row input[type="range"] { flex: 1; height: 20px; min-width: 40px; margin: 0; -webkit-appearance: none; appearance: none; background: linear-gradient(to right, var(--win-blue) 0%, var(--win-blue) var(--pct, 0%), color-mix(in srgb, var(--app-text-color, #111) 14%, transparent) var(--pct, 0%), color-mix(in srgb, var(--app-text-color, #111) 14%, transparent) 100%); border-radius: 10px; cursor: pointer; outline: none; transition: box-shadow .12s ease; } + .fh-row input[type="range"]:hover { box-shadow: 0 0 0 1px color-mix(in srgb, var(--win-blue) 35%, transparent); } + .fh-row input[type="range"]:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--win-blue) 55%, transparent); } + .fh-row input[type="range"]::-webkit-slider-runnable-track { background: transparent; height: 20px; border: none; } + .fh-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 0; height: 0; opacity: 0; } + .fh-row input[type="range"]::-moz-range-track { background: transparent; height: 20px; border: none; } + .fh-row input[type="range"]::-moz-range-progress { background: transparent; height: 20px; } + .fh-row input[type="range"]::-moz-range-thumb { width: 0; height: 0; opacity: 0; } + .fh-row select { + font-size: 10px; padding: 3px 6px; border: 1px solid color-mix(in srgb, var(--btn-small-border, #ccc) 80%, transparent); + border-radius: 2px; background: var(--dropdown-bg); color: var(--app-text-color); + transition: border-color .12s ease, background .12s ease; + } + .fh-row select:hover { border-color: color-mix(in srgb, var(--win-blue) 50%, var(--btn-small-border, #ccc)); } + .fh-row select:focus-visible { outline: 2px solid color-mix(in srgb, var(--win-blue) 55%, transparent); outline-offset: 1px; } + .fh-row input[type="checkbox"] { cursor: pointer; margin-left: 4px; accent-color: var(--win-blue); } + .fh-row input[type="color"] { width: 24px; height: 24px; border: none; border-radius: 2px; cursor: pointer; flex-shrink: 0; box-shadow: 0 0 0 1px color-mix(in srgb, var(--app-text-color, #111) 14%, transparent) inset; } + .fh-color-wrap { display: flex; align-items: center; gap: 4px; flex: 1; } + .fh-color-edit { + width: 24px; height: 24px; font-size: 12px; background: var(--btn-small-bg); + border: 1px solid color-mix(in srgb, var(--btn-small-border, #ccc) 80%, transparent); + border-radius: 3px; cursor: pointer; color: var(--app-text-color); + display: flex; align-items: center; justify-content: center; transition: background .12s ease, border-color .12s ease; + } + .fh-color-edit:hover { background: var(--btn-hover-bg); border-color: var(--btn-hover-border); } + .fh-separator { border-top: 1px solid color-mix(in srgb, var(--section-divider, #e0e0e0) 70%, transparent); margin: 8px 2px; } + .fh-label { + font-size: 10.5px; color: var(--section-title-color); text-align: left; + font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase; + padding: 2px 4px 6px; + } + .fh-section { + background: color-mix(in srgb, var(--app-text-color, #111) 3%, transparent); + border-radius: 3px; padding: 10px 10px 8px; margin: 0 0 8px; + border: 1px solid color-mix(in srgb, var(--app-text-color, #111) 6%, transparent); + } + .fh-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.2); flex-shrink: 0; margin-right: 4px; } + .fh-swatch-label { font-size: 10px; color: var(--section-title-color); white-space: nowrap; } + .fh-hidden { visibility: hidden; pointer-events: none; } + #fh-capStart-row, #fh-easingStart-row, #fh-capEnd-row, #fh-easingEnd-row { min-height: 22px; } + + #freehand-reopen-btn, #pb-reopen-btn, #gradient-reopen-btn { + position: fixed; left: 0; top: 145px; + z-index: 10000; width: 24px; height: 24px; padding: 0; + background: #f5f6f7; + border: 1px solid var(--border-ribbon, #dadbdc); + border-left: none; border-radius: 0 4px 4px 0; + cursor: pointer; display: none; align-items: center; justify-content: center; + font-size: 10px; color: var(--section-title-color); + transition: color .12s ease; + } + #freehand-reopen-btn:hover, #pb-reopen-btn:hover, #gradient-reopen-btn:hover { color: var(--win-blue); } + #freehand-reopen-btn.show, #pb-reopen-btn.show, #gradient-reopen-btn.show { display: flex; } + + .sb-close-x { + width: 24px; height: 24px; border: none; background: transparent; + cursor: pointer; color: #e81123; font-size: 13px; line-height: 1; + border-radius: 2px; display: flex; align-items: center; justify-content: center; + margin-left: 2px; + transition: background .12s ease, color .12s ease; + } + .sb-close-x:hover { background: #e81123; color: #fff; } + .sb-close-x:active { background: #c20b1c; color: #fff; } + + .fh-reset-row, .pb-reset-row { display: flex; justify-content: center; padding: 10px 4px 6px; } + #fh-reset-btn, #pb-reset-btn { + font-size: 11px; padding: 5px 16px; cursor: pointer; + border: 1px solid color-mix(in srgb, var(--btn-small-border, #ccc) 75%, transparent); + background: var(--btn-small-bg, #f9f9f9); + border-radius: 3px; color: var(--app-text-color); + transition: background .12s ease, border-color .12s ease, box-shadow .12s ease; + } + #fh-reset-btn:hover, #pb-reset-btn:hover { + background: color-mix(in srgb, var(--win-blue) 10%, var(--btn-hover-bg, #eaf4ff)); + border-color: var(--win-blue); + } + #fh-reset-btn:active, #pb-reset-btn:active { box-shadow: 0 0 0 3px color-mix(in srgb, var(--win-blue) 22%, transparent); } + + .pb-section { + background: color-mix(in srgb, var(--app-text-color, #111) 3%, transparent); + border-radius: 3px; padding: 6px 6px; margin: 0 0 8px; + border: 1px solid color-mix(in srgb, var(--app-text-color, #111) 6%, transparent); + } + .pb-row { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--threshold-label-color); padding: 0; margin-bottom: 2px; } + .pb-row label { + min-width: 60px; + text-align: right; + white-space: nowrap; + text-transform: uppercase; + } + .pb-row > input[type="range"] { flex: 1; height: 14px; cursor: pointer; min-width: 40px; } + .pb-row select { + font-size: 10px; padding: 3px 6px; border: 1px solid color-mix(in srgb, var(--btn-small-border, #ccc) 80%, transparent); + border-radius: 2px; background: var(--dropdown-bg); color: var(--app-text-color); + transition: border-color .12s ease; + } + .pb-row select:hover { border-color: color-mix(in srgb, var(--win-blue) 50%, var(--btn-small-border, #ccc)); } + .pb-smode-btns { display: flex; gap: 2px; flex: 1; } + .pb-smode-btn { + flex: 1; font-size: 10px; padding: 3px 4px; cursor: pointer; + border: 1px solid transparent; background: transparent; border-radius: 3px; + color: var(--app-text-color); text-align: center; white-space: nowrap; + transition: background .12s ease, color .12s ease, box-shadow .12s ease; + } + .pb-smode-btn.active { background: var(--win-blue, #0078d7); color: #fff; border-color: var(--win-blue, #0078d7); } + .pb-smode-btn:hover { background: color-mix(in srgb, var(--win-blue) 14%, transparent); } + .pb-smode-btn.active:hover { background: var(--accent, #005bb5); } + body.dark-mode .pb-smode-btn:hover { background: color-mix(in srgb, var(--win-blue) 22%, transparent); } + body.dark-mode .pb-smode-btn.active { background: var(--win-blue, #0078d7); } + .pb-row.disabled { opacity: 0.4; pointer-events: none; } + .pb-val { min-width: 24px; text-align: center; font-size: 10px; } + .pb-slider-wrap { position: relative; flex: 1; height: 20px; min-width: 24px; font-size: 10px; } + .pb-slider-wrap input[type="range"] { position: absolute; top: 0; left: 0; width: 100%; height: 20px; margin: 0; -webkit-appearance: none; appearance: none; background: linear-gradient(to right, var(--win-blue) 0%, var(--win-blue) var(--pct, 0%), color-mix(in srgb, var(--app-text-color, #111) 14%, transparent) var(--pct, 0%), color-mix(in srgb, var(--app-text-color, #111) 14%, transparent) 100%); border-radius: 10px; cursor: pointer; outline: none; z-index: 1; transition: box-shadow .12s ease; } + .pb-slider-wrap input[type="range"]:focus-visible { } + .pb-slider-wrap input[type="range"]::-webkit-slider-runnable-track { background: transparent; height: 20px; border: none; } + .pb-slider-wrap input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 0; height: 0; opacity: 0; } + .pb-slider-wrap input[type="range"]::-moz-range-track { background: transparent; height: 20px; border: none; } + .pb-slider-wrap input[type="range"]::-moz-range-progress { background: transparent; height: 20px; } + .pb-slider-wrap input[type="range"]::-moz-range-thumb { width: 0; height: 0; opacity: 0; } + .pb-slider-wrap .pb-val { position: absolute; top: 0; left: 0; width: 100%; height: 20px; line-height: 20px; text-align: right; padding-right: 4px; font-size: 11px; font-weight: 700; pointer-events: none !important; min-width: auto; z-index: 2; box-sizing: border-box; background: linear-gradient(to right, #fff var(--pct, 0%), var(--win-blue, #0078d7) var(--pct, 0%)); -webkit-background-clip: text; background-clip: text; color: transparent; } + .pb-slider-wrap .pb-label-inner { position: absolute; top: 0; left: 0; width: 100%; height: 20px; line-height: 20px; padding-left: 4px; font-size: 11px; font-weight: 700; pointer-events: none !important; white-space: nowrap; z-index: 2; text-align: left; box-sizing: border-box; background: linear-gradient(to right, #fff var(--pct, 0%), var(--win-blue, #0078d7) var(--pct, 0%)); -webkit-background-clip: text; background-clip: text; color: transparent; } + body.dark-mode .pb-slider-wrap input[type="range"], body.dark-mode .fh-row input[type="range"] { background: linear-gradient(to right, var(--win-blue) 0%, var(--win-blue) var(--pct, 0%), color-mix(in srgb, #fff 16%, transparent) var(--pct, 0%), color-mix(in srgb, #fff 16%, transparent) 100%); } + .pb-label { + font-size: 10.5px; color: var(--section-title-color); text-align: left; + font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase; + padding: 2px 4px 6px; + } + #pb-brush-grid { display: grid; grid-template-columns: repeat(4, 66px); gap: 5px 3px; margin-bottom: 6px; margin-left: -9px; } + .pb-brush-tile { position: relative; aspect-ratio: 1; border: 2px solid transparent; border-radius: 4px; cursor: pointer; overflow: hidden; background: #fff; transition: border-color .15s ease; } + .pb-brush-tile:hover { border-color: var(--accent-light, #8ab4f8); } + .pb-brush-tile.active { border-color: var(--accent, #1a73e8); } + .pb-brush-tile canvas { width: 100%; height: 100%; display: block; } + #pb-active-name { text-align: center; font-size: 11px; font-weight: 600; color: var(--app-text-color); padding: 2px 0 6px; } + #pb-key-settings { margin-bottom: 4px; padding: 0; } + #pb-more-settings.pb-more-collapsed #pb-more-body { display: none; } + #pb-more-toggle { + width: 100%; background: color-mix(in srgb, var(--app-text-color, #111) 4%, transparent); + border: 1px solid color-mix(in srgb, var(--section-divider, #e0e0e0) 60%, transparent); + border-radius: 2px; padding: 5px 6px; font-size: 10px; cursor: pointer; + color: var(--section-title-color); text-align: center; margin: 4px 0; + transition: background .12s ease, border-color .12s ease; + } + #pb-more-toggle:hover { background: color-mix(in srgb, var(--win-blue) 10%, transparent); border-color: color-mix(in srgb, var(--win-blue) 30%, transparent); } + .pb-arrow { display: inline-block; vertical-align: middle; transition: transform .15s ease; } + #pb-more-toggle[aria-expanded="true"] .pb-arrow { transform: rotate(180deg); } + #pb-more-body { padding: 6px 2px 2px; } + #paintbrush-sidebar-body > .pb-section { position: relative; } + + .wand-icon { display: none; line-height: 1; width: 22px; height: 22px; align-items: center; justify-content: center; transform: translateX(-1px) translateZ(0); } + .wand-icon.show { display: inline-flex; } + .wand-icon img { + width: 22px; + height: 22px; + display: block; + } + .pencil-icon { display: none; line-height: 1; width: 22px; height: 22px; align-items: center; justify-content: center; transform: translateX(0px) translateZ(0); } + .pencil-icon.show { display: inline-flex; } + .pencil-icon img { + width: 22px; + height: 22px; + display: block; + } + .pencil-icon-standard img { + width: 17px; + height: 19px; + } + .wand-menu-icon { width: 20px; height: 20px; display: inline-flex; justify-content: center; align-items: center; line-height: 1; } + .wand-menu-icon img { + width: 20px; + height: 20px; + display: block; + } + .icon-wand-global { + transform: translateX(1px); + } + .ribbon-toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 0 4px; } + .ribbon-toggle input { margin: 0; width: 14px; height: 14px; accent-color: var(--toggle-icon-color); } + .toggle-btn { + gap: 4px; + background: transparent; + border: 1px solid transparent; + } + .toggle-btn .toggle-icons { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--toggle-icon-color); + } + #anchor-toggle-btn .toggle-icons { color: var(--anchor-toggle-icon-color); } + #theme-mode-btn .toggle-icons { color: var(--theme-mode-toggle-icon-color); } + body.dark-mode .toggle-btn .toggle-icons { color: var(--toggle-icon-color-dark); } + body.dark-mode #anchor-toggle-btn .toggle-icons { color: var(--anchor-toggle-icon-color-dark); } + body.dark-mode #theme-mode-btn .toggle-icons { color: var(--theme-mode-toggle-icon-color-dark); } + body.dark-mode svg [fill="#0078d7"] { fill: #ffffff; } + body.dark-mode svg [stroke="#0078d7"] { stroke: #ffffff; } + .toggle-btn .toggle-icons svg { width: 24px; height: 24px; display: none; } + .toggle-btn .toggle-icons img { display: none; } + .toggle-btn.is-on .icon-on { display: block; } + .toggle-btn:not(.is-on) .icon-off { display: block; } + .toggle-status { + font-size: 11px; + color: var(--toggle-status-color); + } + .hotkeys-icon { color: var(--hotkeys-icon-color); } + body.dark-mode .hotkeys-icon { color: var(--hotkeys-icon-color-dark); } + + /* Selection dropdown (MS Paint style) */ + #select-menu { + --bg-color: #ffffff; + --border-outer: #8e8f8f; + --header-text: #1e395b; + --text-color: #1e1e1e; + --disabled-text: #8c8c8c; + --separator: #e0e0e0; + --item-hover-bg: #eaf6fd; + --item-hover-border: #a8d2f3; + --item-select-bg: #e2f1ff; + --item-select-border: #a4cef5; + --icon-stroke: #1d59aa; + background-color: var(--bg-color); + border: 1px solid var(--border-outer); + box-shadow: 2px 2px 3px rgba(0,0,0,0.2); + padding: 2px; + width: 170px; + height: 197px; + overflow: hidden; + flex-direction: column; + } + #select-menu .header { + color: var(--select-menu-header-color); + font-weight: 600; + padding: 2px 0 4px 15px; + letter-spacing: 0.2px; + background-color: var(--select-menu-header-bg); + } + #select-menu .item { + display: flex; + align-items: center; + height: 20px; + margin: 0 1px 1px; + border: 1px solid transparent; + position: relative; + color: var(--text-color); + } + #select-menu .item.active, + #select-menu .item.checked, + #select-menu .item.selection-highlight { + background-color: var(--item-select-bg); + border-color: var(--item-select-border); + } + #select-menu .item:not(.disabled):not(.active):not(.checked):hover { + background-color: var(--item-hover-bg); + border-color: var(--item-hover-border); + } + #select-menu .gutter { + width: 28px; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + } + #select-menu svg { + width: 16px; + height: 16px; + min-width: 16px; + min-height: 16px; + max-width: 16px; + max-height: 16px; + shape-rendering: crispEdges; + fill: none; + stroke: var(--icon-stroke); + stroke-width: 1; + } + #select-menu img { display: block; } + #select-menu .icon-poly { image-rendering: pixelated; image-rendering: crisp-edges; } + #select-menu .dashed { stroke-dasharray: 2 2; } + #select-menu .icon-all rect { fill: var(--select-menu-icon-all-fill); stroke: var(--icon-stroke); stroke-dasharray: 2 2; } + #select-menu .disabled svg { stroke: var(--select-menu-icon-disabled-stroke); } + #select-menu .disabled .icon-all rect { fill: var(--select-menu-icon-all-fill-disabled); } + #select-menu .icon-delete path { stroke: var(--select-menu-icon-delete-stroke); stroke-width: 2; shape-rendering: geometricPrecision; } + #select-menu .disabled .icon-delete path { stroke: var(--select-menu-icon-disabled-stroke); } + #select-menu .label { color: inherit; padding-left: 4px; white-space: nowrap; } + #select-menu .shortcut { text-decoration: underline; } + #select-menu .disabled { color: var(--disabled-text); pointer-events: none; } + #select-menu .separator { + height: 1px; + background-color: var(--separator); + margin: 4px 0 4px 30px; + width: calc(100% - 34px); + } + + #viewport { flex: 1; background: var(--bg-canvas); overflow: auto; position: relative; display: flex; padding: 10px; z-index: 0; transition: padding-left .24s cubic-bezier(.16,1,.3,1); } + #viewport.free-canvas { padding: 0; } + #viewport.sidebar-open { padding-left: 290px; } + #viewport.no-pad-transition { transition: none !important; } + + #canvas-stage { position: relative; margin: 0; box-shadow: 3px 3px 5px rgba(0,0,0,0.2); background: white; cursor: crosshair; transform-origin: 0 0; flex-shrink: 0; image-rendering: pixelated; } + #canvas-stage.drop-highlight { + outline: 2px dashed #2a73d9; + outline-offset: 4px; + box-shadow: 0 0 0 4px rgba(42,115,217,0.18), 3px 3px 5px rgba(0,0,0,0.2); + } + + /* Hide default cursor when eraser is active so we only see the ghost */ + #canvas-stage.eraser-active { cursor: none !important; } + + canvas { display: block; position: absolute; top: 0; left: 0; image-rendering: pixelated; } + #grad-bar { position: static; } + #layer-main { z-index: 10; } + #layer-temp { z-index: 20; pointer-events: auto; } + #layer-lasso { z-index: 30; pointer-events: none; } + + /* SVG SELECTION OVERLAY */ + #global-overlay-svg { + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + pointer-events: none; + z-index: 30; + overflow: visible; + mix-blend-mode: normal; + } + #global-overlay-svg.overlay-unscaled { + transform: scale(var(--zoom-inv)); + transform-origin: 0 0; + } + #global-overlay-svg.svg-marquee-invert-mode { + mix-blend-mode: difference; + } + + .svg-marquee-rect { + fill: none; + stroke-width: 1px; + stroke-dasharray: 4 4; + vector-effect: non-scaling-stroke; + shape-rendering: crispEdges; + } + .svg-marquee-rect#svg-ghost-rect { + stroke: #8a8a8a; + stroke-dasharray: 2 2; + opacity: 0.9; + } + .svg-marquee-rect.marquee-blue { stroke: #1b6fe5; } + .svg-marquee-rect.marquee-white { stroke: #ffffff; } + .svg-marquee-rect.svg-marquee-invert { + stroke: white; + } + .svg-ants-path { + fill: none; + stroke: white; + stroke-width: 1px; + stroke-dasharray: 15 15; + vector-effect: non-scaling-stroke; + shape-rendering: crispEdges; + animation: ants-march 3.5s linear infinite; + } + .svg-ants-path-back { + fill: none; + stroke: #000; + stroke-width: 1px; + vector-effect: non-scaling-stroke; + shape-rendering: crispEdges; + } + @keyframes ants-march { + to { stroke-dashoffset: -30; } + } + + /* ERASER GHOST CURSOR */ + #eraser-ghost { + position: fixed; + pointer-events: none; + z-index: 9999; + display: none; + outline: 1px solid black; + border: none; + background-color: white; + transform: none; + } + #save-indicator { + position: fixed; + right: 16px; + bottom: 16px; + width: 16px; + height: 16px; + border: 2px solid #3a8dde; + border-top-color: transparent; + border-radius: 50%; + animation: save-spin 0.7s linear infinite; + display: none; + z-index: 20000; + } + @keyframes save-spin { + to { transform: rotate(360deg); } + } + #modal-wincolor { + --win-bg: #f0f0f0; + --win-text: #000000; + --win-border-light: #ffffff; + --win-border-dark: #a0a0a0; + --win-border-darker: #696969; + --win-btn-hover: #e0e0e0; + --win-btn-active: #c0c0c0; + --win-highlight: #0078d7; + --input-border: #d9d9d9; + --input-border-focus: #0078d7; + } + + #modal-wincolor * { box-sizing: border-box; user-select: none; cursor: default !important; } + #modal-wincolor input { user-select: auto !important; cursor: pointer; } + + #modal-depth { + --win-bg: #f0f0f0; + --win-text: #000000; + --win-border-light: #ffffff; + --win-border-dark: #a0a0a0; + --win-highlight: #0078d7; + --border-outer: #d9d9d9; + --input-border: #7a7a7a; + --input-focus: #0078d7; + --btn-hover: #e5f1fb; + --btn-border: #adadad; + } + #modal-depth * { box-sizing: border-box; user-select: none; cursor: default; } + #modal-depth input { user-select: auto; cursor: pointer; } + body.busy-cursor #modal-depth *, + body.busy-cursor #modal-huesat *, + body.busy-cursor #modal-wincolor *, + body.busy-cursor .modal-window, + body.busy-cursor .modal-mask { + cursor: wait !important; + } + #modal-depth .window { + width: 330px; + background-color: var(--win-bg); + border: 1px solid #999; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + display: flex; + flex-direction: column; + } + #modal-depth .title-bar { + background-color: #fff; + height: 28px; + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 8px; + font-size: 12px; + color: #000; + } + #modal-depth .close-btn { + width: 40px; + height: 28px; + border: none; + background: transparent; + font-size: 14px; + display: flex; + justify-content: center; + align-items: center; + margin-left: auto; + } + #modal-depth .close-btn:hover { background-color: #e81123; color: white; } + #modal-depth .content { + padding: 12px 12px 16px 12px; + display: flex; + flex-direction: column; + gap: 12px; + } + #modal-depth .section-header { + font-weight: 600; + margin-bottom: 6px; + color: var(--win-text); + display: block; + } + #modal-depth .vertical-list { + display: flex; + flex-direction: column; + gap: 4px; + padding-left: 4px; + } + #modal-depth .depth-group-label { + display: block; + margin: 0 0 4px 0; + color: var(--win-text); + } + #modal-depth .depth-color-list { + padding-left: 8px; + } + #modal-depth .depth-color-list .control-item { + padding: 1px 0; + } + #modal-depth .depth-options-list { + gap: 5px; + } + #modal-depth .control-item { + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; + } + #modal-depth .depth-color-list input[type="radio"] { + position: absolute; + opacity: 0; + width: 1px; + height: 1px; + pointer-events: none; + } + #modal-depth .depth-radio { + width: 13px; + height: 13px; + flex: 0 0 13px; + border: 1px solid #707070; + border-radius: 50%; + background: #ffffff; + position: relative; + display: grid; + place-items: center; + cursor: default; + } + #modal-depth .depth-color-list input[type="radio"]:checked + .depth-radio { + border: 0; + background: #0078D7; + } + #modal-depth .depth-color-list .control-item:hover input[type="radio"]:checked + .depth-radio { + transform: none; + } + #modal-depth .depth-color-list input[type="radio"]:checked + .depth-radio::after { + content: ""; + width: 5px; + height: 5px; + border-radius: 50%; + background: #ffffff; + position: static; + transform-origin: center; + animation: depth-radio-pop 120ms ease-out; + } + #modal-depth .depth-color-list .control-item:hover input[type="radio"]:checked + .depth-radio::after { + width: 7px; + height: 7px; + } + #modal-depth .depth-color-list input[type="radio"]:focus { + outline: none; + } + #modal-depth #custom-depth-val { + border: 0; + border-bottom: 1px solid var(--input-border); + background: #ffffff; + text-align: center; + appearance: textfield; + -moz-appearance: textfield; + } + #modal-depth #custom-depth-val:focus { + border: 0; + border-bottom: 2px solid var(--input-focus); + padding: 0 4px; + } + #modal-depth #custom-depth-val::-webkit-outer-spin-button, + #modal-depth #custom-depth-val::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + @keyframes depth-radio-pop { + from { transform: scale(0); } + to { transform: scale(1); } + } + #modal-depth .depth-hidden-legacy, +#unified-sidebar .window .depth-hidden-legacy { + display: none !important; + } + #modal-depth input[type="text"], + #modal-depth input[type="number"] { + height: 22px; + border: 1px solid var(--input-border); + padding: 0 4px; + font-family: inherit; + font-size: 12px; + outline: none; + } + #modal-depth input[type="text"]:focus, + #modal-depth input[type="number"]:focus { + border-color: var(--input-focus); + border-width: 2px; + padding: 0 3px; + } + #modal-depth .divider { + height: 1px; + background-color: #d9d9d9; + width: 100%; + margin: 4px 0; + } + #modal-depth .param-row { + display: flex; + align-items: center; + height: 24px; + } + #modal-depth .param-icon { + width: 20px; + display: flex; + justify-content: center; + align-items: center; + margin-right: 8px; + } + #modal-depth .param-label { width: 70px; } + #modal-depth .param-input-group { flex: 1; display: flex; align-items: center; gap: 8px; } + #modal-depth input[type=range] { + -webkit-appearance: none; + width: 100%; + background: transparent; + } + #modal-depth input[type=range]:focus { outline: none; } + #modal-depth input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + background: #cdcdcd; + border-radius: 2px; + border: 1px solid #a6a6a6; + } + #modal-depth input[type=range]::-webkit-slider-thumb { + height: 16px; + width: 10px; + border-radius: 2px; + background: #0078d7; + cursor: pointer; + -webkit-appearance: none; + margin-top: -7px; + } + #modal-depth input[type=range]:hover::-webkit-slider-thumb { background: #005a9e; } + #modal-depth .footer { + padding: 0 12px 12px 12px; + display: flex; + justify-content: center; + gap: 8px; + } + #modal-depth .btn { + padding: 0 12px; + height: 23px; + font-family: inherit; + font-size: 9pt; + border: 1px solid #adadad; + background-color: #ffffff; + cursor: pointer; + white-space: nowrap; + border-radius: 3px; + min-width: 72px; + outline: none; + } + #modal-depth .btn:hover { background-color: #e5f1fb; border-color: #0078d7; } + #modal-depth .btn:active { background-color: #cce4f7; border-color: #005499; } + #modal-depth .btn:focus-visible { box-shadow: 0 0 0 2px #0078d7; } + #modal-depth .btn-primary { background-color: #ffffff; border: 2px solid #0078d7; } + #modal-depth .btn-primary:hover { background-color: #e5f1fb; } + #modal-depth .btn-primary:active { background-color: #cce4f7; border-color: #005499; } + #modal-depth .sm-icon { width: 16px; height: 16px; } + #modal-depth .stroke-black { stroke: #000; stroke-width: 1.5; fill: none; } + #modal-depth .fill-red { fill: #d00000; stroke: none; } + + + /* ── Stray Pixel Cleaner modal ─────────────────────────────────────────────── */ + #modal-gapstitch { --gs-win-bg: #f0f0f0; --gs-text: #000000; --gs-input-border: #7a7a7a; --gs-focus: #0078d7; --gs-btn-border: #adadad; --gs-btn-hover: #e5f1fb; } + #modal-gapstitch * { box-sizing: border-box; user-select: none; cursor: default; } + #modal-gapstitch input { user-select: auto; cursor: pointer; } + #modal-gapstitch .window { width: 310px; background-color: var(--gs-win-bg); border: 1px solid #999; box-shadow: 0 4px 14px rgba(0,0,0,0.22); position: absolute; display: flex; flex-direction: column; font-family: "Segoe UI", "MS Sans Serif", sans-serif; font-size: 11px; } + #modal-gapstitch .title-bar { height: 28px; background: #fff; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; padding: 0 0 0 8px; gap: 6px; } + #modal-gapstitch .title-bar-icon { flex-shrink: 0; } + #modal-gapstitch .title-bar-label { font-weight: 600; font-size: 11px; flex: 1; color: var(--gs-text); } + #modal-gapstitch .close-btn { margin-left: auto; width: 32px; height: 28px; display: flex; align-items: center; justify-content: center; font-size: 14px; cursor: pointer; } + #modal-gapstitch .close-btn:hover { background-color: #e81123; color: white; } + #modal-gapstitch .content { padding: 10px 12px 6px 12px; display: flex; flex-direction: column; gap: 0; } + #modal-gapstitch .gs-section-hdr { font-weight: 600; color: var(--gs-text); display: block; font-size: 11px; border-bottom: 1px solid #d8d8d8; padding-bottom: 4px; margin-bottom: 8px; letter-spacing: 0.01em; } + #modal-gapstitch .gs-rows { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; } + #modal-gapstitch .gs-row { display: grid; grid-template-columns: 108px 1fr 40px; gap: 6px; align-items: center; } + #modal-gapstitch .gs-row-label-wrap { display: flex; align-items: center; gap: 4px; } + #modal-gapstitch .gs-row label { color: #222; font-size: 11px; cursor: default; } + #modal-gapstitch .gs-hint-icon { width: 13px; height: 13px; border-radius: 50%; background: #c8c8c8; color: #fff; font-size: 9px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; cursor: default; flex-shrink: 0; line-height: 1; position: relative; } + #modal-gapstitch .gs-hint-icon:hover .gs-tooltip { display: block; } + #modal-gapstitch .gs-tooltip { display: none; position: absolute; left: 18px; top: -2px; background: #1a1a1a; color: #f5f5f5; font-size: 10px; font-weight: 400; padding: 5px 7px; border-radius: 3px; white-space: normal; width: 170px; line-height: 1.45; z-index: 9999; box-shadow: 0 2px 6px rgba(0,0,0,0.35); pointer-events: none; } + #modal-gapstitch .gs-row input[type="number"] { width: 100%; padding: 2px 4px; border: 1px solid var(--gs-input-border); font-size: 11px; font-family: inherit; background: #fff; color: var(--gs-text); height: 22px; } + #modal-gapstitch .gs-row input[type="number"]:focus { outline: none; border-color: var(--gs-focus); box-shadow: 0 0 0 1px var(--gs-focus); } + #modal-gapstitch .gs-val { text-align: right; color: #666; font-size: 11px; min-width: 34px; } + #modal-gapstitch .gs-desc { font-size: 10px; color: #888; line-height: 1.5; margin-bottom: 10px; padding: 5px 7px; background: #e8eef4; border-left: 2px solid #a0bcd8; } + #modal-gapstitch .footer { padding: 7px 12px 10px 12px; display: flex; justify-content: flex-end; gap: 6px; border-top: 1px solid #ddd; } + #modal-gapstitch .btn { padding: 0 8px; height: 22px; min-width: 70px; font-family: inherit; font-size: 9pt; border: 1px solid #adadad; background-color: #ffffff; cursor: pointer; white-space: nowrap; border-radius: 3px; outline: none; } + #modal-gapstitch .btn:hover { background-color: #e5f1fb; border-color: #0078d7; } + #modal-gapstitch .btn:active { background-color: #cce4f7; border-color: #005499; } + #modal-gapstitch .btn:focus-visible { box-shadow: 0 0 0 2px #0078d7; } + #modal-gapstitch .btn-primary { background: #fff; border: 2px solid #0078d7; border-radius: 3px; } + #modal-gapstitch .btn-primary:hover { background: #e5f1fb; } + #modal-gapstitch .btn-primary:active { background: #cce4f7; border-color: #005499; } + + #modal-export { + --win-bg: #f0f0f0; + --win-text: #000000; + --input-border: #7a7a7a; + --input-focus: #0078d7; + --btn-border: #adadad; + --btn-hover: #e5f1fb; + } + #modal-export * { box-sizing: border-box; user-select: none; cursor: default; } + #modal-export input { user-select: auto; cursor: pointer; } + #modal-export .window { + background-color: var(--win-bg); + border: 1px solid #999; + box-shadow: 0 8px 24px rgba(0,0,0,0.3); + position: absolute; + display: flex; + flex-direction: column; + font-family: "Segoe UI", "MS Sans Serif", sans-serif; + font-size: 11px; + } + #modal-export .title-bar { + height: 28px; + background: #fff; + border-bottom: 1px solid #ccc; + display: flex; + align-items: center; + padding: 0 8px; + } + #modal-export .close-btn { + margin-left: auto; + width: 32px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + } + #modal-export .close-btn:hover { background-color: #e81123; color: white; } + #modal-export .content { padding: 8px 10px 10px 10px; } + #modal-export fieldset { + border: 1px solid #dfdfdf; + margin: 0; + padding: 10px 8px 12px 8px; + border-radius: 2px; + } + #modal-export legend { + font-size: 11px; + color: #000; + } + #modal-export .divider { + height: 1px; + background: #dcdcdc; + margin: 6px 0 8px 0; + } + #modal-export .checkbox-row { + display: flex; + align-items: center; + gap: 6px; + margin: 6px 0; + } + #modal-export .footer { + background-color: var(--win-bg); + padding: 8px 10px 10px 10px; + display: flex; + justify-content: flex-end; + gap: 8px; + } + #modal-export .btn { + padding: 0 12px; + height: 23px; + font-family: inherit; + font-size: 9pt; + border: 1px solid #adadad; + background-color: #ffffff; + cursor: pointer; + white-space: nowrap; + border-radius: 3px; + outline: none; + } + #modal-export .btn:hover { background-color: #e5f1fb; border-color: #0078d7; } + #modal-export .btn:active { background-color: #cce4f7; border-color: #005499; } + #modal-export .btn:focus-visible { box-shadow: 0 0 0 2px #0078d7; } + #modal-export .btn-primary { background: #ffffff; color: #000000; border: 2px solid #0078d7; } + #modal-export .btn-primary:hover { background-color: #e5f1fb; } + #modal-export .btn-primary:active { background-color: #cce4f7; border-color: #005499; } + + /* huesat floats over canvas — never dim the background so colors stay visible */ + #modal-huesat.modal-mask { background: transparent !important; } + + #modal-huesat { + --win-bg: #f0f0f0; + --win-text: #000000; + --input-border: #7a7a7a; + --input-focus: #0078d7; + --btn-border: #adadad; + --btn-hover: #e5f1fb; + } + #modal-huesat * { box-sizing: border-box; user-select: none; cursor: default; } + #modal-huesat input { user-select: auto; cursor: pointer; } + #modal-huesat .window { + width: 320px; + background-color: var(--win-bg); + border: 1px solid #999; + box-shadow: 0 10px 30px rgba(0,0,0,0.4); + position: absolute; + padding: 8px; + display: flex; + flex-direction: column; + font-family: "Segoe UI", "MS Sans Serif", sans-serif; + font-size: 11px; + } + #modal-huesat .title-bar { + height: 28px; + background: white; + border-bottom: 1px solid #ccc; + display: flex; + align-items: center; + padding-left: 8px; + margin: -8px -8px 8px -8px; + } + #modal-huesat .close-btn { + margin-left: auto; + width: 32px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + } + #modal-huesat .close-btn:hover { background-color: #e81123; color: white; } + #modal-huesat .content { padding: 4px 8px 8px 8px; } + #modal-huesat .row { + display: grid; + grid-template-columns: 70px 1fr 52px; + gap: 8px; + align-items: center; + margin-bottom: 8px; + } + #modal-huesat .channel-area { + --hs-area-bg: #f8f8f8; + border: 1px solid #e0e0e0; + background: #f8f8f8; + border-radius: 4px; + padding: 10px 8px 8px 8px; + margin: 4px 0 10px 0; + } + #modal-huesat .channel-label { font-size: 10px; color: #888; margin-bottom: 8px; letter-spacing: 0.04em; text-transform: uppercase; } + + /* Hexagonal colour wheel — absolute positioned around master */ + #modal-huesat .channel-wheel { + position: relative; + width: 210px; + height: 126px; + margin: 0 auto 6px auto; + } + #modal-huesat .channel-wrap { + width: 36px; + height: 24px; + position: absolute; + cursor: pointer; + } + #modal-huesat .channel-btn { + width: 36px; + height: 24px; + border-radius: 3px; + border: 1px solid rgba(0, 0, 0, 0.3); + cursor: pointer; + position: absolute; + top: 0; left: 0; + box-sizing: border-box; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.3); + } + #modal-huesat .channel-wrap.active .channel-btn { + border-color: #0078d7; + box-shadow: 0 0 0 2px rgba(0,120,215,0.25), inset 0 1px 0 rgba(255,255,255,0.3); + } + #modal-huesat .pos-r { top: 2px; left: 87px; } + #modal-huesat .pos-y { top: 30px; left: 22px; } + #modal-huesat .pos-m { top: 30px; right: 22px; } + #modal-huesat .pos-g { top: 72px; left: 22px; } + #modal-huesat .pos-b { top: 72px; right: 22px; } + #modal-huesat .pos-c { top: 100px; left: 87px; } + #modal-huesat .pos-r .channel-btn { background: #ff0000; } + #modal-huesat .pos-y .channel-btn { background: #ffff00; } + #modal-huesat .pos-m .channel-btn { background: #ff00ff; } + #modal-huesat .pos-g .channel-btn { background: #00ff00; } + #modal-huesat .pos-b .channel-btn { background: #0000ff; } + #modal-huesat .pos-c .channel-btn { background: #00ffff; } + + /* Master button in the centre */ + #modal-huesat .master-btn { + position: absolute; + top: 50px; + left: 50%; + transform: translateX(-50%); + background: #f0f0f0; + color: #222; + padding: 3px 12px; + border-radius: 2px; + cursor: pointer; + font-size: 11px; + font-family: 'Segoe UI', sans-serif; + border: 1px solid #adadad; + white-space: nowrap; + transition: background 0.08s, border-color 0.08s, color 0.08s, outline 0.08s; + outline: 1px solid transparent; + outline-offset: 1px; + box-shadow: 0 1px 2px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.7); + } + #modal-huesat .master-btn:hover { background: #e8e8e8; border-color: #888; } + #modal-huesat .master-btn.active { background: #f0f0f0; border-color: #0078d7; color: #222; font-weight: 600; outline: none; } + + /* Live preview strip */ + + #modal-huesat .hs-live-preview, + #modal-huesat #hs-prev-original, + #modal-huesat #hs-prev-adjusted, + #modal-huesat .hs-prev-divider { display: none; } + + #modal-huesat .sub-row { + display: flex; + justify-content: space-between; + align-items: center; + margin: 6px 0 4px 0; + } + #modal-huesat .sub-row label { font-size: 11px; color: #555; } + #modal-huesat input[type="range"] { + width: 100%; + height: 7px; + -webkit-appearance: none; + appearance: none; + border-radius: 3px; + outline: none; + cursor: pointer; + } + #modal-huesat input[type="range"]::-webkit-slider-runnable-track { + height: 7px; + background: transparent; + border-radius: 3px; + border: none; + } + #modal-huesat input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 10px; + height: 19px; + border-radius: 2px; + background: rgba(255,255,255,0.92); + border: 1px solid rgba(0,0,0,0.28); + margin-top: -6px; + cursor: pointer; + } + #modal-huesat input[type="range"]::-moz-range-track { + height: 7px; + background: transparent; + border-radius: 3px; + border: none; + } + #modal-huesat input[type="range"]::-moz-range-progress { + height: 7px; + background: transparent; + border-radius: 3px; + } + #modal-huesat input[type="range"]::-moz-range-thumb { + width: 10px; + height: 19px; + border-radius: 2px; + background: rgba(255,255,255,0.92); + border: 1px solid rgba(0,0,0,0.28); + cursor: pointer; + } + #modal-huesat #hs-hue { background: linear-gradient(to right, hsl(0,100%,50%), hsl(30,100%,50%), hsl(60,100%,50%), hsl(90,100%,50%), hsl(120,100%,50%), hsl(150,100%,50%), hsl(180,100%,50%), hsl(210,100%,50%), hsl(240,100%,50%), hsl(270,100%,50%), hsl(300,100%,50%), hsl(330,100%,50%), hsl(360,100%,50%)); border-radius: 3px; } + #modal-huesat #hs-sat { background: linear-gradient(to right, #808080 0%, #ff0000 100%); border-radius: 3px; } + #modal-huesat #hs-light { background: linear-gradient(to right, #000 0%, #888 50%, #fff 100%); border-radius: 3px; } + #modal-huesat #hs-chroma { background: linear-gradient(to right, #888 0%, #e85e3a 100%); border-radius: 3px; } + #modal-huesat #hs-overlap { background: linear-gradient(to right, #bbb 0%, #0078d7 100%); border-radius: 3px; } + + #modal-huesat input[type="number"] { + height: 22px; + border: 1px solid var(--input-border); + padding: 0 4px; + font-family: inherit; + font-size: 11px; + width: 52px; + } + #modal-huesat input[type="number"]:focus { + outline: none; + border-color: var(--input-focus); + border-width: 2px; + padding: 0 3px; + } + #modal-huesat .footer { + display: flex; + justify-content: flex-end; + gap: 6px; + padding: 6px 8px 8px 8px; + border-top: 1px solid #d9d9d9; + } + #modal-huesat .btn { + height: 23px; + padding: 0 12px; + min-width: 64px; + background-color: #ffffff; + border: 1px solid #adadad; + border-radius: 3px; + cursor: pointer; + font-family: inherit; + font-size: 9pt; + color: var(--win-text); + white-space: nowrap; + outline: none; + } + #modal-huesat .btn:hover { background-color: #e5f1fb; border-color: #0078d7; } + #modal-huesat .btn:active { background-color: #cce4f7; border-color: #005499; } + #modal-huesat .btn:focus-visible { box-shadow: 0 0 0 2px #0078d7; } + #modal-huesat .btn-primary { background-color: #ffffff; border: 2px solid #0078d7; } + #modal-huesat .btn-primary:hover { background-color: #e5f1fb; } + #modal-huesat .btn-primary:active { background-color: #cce4f7; border-color: #005499; } + /* ── Hue/Sat sidebar mode ── */ + #modal-huesat.hs-sidebar-mode { + position: fixed !important; + top: 145px; left: 0; bottom: 0; + width: 320px !important; + z-index: 9000; + background: transparent; + display: flex !important; + align-items: stretch; + pointer-events: none; + } + #modal-huesat.hs-sidebar-mode .window { + position: relative !important; + width: 320px !important; + height: 100% !important; + border-radius: 0 !important; + border-left: none !important; + border-top: none !important; + border-bottom: none !important; + box-shadow: none !important; + overflow-y: auto; + overflow-x: hidden; + pointer-events: all; + display: flex; + flex-direction: column; + } + #modal-huesat.hs-sidebar-mode .content { + flex: 1; + overflow-y: auto; + } + /* Sidebar overlays the viewport — canvas does not shift */ + #modal-resize.utility-sidebar-mode, + #modal-depth.utility-sidebar-mode { + position: fixed !important; + top: 145px; left: 0; bottom: 0; + z-index: 9000; + background: transparent; + display: flex !important; + align-items: stretch; + pointer-events: none; + } + #modal-resize.utility-sidebar-mode { width: 264px !important; } + #modal-depth.utility-sidebar-mode { width: 330px !important; } + #modal-resize.utility-sidebar-mode .window, + #modal-depth.utility-sidebar-mode .window { + position: relative !important; + height: 100% !important; + border-radius: 0 !important; + border-left: none !important; + border-top: none !important; + border-bottom: none !important; + box-shadow: none !important; + overflow-y: auto; + overflow-x: hidden; + pointer-events: all; + display: flex; + flex-direction: column; + } + #modal-resize.utility-sidebar-mode .window { width: 264px !important; } + #modal-depth.utility-sidebar-mode .window { width: 330px !important; } + #modal-resize.utility-sidebar-mode .content, + #modal-depth.utility-sidebar-mode .content { + flex: 1; + overflow-y: auto; + } + body.huesat-sidebar-open #huesat-split-handle { + left: 280px !important; + top: 145px !important; + } + /* Sidebar dock/undock button */ + .hs-sidebar-toggle, + .utility-sidebar-toggle { + height: 100%; + align-self: stretch; + box-sizing: border-box; + width: 26px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + color: #666; + font-size: 13px; + border-right: 1px solid rgba(0,0,0,0.12); + margin-right: 6px; + transition: background .1s, color .1s; + user-select: none; + } + .hs-sidebar-toggle:hover, + .utility-sidebar-toggle:hover { + background: #e8f0fe; + color: #0078d7; + } + .hs-sidebar-toggle svg, + .utility-sidebar-toggle svg { + width: 14px; + height: 14px; + fill: currentColor; + } + .hs-sidebar-toggle:hover, + .utility-sidebar-toggle:hover { + background: #e8f0fe; + color: #0078d7; + } + .hs-sidebar-toggle svg, + .utility-sidebar-toggle svg { + width: 14px; + height: 14px; + fill: currentColor; + } + .util-dock-icon { + width: 16px; + height: 16px; + flex: none; + display: block; + image-rendering: pixelated; + image-rendering: crisp-edges; + } + #unified-sidebar .sidebar-title-btn .util-dock-icon { + width: 16px; + height: 16px; + } + + /* ── Unified Sidebar (shared empty container) ── */ + #unified-sidebar { + position: fixed; + left: -296px; + top: 145px; + bottom: 24px; + width: 280px; + max-width: 90vw; + background: var(--bg-ribbon, #f5f6f7); + border-right: 1px solid var(--border-ribbon, #dadbdc); + z-index: 9999; + display: flex; + flex-direction: column; + overflow: hidden; + transition: left .24s cubic-bezier(.16,1,.3,1); + } + /* ── Unified sidebar CSS variables ── */ + #unified-sidebar { + --s-bg: #ffffff; + --s-chrome: #f3f3f3; + --s-hover: #e5e5e5; + --s-pressed: #d0d0d0; + --s-sep: #e0e0e0; + --s-accent: #0078d7; + --s-accent-hov: #106ebe; + --s-accent-press:#005a9e; + --s-title-bg: #ffffff; + --s-title-txt: #000000; + --s-title-hov: rgba(255,255,255,.15); + --s-close-hov: #e81123; + --s-font: "Segoe UI", system-ui, sans-serif; + --s-size: 9pt; + --s-color: #201f1e; + --s-muted: #605e5c; + } + #unified-sidebar.hidden { left: -296px; } + #unified-sidebar:not(.hidden) { left: 0; } + /* ── Title bar ── */ + #unified-sidebar .sidebar-title { + height: 28px; + min-height: 28px; + background: var(--s-title-bg); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 0 0 8px; + user-select: none; + flex-shrink: 0; + } + #unified-sidebar .sidebar-title-text { + font-family: var(--s-font); + font-size: var(--s-size); + font-weight: 600; + color: var(--s-title-txt); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: .01em; + } + #unified-sidebar .sidebar-title-buttons { + display: flex; + align-items: stretch; + height: 100%; + flex-shrink: 0; + } + /* Float (⬈) and Close (✕) caption buttons */ + #unified-sidebar .sidebar-title-btn { + width: 28px; + height: 100%; + border: none; + background: transparent; + font-size: 11px; + font-family: var(--s-font); + color: var(--s-title-txt); + cursor: default; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + flex-shrink: 0; + border-radius: 0; + outline: none; + transition: background .1s; + } + #unified-sidebar .sidebar-title-btn:hover { + background: var(--s-title-hov); + } + #unified-sidebar .sidebar-title-btn:active { + background: rgba(255,255,255,.25); + } + #unified-sidebar #sidebar-close-btn:hover { + background: var(--s-close-hov); + color: #ffffff; + } + /* ── Tab strip — Fluent-style underline tabs ── */ + #unified-sidebar .sidebar-modes { + display: flex; + align-items: stretch; + background: var(--s-chrome); + border-bottom: 1px solid var(--s-sep); + padding: 0; + overflow-x: auto; + flex-shrink: 0; + } + #unified-sidebar .sidebar-modes button { + position: relative; + height: 32px; + padding: 0 14px; + flex: 1; + background: transparent; + border: none; + font-family: var(--s-font); + font-size: var(--s-size); + font-weight: 400; + color: var(--s-muted); + cursor: default; + border-radius: 0; + white-space: nowrap; + outline: none; + transition: color .1s, background .1s; + } + #unified-sidebar .sidebar-modes button:hover { + background: var(--s-hover); + color: var(--s-color); + } + #unified-sidebar .sidebar-modes button:active { + background: var(--s-pressed); + } + /* Active tab: bold text + 2px blue underline bar */ + #unified-sidebar .sidebar-modes button.active { + color: var(--s-accent); + font-weight: 600; + background: var(--s-bg); + } + #unified-sidebar .sidebar-modes button.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 14px; + right: 14px; + height: 2px; + background: var(--s-accent); + border-radius: 1px 1px 0 0; + } + #unified-sidebar .sidebar-content { + flex: 1; + overflow: auto; + position: relative; + } + #unified-sidebar .window { + position: static !important; + width: 100% !important; + height: 100% !important; + border: none !important; + border-radius: 0 !important; + box-shadow: none !important; + background: var(--win-bg, #fff); + display: flex; + flex-direction: column; + overflow: hidden; + padding: 8px; + } + #unified-sidebar .window .title-bar { + display: none; + } + #unified-sidebar .window .content { + flex: 1; + overflow-y: auto; + } + #unified-sidebar .window .footer { + flex-shrink: 0; + } + #unified-sidebar .modal-mask { + position: static !important; + display: flex !important; + flex-direction: column; + width: 100% !important; + height: 100% !important; + left: auto !important; + top: auto !important; + right: auto !important; + bottom: auto !important; + background: transparent !important; + pointer-events: auto; + z-index: auto; + border: none; + box-shadow: none; + border-radius: 0; + align-items: stretch; + justify-content: flex-start; + } + /* ── Resize/Skew sidebar overrides (docked) ── */ + #unified-sidebar #modal-resize .window { + background: #F0F0F0; + } + /* Each fieldset becomes a 2-column grid: icon on the left, fields stacked on the right. */ + #unified-sidebar #modal-resize .rz-fieldset { + min-width: 0; + overflow: visible; + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 10px; + row-gap: -6px; + align-items: center; + margin-bottom: 14px; + padding-bottom: 0; + } + #unified-sidebar #modal-resize .rz-fieldset:last-of-type { margin-bottom: 0; } + /* Neutralize the floating-window margin hacks for the sidebar. */ + #unified-sidebar #modal-resize .row, + #unified-sidebar #modal-resize .rz-h-row, + #unified-sidebar #modal-resize .rz-v-row, + #unified-sidebar #modal-resize .sk-h-row, + #unified-sidebar #modal-resize .sk-v-row, + #unified-sidebar #modal-resize .rz-ratio-row, + #unified-sidebar #modal-resize .checkbox-row { margin: 0; margin-left: 0; } + /* Hide empty icon spacers so column-2 fields start flush left. */ + #unified-sidebar #modal-resize .col-icon:not(:has(img)) { display: none; } + #unified-sidebar #modal-resize .col-icon { width: 32px; align-items: center; justify-content: center; } + #unified-sidebar #modal-resize .col-label { width: 48px; } + /* Icon rows span the full fieldset height, centered in column 1. */ + #unified-sidebar #modal-resize .rz-fieldset > .row:has(.resize-icon-img), + #unified-sidebar #modal-resize .rz-fieldset > .row:has(.skew-icon-img) { + grid-column: 1; + grid-row: 1 / -1; + align-self: center; + justify-self: center; + } + /* Everything else (By / Horizontal / Vertical / aspect ratio) lives in column 2. */ + #unified-sidebar #modal-resize .rz-fieldset > .row:not(:has(img)), + #unified-sidebar #modal-resize .rz-fieldset > .checkbox-row { + grid-column: 2; + } + /* Constrain icon size so it never overflows the narrow column. */ + #unified-sidebar #modal-resize .resize-icon-img, + #unified-sidebar #modal-resize .skew-icon-img { + width: auto; + max-width: none; + height: auto; + image-rendering: pixelated; + transform: none; + } + #unified-sidebar #modal-resize .rz-h-row { transform: none; } + #unified-sidebar #modal-resize .rz-v-row { transform: none; } + #unified-sidebar #modal-resize .rz-ratio-row { transform: none; } + #unified-sidebar #modal-resize #rz-v-px { transform: none; } + #unified-sidebar #modal-resize .rz-h-dual { transform: none; } + #unified-sidebar #modal-resize .checkbox-indent { + margin-left: 0; + padding-left: 0; + } + #unified-sidebar #modal-resize input[type="text"] { + width: 64px; + height: 20px; + box-sizing: border-box; + padding: 0 6px; + border: none; + border-bottom: 1px solid #7a7a7a; + background: #ffffff; + font-size: 11px; + outline: none; + } + #unified-sidebar #modal-resize .rz-dual input.rz-num { width: 47px; } + #unified-sidebar #modal-resize input[type="text"]:hover { + border-bottom-color: #3c3c3c; + } + #unified-sidebar #modal-resize input[type="text"]:focus { + border-bottom-color: var(--toggle-icon-color); + box-shadow: none; + } + + #huesat-split-handle { + position: fixed; + top: 0; + bottom: 0; + width: 4px; + background: rgba(255,255,255,0.8); + display: none; + cursor: col-resize; + z-index: 20000; + } + #huesat-split-handle .hs-circle { + width: 24px; + height: 24px; + background: #fff; + border-radius: 50%; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + color: #555; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + + #modal-wincolor .window { + width: 447px; + height: 360px; + background-color: var(--win-bg); + border: 1px solid #999; + box-shadow: 0 10px 30px rgba(0,0,0,0.4); + position: absolute; + padding: 8px; + color: var(--win-text); + display: flex; + flex-direction: column; + font-family: "Segoe UI", "MS Sans Serif", sans-serif; + font-size: 9pt; + } + + #modal-wincolor .title-bar { + height: 30px; + background: white; + border-bottom: 1px solid #ccc; + display: flex; + align-items: center; + padding-left: 10px; + font-size: 12px; + color: #000; + margin: -8px -8px 8px -8px; + } + #modal-wincolor .close-btn { + margin-left: auto; + width: 32px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + } + #modal-wincolor .close-btn span { transform: translateX(1px); display: inline-block; } + #modal-wincolor .close-btn:hover { background-color: #e81123; color: white; } + + #modal-wincolor .content-area { display: flex; gap: 8px; } + #modal-wincolor .left-pane { width: 189px; flex-shrink: 0; } + #modal-wincolor .right-pane { width: 230px; flex-grow: 1; display: flex; flex-direction: column; } + + #modal-wincolor .label { margin-bottom: 4px; display: block; } + #modal-wincolor #wincolor-custom { margin-top: 6px; } + + #modal-wincolor .swatch-grid { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 4px; + margin-bottom: 10px; + } + #modal-wincolor .swatch { + width: 20px; + height: 17px; + border-top: 1px solid var(--win-border-dark); + border-left: 1px solid var(--win-border-dark); + border-bottom: 1px solid var(--win-border-light); + border-right: 1px solid var(--win-border-light); + background-color: #fff; + position: relative; + } + #modal-wincolor .swatch.selected::after { + content: ''; + position: absolute; + top: -1px; left: -1px; right: -1px; bottom: -1px; + border: 1px solid #000; + } + #modal-wincolor .swatch:hover { border-top: 1px solid var(--win-border-dark); border-left: 1px solid var(--win-border-dark); border-bottom: 1px solid var(--win-border-light); border-right: 1px solid var(--win-border-light); } + + #modal-wincolor button { + height: 22px; + padding: 0 8px; + background: #e1e1e1; + border: 1px solid #adadad; + border-radius: 3px; + font-family: inherit; + font-size: 9pt; + min-width: 70px; + } + #modal-wincolor button:hover:not(:disabled) { + background-color: #e5f1fb; + border-color: var(--toggle-icon-color); + } + #modal-wincolor button:active:not(:disabled) { + background-color: #cce4f7; + border-color: #005499; + } + #modal-wincolor button:disabled { + color: #838383; + background-color: #f0f0f0; + border-color: #d0d0d0; + } + #modal-wincolor .btn-define { width: 100%; margin-bottom: 8px; text-align: center; } + #modal-wincolor .bottom-actions { display: flex; gap: 8px; } + #modal-wincolor .btn-ok { border: 2px solid var(--win-highlight); border-radius: 4px; } + #modal-wincolor .wincolor-bottom-row { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + margin-top: 4px; + padding-left: 4px; + } + #modal-wincolor .bottom-actions button { background: #fff; } + + #modal-wincolor .color-editor-top { display: flex; gap: 17px; margin-bottom: 8px; width: 230px; } + #modal-wincolor .spectrum-box { + width: 185px; + height: 188px; + border-top: 1px solid var(--win-border-dark); + border-left: 1px solid var(--win-border-dark); + border-right: 1px solid var(--win-border-light); + border-bottom: 1px solid var(--win-border-light); + position: relative; + cursor: default; + background: #fff; + padding: 1px; + box-sizing: border-box; + margin: 0; + } + #modal-wincolor #wincolor-spectrum { display: block; cursor: default; } + + #modal-wincolor .crosshair { + position: absolute; + width: 19px; + height: 19px; + pointer-events: none; + } + #modal-wincolor .crosshair::before, + #modal-wincolor .crosshair::after { + content: ''; + position: absolute; + } + #modal-wincolor .crosshair::before { + top: 8px; + left: 0; + width: 19px; + height: 3px; + background: linear-gradient(to right, #000 0 5px, transparent 5px 14px, #000 14px 19px); + } + #modal-wincolor .crosshair::after { + top: 0; + left: 8px; + width: 3px; + height: 19px; + background: linear-gradient(to bottom, #000 0 5px, transparent 5px 14px, #000 14px 19px); + } + + #modal-wincolor .lum-slider-container { position: relative; width: 24px; height: 188px; margin: 0; } + #modal-wincolor .lum-strip { + width: 12px; + height: 188px; + border-top: 1px solid var(--win-border-dark); + border-left: 1px solid var(--win-border-dark); + border-right: 1px solid var(--win-border-light); + border-bottom: 1px solid var(--win-border-light); + display: block; + position: absolute; + left: 0; + top: 0; + } + #modal-wincolor .lum-arrow { + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 7px solid transparent; + border-right: 7px solid #000; + position: absolute; + left: 16px; + top: 0; + transform: translateY(-50%); + } + + #modal-wincolor .color-editor-bottom { display: flex; gap: 8px; } + #modal-wincolor .preview-box-container { display: flex; flex-direction: column; align-items: center; padding-top: 4px; } + #modal-wincolor .preview-box { + width: 58px; + height: 40px; + border-top: 1px solid var(--win-border-dark); + border-left: 1px solid var(--win-border-dark); + border-right: 1px solid var(--win-border-light); + border-bottom: 1px solid var(--win-border-light); + background-color: #000; + margin-bottom: 4px; + } + #modal-wincolor .preview-label { font-size: 11px; color: #000; } + #modal-wincolor .input-group { + display: grid; + grid-template-columns: 36px 30px 44px 30px; + column-gap: 4px; + row-gap: 4px; + align-items: center; + font-size: 9pt; + } + #modal-wincolor .input-label { text-align: right; } + #modal-wincolor input[type="text"] { + width: 100%; + height: 18px; + border: none; + border-bottom: 1px solid #7a7a7a; + padding: 0 3px; + font-family: inherit; + font-size: 9pt; + background: #fff; + } + #modal-wincolor input[type="text"]:focus { outline: none; border-bottom-color: var(--toggle-icon-color); } + #modal-wincolor .btn-add { width: 220px; margin-top: 0; margin-left: 31px; background: #fff; } + #modal-wincolor .tabs { + display: flex; + gap: 4px; + margin-bottom: 6px; + } + #modal-wincolor .tab-btn { + padding: 2px 8px; + border: 1px solid #999; + background: #e1e1e1; + font-size: 11px; + cursor: pointer; + border-radius: 2px; + } + #modal-wincolor .tab-btn.active { + background: #ffffff; + border-color: var(--toggle-icon-color); + box-shadow: inset 0 0 0 1px #0078d7; + } + #modal-wincolor .bpp-panel { + display: none; + padding: 6px; + border: 1px solid #d0d0d0; + background: #fff; + margin-bottom: 6px; + } + #modal-wincolor .bpp-row { + display: grid; + grid-template-columns: 24px 1fr 44px; + gap: 6px; + align-items: center; + margin-bottom: 6px; + font-size: 11px; + } + #modal-wincolor .bpp-row input[type="range"] { width: 100%; } + #modal-wincolor .bpp-preview { + height: 40px; + border: 1px solid #777; + background: #000; + } + + /* Hotkeys modal */ + #modal-hotkeys .window { + width: 700px; + height: 1600px; + background-color: #f0f0f0; + border: 1px solid #999; + box-shadow: 0 10px 30px rgba(0,0,0,0.4); + position: absolute; + padding: 8px; + display: flex; + flex-direction: column; + font-family: "Segoe UI", "MS Sans Serif", sans-serif; + font-size: 9pt; + } + #modal-hotkeys .title-bar { + height: 30px; + background: white; + border-bottom: 1px solid #ccc; + display: flex; + align-items: center; + padding-left: 10px; + font-size: 12px; + color: #000; + margin: -8px -8px 8px -8px; + } + #modal-hotkeys .close-btn { + margin-left: auto; + width: 32px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + } + #modal-hotkeys .close-btn:hover { background-color: #e81123; color: white; } + #modal-hotkeys .content { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + } + /* Toolbar customization modal */ + #modal-toolbar .window { + width: 430px; + background-color: #f0f0f0; + border: 1px solid #999; + box-shadow: 0 0 15px rgba(0,0,0,0.35); + display: flex; + flex-direction: column; + } + #modal-toolbar .title-bar { + height: 26px; + background-color: #ffffff; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px; + font-size: 11px; + border-bottom: 1px solid #cfcfcf; + } + #modal-toolbar .close-btn { + width: 36px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #111; + } + #modal-toolbar .close-btn:hover { + background-color: #e81123; + color: #fff; + } + #modal-toolbar .content { + padding: 6px 8px 8px 8px; + display: flex; + flex-direction: column; + gap: 6px; + } + #modal-toolbar .toolbar-subtitle { + font-size: 11px; + color: #555; + } + #modal-toolbar .toolbar-list { + border: 1px solid #d0d0d0; + background: #ffffff; + padding: 6px; + display: flex; + flex-direction: column; + gap: 6px; + max-height: 320px; + overflow: auto; + } + #modal-toolbar .toolbar-row { + display: grid; + grid-template-columns: 1fr 90px 70px; + gap: 8px; + align-items: center; + font-size: 11px; + padding: 2px 0; + } + #modal-toolbar .toolbar-header { + font-weight: 600; + color: #444; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 4px; + } + #modal-toolbar select { + height: 20px; + font-size: 11px; + } + #modal-toolbar .toolbar-empty { + color: #666; + font-size: 11px; + padding: 6px 2px; + } + #modal-toolbar .footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 8px 10px 10px; + border-top: 1px solid #d9d9d9; + background-color: #f0f0f0; + } + /* Tool customizer modal */ + #modal-tool-customizer .window { + width: 780px; + background-color: #f0f0f0; + border: 1px solid #999; + box-shadow: 0 0 15px rgba(0,0,0,0.35); + display: flex; + flex-direction: column; + } + #modal-tool-customizer .title-bar { + height: 26px; + background-color: #ffffff; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px; + font-size: 11px; + border-bottom: 1px solid #cfcfcf; + cursor: move; + user-select: none; + } + #modal-tool-customizer .close-btn { + width: 36px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #111; + } + #modal-tool-customizer .close-btn:hover { + background-color: #e81123; + color: #fff; + } + #modal-tool-customizer .content { + padding: 6px 8px 8px; + display: flex; + flex-direction: column; + gap: 6px; + } + #modal-tool-customizer .footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 8px 10px 10px; + border-top: 1px solid #d9d9d9; + background-color: #f0f0f0; + } + #modal-tool-customizer .footer .btn { + height: 22px; + padding: 0 12px; + border: 1px solid #adadad; + background: #fff; + font-size: 11px; + border-radius: 2px; + } + #modal-tool-customizer .footer .btn:hover { + background-color: #e5f1fb; + border-color: #0078d7; + } + #modal-tool-customizer .footer .btn.btn-primary { + background: #fff; + border: 2px solid #0078d7; + } + /* Updates modal */ + #modal-update .window { + width: 520px; + max-width: calc(100vw - 34px); + max-height: calc(100vh - 48px); + background-color: #f0f0f0; + border: 1px solid #999; + box-shadow: 0 0 15px rgba(0,0,0,0.35); + display: flex; + flex-direction: column; + font-family: "Segoe UI", "MS Sans Serif", sans-serif; + font-size: 11px; + } + #modal-update .title-bar { + height: 26px; + background-color: #ffffff; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px; + font-size: 11px; + border-bottom: 1px solid #cfcfcf; + cursor: move; + user-select: none; + } + #modal-update .close-btn { + width: 36px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #111; + position: relative; + } + #modal-update .close-btn:hover { background-color: #e81123; color: #fff; } + #modal-update .close-btn .x { + width: 12px; + height: 12px; + display: block; + position: relative; + } + #modal-update .close-btn .x::before, + #modal-update .close-btn .x::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 14px; + height: 2px; + background: currentColor; + transform-origin: center; + border-radius: 2px; + transform: translate(-50%, -50%) rotate(45deg); + } + #modal-update .close-btn .x::after { + transform: translate(-50%, -50%) rotate(-45deg); + } + #modal-update .content { + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + } + #modal-update .update-status { + border: 1px solid #d0d0d0; + background: #ffffff; + padding: 8px 10px; + border-radius: 3px; + font-size: 12px; + color: #1f1f1f; + line-height: 1.25; + } + #modal-update .update-status.is-checking { background: #fffdf0; border-color: #e3d6a0; } + #modal-update .update-status.is-available { background: #f1fbf2; border-color: #9fd2a4; } + #modal-update .update-status.is-ok { background: #f2f7ff; border-color: #9abbe9; } + #modal-update .update-status.is-warn { background: #fff8e6; border-color: #e6c37a; } + #modal-update .update-status.is-error { background: #fff3f3; border-color: #e2a0a0; } + #modal-update .update-progress { + height: 3px; + background: #e6e6e6; + border-radius: 999px; + overflow: hidden; + } + #modal-update .update-progress .bar { + width: 35%; + height: 100%; + background: var(--toggle-icon-color); + animation: update-indeterminate 1.1s ease-in-out infinite; + } + @keyframes update-indeterminate { + 0% { transform: translateX(-120%); } + 100% { transform: translateX(320%); } + } + #modal-update .update-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + } + #modal-update .update-card { + border: 1px solid #d0d0d0; + background: #ffffff; + padding: 8px 10px; + border-radius: 3px; + min-width: 0; + } + #modal-update .update-card-k { font-size: 11px; color: #666; } + #modal-update .update-card-v { + margin-top: 2px; + font-size: 13px; + font-weight: 700; + color: #222; + font-family: Consolas, "Courier New", monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + #modal-update .update-notes-wrap { + border: 1px solid #d0d0d0; + background: #ffffff; + border-radius: 3px; + min-height: 0; + display: flex; + flex-direction: column; + } + #modal-update .update-notes-title { + padding: 7px 10px 6px; + border-bottom: 1px solid #e4e4e4; + color: #555; + font-size: 11px; + } + #modal-update .update-notes { + padding: 8px 10px; + height: 240px; + overflow: auto; + white-space: normal; + font-size: 12px; + line-height: 1.35; + color: #222; + overflow-wrap: anywhere; + } + #modal-update .update-notes .empty { + color: #666; + } + #modal-update .update-notes h1, + #modal-update .update-notes h2, + #modal-update .update-notes h3, + #modal-update .update-notes h4 { + margin: 10px 0 6px 0; + font-family: "Segoe UI", "MS Sans Serif", sans-serif; + color: #111; + line-height: 1.15; + } + #modal-update .update-notes h1 { font-size: 18px; } + #modal-update .update-notes h2 { font-size: 16px; } + #modal-update .update-notes h3 { font-size: 14px; } + #modal-update .update-notes h4 { font-size: 13px; } + #modal-update .update-notes p { margin: 6px 0; } + #modal-update .update-notes ul, + #modal-update .update-notes ol { margin: 6px 0 6px 18px; padding: 0; } + #modal-update .update-notes li { margin: 2px 0; } + #modal-update .update-notes a { color: #0b61c3; text-decoration: underline; } + #modal-update .update-notes code { + font-family: Consolas, "Courier New", monospace; + background: #f1f1f1; + border: 1px solid #e3e3e3; + border-radius: 3px; + padding: 0 4px; + font-size: 11px; + } + #modal-update .update-notes pre { + background: #f6f6f6; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 8px 10px; + overflow: auto; + margin: 8px 0; + white-space: pre; + line-height: 1.35; + } + #modal-update .update-notes pre code { + background: transparent; + border: 0; + padding: 0; + font-size: 11px; + } + #modal-update .footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + padding: 8px 10px 10px; + border-top: 1px solid #d9d9d9; + background-color: #f0f0f0; + } + #modal-update .footer .spacer { flex: 1; } + #modal-update .footer .btn { + height: 24px; + padding: 0 10px; + border: 1px solid #adadad; + background-color: #e1e1e1; + font-family: inherit; + font-size: 12px; + cursor: pointer; + border-radius: 2px; + } + #modal-update .footer .btn:hover { background-color: #e5f1fb; border-color: var(--toggle-icon-color); } + #modal-update .footer .btn.btn-primary { + background: #fff; + border: 1px solid #0078d7; + box-shadow: inset 0 0 0 1px #0078d7; + } + #modal-update .footer .btn.btn-primary:hover { background-color: #e5f1fb; border-color: #1084d9; } + @media (max-width: 560px) { + #modal-update .update-meta { grid-template-columns: 1fr; } + #modal-update .update-notes { height: 200px; } + } + .hotkey-toolbar { + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + } + .hotkey-toolbar .btn { + height: 22px; + padding: 0 8px; + background: #e1e1e1; + border: 1px solid #adadad; + border-radius: 3px; + font-family: inherit; + font-size: 9pt; + } + .hotkey-list { + flex: 1; + overflow: auto; + border: 1px solid #d0d0d0; + background: white; + padding: 4px; + display: grid; + grid-template-columns: 1fr; + gap: 4px; + } + body.dark-mode .hotkey-list { + background: #0f1012; + border-color: #1f2124; + } + body.dark-mode .hotkey-row { + border-bottom-color: #1f2124; + } + .hotkey-row { + display: grid; + grid-template-columns: 1.2fr 1fr 1fr; + gap: 6px; + align-items: center; + border-bottom: 1px solid #eee; + padding-bottom: 4px; + } + .hotkey-label { color: #222; } + /* Clean, consistent sliders */ + input[type="range"] { + -webkit-appearance: none; + width: 100%; + height: 18px; + background: transparent; + } + input[type="range"]:focus { outline: none; } + input[type="range"]::-webkit-slider-runnable-track { + height: 4px; + background: #d6d6d6; + border-radius: 2px; + border: none; + } + input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 16px; + border-radius: 2px; + background: var(--win-blue); + border: none; + margin-top: -6px; + } + input[type="range"]::-moz-range-track { + height: 4px; + background: #d6d6d6; + border-radius: 2px; + border: none; + } + input[type="range"]::-moz-range-progress { + height: 4px; + background: #bcdcff; + border-radius: 2px; + } + input[type="range"]::-moz-range-thumb { + width: 12px; + height: 16px; + border-radius: 2px; + background: var(--win-blue); + border: none; + } + body.dark-mode input[type="range"]::-webkit-slider-runnable-track, + body.dark-mode input[type="range"]::-moz-range-track { + background: #2b2d31; + } + body.dark-mode input[type="range"]::-moz-range-progress { + background: #2f5f8f; + } + /* ═══════════════════════════════════════════════════════════════ + TEST MODE — proper dark theme + ═══════════════════════════════════════════════════════════════ */ + body.test-mode { + /* core surface scale — darkest to lightest */ + --tm-s0: #141414; /* deepest bg */ + --tm-s1: #1c1c1c; /* app bg */ + --tm-s2: #222222; /* titlebar / tab row */ + --tm-s3: #272727; /* ribbon */ + --tm-s4: #2e2e2e; /* panels / section bg */ + --tm-s5: #363636; /* hover bg */ + --tm-s6: #3e3e3e; /* active / selected */ + --tm-s7: #484848; /* borders */ + --tm-s8: #585858; /* lighter borders */ + /* text */ + --tm-t1: #e8e8e8; /* primary text */ + --tm-t2: #999999; /* secondary / labels */ + --tm-t3: #666666; /* muted */ + /* accent — neutral warm silver */ + --tm-a1: #a8a8a8; /* icons, highlights */ + --tm-a2: #505050; /* primary-button bg */ + --tm-hover-bg: rgba(255,255,255,0.06); + --tm-hover-border: rgba(255,255,255,0.18); + --tm-selected-bg: rgba(255,255,255,0.09); + --tm-selected-border: rgba(255,255,255,0.26); + } + + /* ── root variable overrides ── */ + body.test-mode { + --bg-ribbon: var(--tm-s3); + --border-ribbon: var(--tm-s7); + --app-text-color: var(--tm-t1); + --app-bg: var(--tm-s1); + --titlebar-bg: var(--tm-s2); + --titlebar-action-icon: var(--tm-t2); + --titlebar-action-hover-bg: var(--tm-s5); + --titlebar-separator: var(--tm-s7); + --titlebar-filename: var(--tm-t1); + --titlebar-control-icon: var(--tm-t1); + --tab-row-bg: var(--tm-s3); + --section-divider: var(--tm-s7); + --section-title-color: var(--tm-t2); + --btn-hover-bg: var(--tm-hover-bg); + --btn-hover-border: var(--tm-hover-border); + --btn-hover-shadow: rgba(255,255,255,0.06); + --btn-active-bg: var(--tm-selected-bg); + --btn-active-border: var(--tm-selected-border); + --btn-active-shadow: rgba(255,255,255,0.10); + --dropdown-bg: var(--tm-s4); + --dropdown-border: var(--tm-s8); + --dropdown-shadow: rgba(0,0,0,0.5); + --dropdown-item-hover-bg: var(--tm-s5); + --dropdown-arrow-color: var(--tm-t2); + --dropdown-separator: var(--tm-s7); + --btn-small-bg: var(--tm-s4); + --btn-small-border: var(--tm-s7); + --threshold-label-color: var(--tm-t2); + --threshold-values-color: var(--tm-t1); + --toggle-icon-color: var(--tm-a1); + --toggle-status-color: var(--tm-t2); + --hotkeys-icon-color: var(--tm-a1); + --anchor-toggle-icon-color: var(--tm-a1); + --theme-mode-toggle-icon-color: var(--tm-a1); + --select-menu-header-color: var(--tm-t2); + --select-menu-header-bg: var(--tm-s4); + --select-menu-icon-all-fill: rgba(255,255,255,0.10); + --select-menu-icon-all-fill-disabled: var(--tm-s5); + --select-menu-icon-disabled-stroke: var(--tm-t3); + --theme-colors-window-bg: var(--tm-s3); + --theme-colors-titlebar-bg-start: var(--tm-s4); + --theme-colors-titlebar-bg-end: var(--tm-s3); + --theme-colors-titlebar-border: var(--tm-s7); + --theme-colors-top-note-color: var(--tm-t2); + --theme-colors-result-count-color: var(--tm-t2); + --theme-colors-list-bg: var(--tm-s3); + --theme-colors-editor-panel-bg-start: var(--tm-s4); + --theme-colors-editor-panel-bg-end: var(--tm-s3); + --theme-colors-editor-panel-border: var(--tm-s7); + --theme-colors-editor-title-color: var(--tm-t1); + --theme-colors-editor-key-color: var(--tm-t1); + --theme-colors-editor-input-bg: var(--tm-s5); + --theme-colors-editor-input-border: var(--tm-s8); + --theme-colors-editor-swatch-border: var(--tm-s8); + --theme-colors-editor-hint-color: var(--tm-t3); + --theme-colors-mini-toggle-color: var(--tm-t1); + --palette-mini-swatch-border: #050505; + --palette-mini-swatch-inner: #ffffff; + --palette-mini-swatch-hover-border: var(--tm-a1); + --palette-mini-swatch-hover-inner: rgba(255,255,255,0.12); + --view-center-icon-color: var(--tm-a1); + --view-edgeclean-icon-color: var(--tm-a1); + --view-theme-icon-color: var(--tm-a1); + --select-icon-free-color: var(--tm-a1); + --select-icon-poly-main-color: var(--tm-a1); + --select-icon-poly-accent-color: #c0c0c0; + --bg-hover: var(--tm-hover-bg); + --border-hover: var(--tm-hover-border); + --bg-selected: var(--tm-selected-bg); + --border-selected: var(--tm-selected-border); + --bg-canvas: #3c3c3c; + --win-blue: var(--tm-a1); + } + + /* ── body + core layout ── */ + body.test-mode { background: var(--tm-s1); color: var(--tm-t1); } + + /* ── title bar ── */ + body.test-mode #title-bar { background: var(--tm-s2); border-bottom: none; } + body.test-mode .tab-row { background: var(--tm-s3); border-bottom-color: var(--tm-s7); border-top: 1px solid var(--tm-s1); color: var(--tm-t1); } + body.test-mode .tab { color: var(--tm-t2); border-color: transparent; } + body.test-mode .tab:not(.file):not(.active):hover { color: var(--tm-t1); background: var(--tm-s5); border-color: var(--tm-s7); } + body.test-mode .tab.active { background: var(--tm-s3); border-color: var(--tm-s7); border-bottom-color: var(--tm-s3); color: var(--tm-t1); } + body.test-mode .tab-row .dd-arrow { color: var(--tm-t1); } + body.test-mode .tab.file { background: var(--tm-s6); color: var(--tm-t1); border-color: var(--tm-s8); } + body.test-mode .tab.file:hover { background: var(--tm-s7); } + + /* ── ribbon ── */ + body.test-mode .ribbon { background: var(--tm-s3); border-bottom: 1px solid var(--tm-s7); } + body.test-mode .section { border-right-color: var(--tm-s7); } + body.test-mode .section-title { color: var(--tm-t3); } + body.test-mode .btn-large { color: var(--tm-t1); } + body.test-mode .btn-large:hover { background: var(--tm-s5); border-color: var(--tm-s7); } + body.test-mode .btn-large.active, + body.test-mode .btn-large.is-on { background: var(--tm-selected-bg); border-color: var(--tm-selected-border); } + body.test-mode .toggle-status { color: var(--tm-t2); } + body.test-mode .toggle-btn .toggle-icons { color: var(--tm-a1); } + body.test-mode #anchor-toggle-btn .toggle-icons { color: var(--tm-a1); } + body.test-mode #theme-mode-btn .toggle-icons { color: var(--tm-a1); } + body.test-mode #test-mode-btn .toggle-icons { color: var(--tm-a1); } + body.test-mode svg [fill="#0078d7"] { fill: var(--tm-a1); } + body.test-mode svg [stroke="#0078d7"] { stroke: var(--tm-a1); } + + /* ribbon card */ + body.test-mode .ribbon-card { background: var(--tm-s4); border-color: var(--tm-s7); color: var(--tm-t1); } + body.test-mode .ribbon-card .card-label { color: var(--tm-t1); } + body.test-mode .ribbon-card .mini-label { color: var(--tm-t2); } + body.test-mode .compact-input { background: var(--tm-s4); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode .ribbon-card.tile-compact .compact-input { background: var(--tm-s4); border-bottom-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode .ribbon-card.tile-compact .compact-input:focus { border-bottom-color: var(--tm-a1); } + body.test-mode .ribbon-card .compact-input:focus { border-bottom-color: var(--tm-a1); } + body.test-mode #pen-size-input { background: var(--tm-s4); border-bottom-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #pen-size-input:focus { border-bottom-color: var(--tm-a1); } + body.test-mode #size-control-wrap label { color: var(--tm-t2) !important; } + + /* ── dropdown menus ── */ + body.test-mode .dropdown-panel, + body.test-mode .paint-dropdown { background: var(--tm-s4); border-color: var(--tm-s7); box-shadow: 0 4px 16px rgba(0,0,0,0.5); } + body.test-mode .dropdown-item, + body.test-mode .paint-dropdown-item { color: var(--tm-t1); } + body.test-mode .dropdown-item:hover, + body.test-mode .paint-dropdown-item:hover { background: var(--tm-s5); } + body.test-mode .dropdown-separator { background: var(--tm-s7); } + + /* ── modals — shared window chrome ── */ + body.test-mode .modal-mask { background: rgba(0,0,0,0.55); } + body.test-mode #modal-resize .window, + body.test-mode #modal-depth .window, + body.test-mode #modal-export .window, + body.test-mode #modal-huesat .window, + body.test-mode #modal-hotkeys .window, + body.test-mode #modal-info .window, + body.test-mode #modal-colors .window, + body.test-mode #modal-confirm-reset .window, + body.test-mode #modal-toolbar .window, + body.test-mode #modal-wincolor .window, + body.test-mode #modal-tool-customizer .window, + body.test-mode .modal-window { + background: var(--tm-s3); + border-color: var(--tm-s7); + color: var(--tm-t1); + box-shadow: 0 12px 40px rgba(0,0,0,0.6); + } + + /* modal title bars */ + body.test-mode #modal-resize .title-bar, + body.test-mode #modal-depth .title-bar, + body.test-mode #modal-export .title-bar, + body.test-mode #modal-huesat .title-bar, + body.test-mode #modal-hotkeys .title-bar, + body.test-mode #modal-info .title-bar, + body.test-mode #modal-colors .title-bar, + body.test-mode #modal-confirm-reset .title-bar, + body.test-mode #modal-toolbar .title-bar, + body.test-mode #modal-wincolor .title-bar, + body.test-mode #modal-tool-customizer .title-bar { + background: var(--tm-s4); + border-bottom-color: var(--tm-s7); + color: var(--tm-t1); + } + body.test-mode .close-btn { color: var(--tm-t2); } + body.test-mode .close-btn:hover { background: #c42b1c; color: #fff; } + + /* modal footers */ + body.test-mode #modal-resize .footer, + body.test-mode #modal-depth .footer, + body.test-mode #modal-export .footer, + body.test-mode #modal-huesat .footer, + body.test-mode #modal-hotkeys .footer, + body.test-mode #modal-colors .footer, + body.test-mode #modal-tool-customizer .footer { + border-top-color: var(--tm-s7); + } + + /* modal buttons */ + body.test-mode .modal-mask .btn, + body.test-mode .modal-mask button.btn { + background: var(--tm-s5); + border-color: var(--tm-s8); + color: var(--tm-t1); + } + body.test-mode .modal-mask .btn:hover { background: var(--tm-s6); border-color: var(--tm-s8); } + body.test-mode .modal-mask .btn-primary { background: var(--tm-s6); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode .modal-mask .btn-primary:hover { background: var(--tm-s7); border-color: var(--tm-a1); } + body.test-mode .modal-mask .btn-ok { background: var(--tm-s6); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode .modal-mask .btn-ok:hover { background: var(--tm-s7); border-color: var(--tm-a1); } + + /* modal inputs */ + body.test-mode .modal-mask input[type="text"], + body.test-mode .modal-mask input[type="number"], + body.test-mode .modal-mask select, + body.test-mode .modal-mask textarea { + background: var(--tm-s5); + border-color: var(--tm-s8); + color: var(--tm-t1); + } + body.test-mode .modal-mask input[type="text"]:focus, + body.test-mode .modal-mask input[type="number"]:focus, + body.test-mode .modal-mask select:focus { border-color: var(--tm-a1); outline: none; } + body.test-mode .modal-mask label, + body.test-mode .modal-mask .row label { color: var(--tm-t1); } + body.test-mode .modal-mask fieldset { border-color: var(--tm-s7); } + body.test-mode .modal-mask legend { color: var(--tm-t2); } + body.test-mode .modal-mask .section-header { color: var(--tm-t2); border-bottom-color: var(--tm-s7); } + + /* ── modal-depth specific ── */ + body.test-mode #modal-depth { --win-bg: var(--tm-s3); --win-text: var(--tm-t1); --input-border: var(--tm-s8); --input-focus: var(--tm-a1); --btn-border: var(--tm-s8); --btn-hover: var(--tm-s5); } + body.test-mode #modal-depth .btn { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-depth .btn:hover { background: var(--tm-s6); border-color: var(--tm-a1); } + body.test-mode #modal-depth .btn-primary { background: var(--tm-s6); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-depth .btn-primary:hover { background: var(--tm-s7); border-color: var(--tm-a1); } + body.test-mode #modal-depth .depth-radio { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-depth .depth-color-list input[type="radio"]:checked + .depth-radio { background: var(--tm-s6); border-color: var(--tm-a1); color: var(--tm-t1); } + body.test-mode #modal-depth .depth-group-label { color: var(--tm-t2); } + + /* ── modal-huesat specific ── */ + body.test-mode #modal-huesat { --win-bg: var(--tm-s3); --win-text: var(--tm-t1); --input-border: var(--tm-s8); --input-focus: var(--tm-a1); --btn-border: var(--tm-s8); --btn-hover: var(--tm-s5); } + body.test-mode #modal-huesat .channel-area { --hs-area-bg: var(--tm-s4); background: var(--tm-s4); border-color: var(--tm-s7); } + body.test-mode #modal-huesat .channel-label { color: var(--tm-t2); } + body.test-mode #modal-huesat .channel-btn { filter: brightness(0.9); } + body.test-mode #modal-huesat .channel-wrap.active { background: transparent; } + body.test-mode #modal-huesat .master-btn { background: var(--tm-s6); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-huesat .master-btn.active { background: var(--tm-t1); color: var(--tm-s1); border-color: var(--tm-t1); } + body.test-mode #modal-huesat .preset-row { background: var(--tm-s4); border-color: var(--tm-s7); } + body.test-mode #modal-huesat .preset-row label { color: var(--tm-t2); } + + /* ── modal-resize specific ── */ + body.test-mode #modal-resize { --win-bg: var(--tm-s3); --win-text: var(--tm-t1); } + body.test-mode #modal-resize .col-label { color: var(--tm-t1); } + + + /* ── modal-export specific ── */ + body.test-mode #modal-export { --win-bg: var(--tm-s3); --input-border: var(--tm-s8); --input-focus: var(--tm-a1); --btn-border: var(--tm-s8); --btn-hover: var(--tm-s5); } + body.test-mode #modal-export .export-format-btn { background: var(--tm-s5); border-color: var(--tm-s7); color: var(--tm-t1); } + body.test-mode #modal-export .export-format-btn.active { background: var(--tm-s6); border-color: var(--tm-a1); color: var(--tm-t1); } + + /* ── modal-colors (theme colors) specific ── */ + body.test-mode #modal-colors .window { border-color: var(--tm-s7); } + body.test-mode #modal-colors .close-btn { color: var(--tm-t1); } + body.test-mode #modal-colors .select-hint { color: var(--tm-t2); } + body.test-mode #modal-colors .color-list { border-color: var(--tm-s7); } + body.test-mode #modal-colors .color-item:hover { background: var(--tm-s5); border-color: var(--tm-s7); } + body.test-mode #modal-colors .color-item.is-active { background: var(--tm-selected-bg); border-color: var(--tm-selected-border); } + body.test-mode #modal-colors .color-label { color: var(--tm-t1); } + body.test-mode #modal-colors .color-element-label { color: var(--tm-t2); } + body.test-mode #modal-colors .color-input { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-colors .color-input.invalid { border-color: #e81123; background: rgba(232,17,35,0.15); } + body.test-mode #modal-colors .preset-select, + body.test-mode #modal-colors .preset-input, + body.test-mode #modal-colors .search-input { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-colors .btn { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-colors .btn:hover { background: var(--tm-s6); border-color: var(--tm-a1); color: var(--tm-t1); } + body.test-mode #modal-colors .editor-grid label { color: var(--tm-t2); } + body.test-mode #modal-colors .editor-grid input[type="number"] { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-colors .color-item { border-bottom-color: var(--tm-s7); } + body.test-mode #modal-colors .color-label { color: var(--tm-t1); } + body.test-mode #modal-colors .color-element-label { color: var(--tm-t2); } + body.test-mode #modal-colors .preset-row { background: var(--tm-s4); border-color: var(--tm-s7); } + body.test-mode #modal-colors .preset-select { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-colors .preset-input { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + + /* ── modal-hotkeys ── */ + body.test-mode .hotkey-list { background: var(--tm-s4); border-color: var(--tm-s7); } + body.test-mode .hotkey-row { border-bottom-color: var(--tm-s7); } + body.test-mode .hotkey-label { color: var(--tm-t1); } + body.test-mode .hotkeys-icon { color: var(--tm-a1); } + + /* ── modal-info ── */ + body.test-mode #modal-info .info-section { border-bottom-color: var(--tm-s7); } + body.test-mode #modal-info .info-title { color: var(--tm-a1); } + body.test-mode #modal-info li, + body.test-mode #modal-info p { color: var(--tm-t1); } + body.test-mode #modal-info .link-btn { color: var(--tm-a1); } + + /* ── file menu ── */ + body.test-mode #file-menu.file-menu-template { background: var(--tm-s3); border-color: var(--tm-s7); box-shadow: 2px 4px 16px rgba(0,0,0,0.5); } + body.test-mode #file-menu .file-menu-left { background: var(--tm-s4); border-right-color: var(--tm-s7); } + body.test-mode #file-menu .file-menu-item { color: var(--tm-t1); } + body.test-mode #file-menu .file-menu-item:hover { background: var(--tm-s5); border-color: var(--tm-s7); } + body.test-mode #file-menu .file-menu-separator { background: var(--tm-s7); } + body.test-mode #file-menu .file-menu-right { background: var(--tm-s3); } + body.test-mode #file-menu .file-menu-recent-toggle { background: var(--tm-s4); border-color: var(--tm-s7); color: var(--tm-t2); } + body.test-mode #file-menu .file-menu-recent-toggle:hover { background: var(--tm-s5); color: var(--tm-t1); } + body.test-mode #file-menu.file-menu-template.recent-collapsed .file-menu-recent-toggle { border-right: 1px solid var(--tm-s7); border-left: none; } + body.test-mode #file-menu .file-menu-recent-header { color: var(--tm-t1); border-bottom-color: var(--tm-s7); } + body.test-mode #file-menu .file-menu-recent-item { color: var(--tm-t2); } + body.test-mode #file-menu .file-menu-recent-item:hover { background: var(--tm-s5); color: var(--tm-t1); } + + /* ── select menu ── */ + body.test-mode .select-menu { background: var(--tm-s4); border-color: var(--tm-s7); box-shadow: 0 4px 16px rgba(0,0,0,0.5); } + body.test-mode .select-menu-header { background: var(--tm-s4); color: var(--tm-t2); border-bottom-color: var(--tm-s7); } + body.test-mode .select-menu-item { color: var(--tm-t1); } + body.test-mode .select-menu-item:hover { background: var(--tm-s5); } + + /* ── sliders ── */ + body.test-mode input[type="range"]::-webkit-slider-runnable-track, + body.test-mode input[type="range"]::-moz-range-track { background: var(--tm-s6); } + body.test-mode input[type="range"]::-moz-range-progress { background: var(--tm-a2); } + body.test-mode input[type="range"]::-webkit-slider-thumb { background: var(--tm-a1); } + body.test-mode input[type="range"]::-moz-range-thumb { background: var(--tm-a1); } + + /* ── save reminder / hotkey panel ── */ + body.test-mode #save-reminder-modal .window, + body.test-mode #close-confirm-modal .window { background: var(--tm-s3); border-color: var(--tm-s7); color: var(--tm-t1); } + + /* ── layer panel ── */ + body.test-mode #lsys-panel { background: var(--tm-s3); border-color: var(--tm-s7); } + body.test-mode #lsys-list { background: var(--tm-s4); } + body.test-mode .lsys-layer { background: var(--tm-s4); border-bottom-color: var(--tm-s7); color: var(--tm-t1); } + body.test-mode .lsys-layer.active { background: var(--tm-s5); } + body.test-mode .lsys-layer-name { color: var(--tm-t1); } + body.test-mode #lsys-toolbar { background: var(--tm-s3); border-top-color: var(--tm-s7); } + body.test-mode .lsys-btn { color: var(--tm-t1); } + body.test-mode .lsys-btn:hover { background: var(--tm-s5); } + + /* ── status bar / bottom bar ── */ + body.test-mode #status-bar, + body.test-mode .status-bar { background: var(--tm-s2); border-top-color: var(--tm-s7); color: var(--tm-t2); } + + /* ── palette area ── */ + body.test-mode .mini-swatch.empty { box-shadow: none; } + body.test-mode #palette-bar, + body.test-mode .palette-bar { background: var(--tm-s3); border-top-color: var(--tm-s7); } + + /* ── modal-wincolor ── */ + body.test-mode #modal-wincolor .window { background: var(--tm-s3); border-color: var(--tm-s7); } + body.test-mode #modal-wincolor .tab-btn { background: var(--tm-s5); border-color: var(--tm-s7); color: var(--tm-t1); } + body.test-mode #modal-wincolor .tab-btn.active { background: var(--tm-s6); border-color: var(--tm-a1); color: var(--tm-t1); } + + /* ── swap-colors button (inline styles override via specificity) ── */ + body.test-mode #swap-colors-btn { background: var(--tm-s4) !important; border-color: var(--tm-s7) !important; color: var(--tm-t1); } + body.test-mode #swap-colors-btn:hover { background: var(--tm-s5) !important; border-color: var(--tm-s8) !important; } + + /* ════════════════════════════════════════════════════════════════ + * DARK REFINED MODE — lighter, purple-grey tinted dark theme + * Ribbon: #43434A | Title bar: #1F1F22 + * ════════════════════════════════════════════════════════════════ */ + body.dark-refined-mode { + /* core surface scale */ + --dr-s0: #17171A; /* deepest bg */ + --dr-s1: #1A1A1E; /* app bg */ + --dr-s2: #1F1F22; /* titlebar */ + --dr-s3: #43434A; /* ribbon / tab row */ + --dr-s4: #4E4E56; /* panels / section bg */ + --dr-s5: #56565F; /* hover bg */ + --dr-s6: #5E5E68; /* active / selected */ + --dr-s7: #6A6A75; /* borders */ + --dr-s8: #7A7A86; /* lighter borders */ + /* text */ + --dr-t1: #EAEAEA; /* primary text */ + --dr-t2: #A0A0AE; /* secondary / labels */ + --dr-t3: #72727E; /* muted */ + /* accent — softer blue */ + --dr-a1: #6AADEE; /* links, highlights */ + --dr-a2: #2F68A4; /* active borders */ + --dr-hover-bg: rgba(106,173,238,0.10); + --dr-hover-border: rgba(106,173,238,0.30); + --dr-selected-bg: rgba(106,173,238,0.15); + --dr-selected-border: rgba(106,173,238,0.40); + } + + /* ── root variable overrides ── */ + body.dark-refined-mode { + --bg-ribbon: var(--dr-s3); + --border-ribbon: var(--dr-s7); + --app-text-color: var(--dr-t1); + --app-bg: var(--dr-s1); + --titlebar-bg: var(--dr-s2); + --titlebar-action-icon: var(--dr-t2); + --titlebar-action-hover-bg: var(--dr-s5); + --titlebar-separator: var(--dr-s7); + --titlebar-filename: var(--dr-t1); + --titlebar-control-icon: var(--dr-t1); + --tab-row-bg: var(--dr-s3); + --section-divider: var(--dr-s7); + --section-title-color: var(--dr-t2); + --btn-hover-bg: var(--dr-hover-bg); + --btn-hover-border: var(--dr-hover-border); + --btn-hover-shadow: rgba(106,173,238,0.15); + --btn-active-bg: var(--dr-selected-bg); + --btn-active-border: var(--dr-selected-border); + --btn-active-shadow: rgba(106,173,238,0.25); + --dropdown-bg: var(--dr-s4); + --dropdown-border: var(--dr-s8); + --dropdown-shadow: rgba(0,0,0,0.45); + --dropdown-item-hover-bg: var(--dr-s5); + --dropdown-arrow-color: var(--dr-t2); + --dropdown-separator: var(--dr-s7); + --btn-small-bg: var(--dr-s4); + --btn-small-border: var(--dr-s7); + --threshold-label-color: var(--dr-t2); + --threshold-values-color: var(--dr-t1); + --toggle-icon-color: var(--dr-a1); + --toggle-status-color: var(--dr-t2); + --hotkeys-icon-color: var(--dr-a1); + --anchor-toggle-icon-color: var(--dr-a1); + --theme-mode-toggle-icon-color: var(--dr-a1); + --select-menu-header-color: var(--dr-t2); + --select-menu-header-bg: var(--dr-s4); + --select-menu-icon-all-fill: rgba(106,173,238,0.18); + --select-menu-icon-all-fill-disabled: var(--dr-s5); + --select-menu-icon-disabled-stroke: var(--dr-t3); + --theme-colors-window-bg: var(--dr-s3); + --theme-colors-titlebar-bg-start: var(--dr-s4); + --theme-colors-titlebar-bg-end: var(--dr-s3); + --theme-colors-titlebar-border: var(--dr-s7); + --theme-colors-top-note-color: var(--dr-t2); + --theme-colors-result-count-color: var(--dr-t2); + --theme-colors-list-bg: var(--dr-s3); + --theme-colors-editor-panel-bg-start: var(--dr-s4); + --theme-colors-editor-panel-bg-end: var(--dr-s3); + --theme-colors-editor-panel-border: var(--dr-s7); + --theme-colors-editor-title-color: var(--dr-t1); + --theme-colors-editor-key-color: var(--dr-t1); + --theme-colors-editor-input-bg: var(--dr-s5); + --theme-colors-editor-input-border: var(--dr-s8); + --theme-colors-editor-swatch-border: var(--dr-s8); + --theme-colors-editor-hint-color: var(--dr-t3); + --theme-colors-mini-toggle-color: var(--dr-t1); + --palette-mini-swatch-border: #050507; + --palette-mini-swatch-inner: #ffffff; + --palette-mini-swatch-hover-border: var(--dr-a1); + --palette-mini-swatch-hover-inner: rgba(106,173,238,0.2); + --view-center-icon-color: var(--dr-a1); + --view-edgeclean-icon-color: var(--dr-a1); + --view-theme-icon-color: var(--dr-a1); + --select-icon-free-color: var(--dr-a1); + --select-icon-poly-main-color: var(--dr-a1); + --select-icon-poly-accent-color: #80c0f4; + --bg-hover: var(--dr-hover-bg); + --border-hover: var(--dr-hover-border); + --bg-selected: var(--dr-selected-bg); + --border-selected: var(--dr-selected-border); + --bg-canvas: #4a4a52; + --win-blue: var(--dr-a1); + } + + /* ── body + core layout ── */ + body.dark-refined-mode { background: var(--dr-s1); color: var(--dr-t1); } + + /* ── title bar ── */ + body.dark-refined-mode #title-bar { background: var(--dr-s2); border-bottom: none; } + body.dark-refined-mode .tab-row { background: var(--dr-s3); border-bottom-color: var(--dr-s7); border-top: 1px solid var(--dr-s2); color: var(--dr-t1); } + body.dark-refined-mode .tab { color: var(--dr-t2); border-color: transparent; } + body.dark-refined-mode .tab:not(.file):not(.active):hover { color: var(--dr-t1); background: var(--dr-s5); border-color: var(--dr-s7); } + body.dark-refined-mode .tab.active { background: var(--dr-s3); border-color: var(--dr-s7); border-bottom-color: var(--dr-s3); color: var(--dr-t1); } + body.dark-refined-mode .tab-row .dd-arrow { color: var(--dr-t1); } + + /* ── ribbon ── */ + body.dark-refined-mode .ribbon { background: var(--dr-s3); border-bottom: 1px solid var(--dr-s7); } + body.dark-refined-mode .section { border-right-color: var(--dr-s7); } + body.dark-refined-mode .section-title { color: var(--dr-t3); } + body.dark-refined-mode .btn-large { color: var(--dr-t1); } + body.dark-refined-mode .btn-large:hover { background: var(--dr-s5); border-color: var(--dr-s7); } + body.dark-refined-mode .btn-large.active, + body.dark-refined-mode .btn-large.is-on { background: var(--dr-selected-bg); border-color: var(--dr-selected-border); } + body.dark-refined-mode .toggle-status { color: var(--dr-t2); } + body.dark-refined-mode .toggle-btn .toggle-icons { color: var(--dr-a1); } + body.dark-refined-mode #anchor-toggle-btn .toggle-icons { color: var(--dr-a1); } + body.dark-refined-mode #theme-mode-btn .toggle-icons { color: var(--dr-a1); } + body.dark-refined-mode #test-mode-btn .toggle-icons { color: var(--dr-a1); } + body.dark-refined-mode #dark-refined-btn .toggle-icons { color: var(--dr-a1); } + body.dark-refined-mode svg [fill="#0078d7"] { fill: var(--dr-a1); } + body.dark-refined-mode svg [stroke="#0078d7"] { stroke: var(--dr-a1); } + + /* ribbon card */ + body.dark-refined-mode .ribbon-card { background: var(--dr-s4); border-color: var(--dr-s7); color: var(--dr-t1); } + body.dark-refined-mode .ribbon-card .card-label { color: var(--dr-t1); } + body.dark-refined-mode .ribbon-card .mini-label { color: var(--dr-t2); } + body.dark-refined-mode .compact-input { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + + /* ── dropdown menus ── */ + body.dark-refined-mode .dropdown-panel, + body.dark-refined-mode .paint-dropdown { background: var(--dr-s4); border-color: var(--dr-s7); box-shadow: 0 4px 16px rgba(0,0,0,0.45); } + body.dark-refined-mode .dropdown-item, + body.dark-refined-mode .paint-dropdown-item { color: var(--dr-t1); } + body.dark-refined-mode .dropdown-item:hover, + body.dark-refined-mode .paint-dropdown-item:hover { background: var(--dr-s5); } + body.dark-refined-mode .dropdown-separator { background: var(--dr-s7); } + + /* ── modals — shared window chrome ── */ + body.dark-refined-mode .modal-mask { background: rgba(0,0,0,0.50); } + body.dark-refined-mode #modal-resize .window, + body.dark-refined-mode #modal-depth .window, + body.dark-refined-mode #modal-export .window, + body.dark-refined-mode #modal-huesat .window, + body.dark-refined-mode #modal-hotkeys .window, + body.dark-refined-mode #modal-info .window, + body.dark-refined-mode #modal-colors .window, + body.dark-refined-mode #modal-confirm-reset .window, + body.dark-refined-mode #modal-toolbar .window, + body.dark-refined-mode #modal-wincolor .window, + body.dark-refined-mode #modal-tool-customizer .window, + body.dark-refined-mode .modal-window { + background: var(--dr-s3); + border-color: var(--dr-s7); + color: var(--dr-t1); + box-shadow: 0 12px 40px rgba(0,0,0,0.5); + } + + /* modal title bars */ + body.dark-refined-mode #modal-resize .title-bar, + body.dark-refined-mode #modal-depth .title-bar, + body.dark-refined-mode #modal-export .title-bar, + body.dark-refined-mode #modal-huesat .title-bar, + body.dark-refined-mode #modal-hotkeys .title-bar, + body.dark-refined-mode #modal-info .title-bar, + body.dark-refined-mode #modal-colors .title-bar, + body.dark-refined-mode #modal-confirm-reset .title-bar, + body.dark-refined-mode #modal-toolbar .title-bar, + body.dark-refined-mode #modal-wincolor .title-bar, + body.dark-refined-mode #modal-tool-customizer .title-bar { + background: var(--dr-s4); + border-bottom-color: var(--dr-s7); + color: var(--dr-t1); + } + body.dark-refined-mode .close-btn { color: var(--dr-t2); } + body.dark-refined-mode .close-btn:hover { background: #c42b1c; color: #fff; } + + /* modal footers */ + body.dark-refined-mode #modal-resize .footer, + body.dark-refined-mode #modal-depth .footer, + body.dark-refined-mode #modal-export .footer, + body.dark-refined-mode #modal-huesat .footer, + body.dark-refined-mode #modal-hotkeys .footer, + body.dark-refined-mode #modal-colors .footer, + body.dark-refined-mode #modal-tool-customizer .footer { + border-top-color: var(--dr-s7); + } + + /* modal buttons */ + body.dark-refined-mode .modal-mask .btn, + body.dark-refined-mode .modal-mask button.btn { + background: var(--dr-s5); + border-color: var(--dr-s8); + color: var(--dr-t1); + } + body.dark-refined-mode .modal-mask .btn:hover { background: var(--dr-s6); border-color: var(--dr-a2); } + body.dark-refined-mode .modal-mask .btn-primary { background: var(--dr-a2); border-color: #1a5090; color: #fff; } + body.dark-refined-mode .modal-mask .btn-primary:hover { background: #2070b8; } + body.dark-refined-mode .modal-mask .btn-ok { background: var(--dr-a2); border-color: #1a5090; color: #fff; } + body.dark-refined-mode .modal-mask .btn-ok:hover { background: #2070b8; } + + /* modal inputs */ + body.dark-refined-mode .modal-mask input[type="text"], + body.dark-refined-mode .modal-mask input[type="number"], + body.dark-refined-mode .modal-mask select, + body.dark-refined-mode .modal-mask textarea { + background: var(--dr-s5); + border-color: var(--dr-s8); + color: var(--dr-t1); + } + body.dark-refined-mode .modal-mask input[type="text"]:focus, + body.dark-refined-mode .modal-mask input[type="number"]:focus, + body.dark-refined-mode .modal-mask select:focus { border-color: var(--dr-a1); outline: none; } + body.dark-refined-mode .modal-mask label, + body.dark-refined-mode .modal-mask .row label { color: var(--dr-t1); } + body.dark-refined-mode .modal-mask fieldset { border-color: var(--dr-s7); } + body.dark-refined-mode .modal-mask legend { color: var(--dr-t2); } + body.dark-refined-mode .modal-mask .section-header { color: var(--dr-t2); border-bottom-color: var(--dr-s7); } + + /* modal-depth */ + body.dark-refined-mode #modal-depth { --win-bg: var(--dr-s3); --win-text: var(--dr-t1); --input-border: var(--dr-s8); --input-focus: var(--dr-a1); --btn-border: var(--dr-s8); --btn-hover: var(--dr-s5); } + body.dark-refined-mode #modal-depth .btn { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-depth .btn:hover { background: var(--dr-s6); border-color: var(--dr-a1); } + body.dark-refined-mode #modal-depth .btn-primary { background: var(--dr-a2); border-color: #1a5090; color: #fff; } + body.dark-refined-mode #modal-depth .btn-primary:hover { background: #2070b8; } + body.dark-refined-mode #modal-depth .depth-radio { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-depth .depth-color-list input[type="radio"]:checked + .depth-radio { background: var(--dr-a2); border-color: var(--dr-a1); color: #fff; } + body.dark-refined-mode #modal-depth .depth-group-label { color: var(--dr-t2); } + + /* modal-huesat */ + body.dark-refined-mode #modal-huesat { --win-bg: var(--dr-s3); --win-text: var(--dr-t1); --input-border: var(--dr-s8); --input-focus: var(--dr-a1); --btn-border: var(--dr-s8); --btn-hover: var(--dr-s5); } + body.dark-refined-mode #modal-huesat .channel-area { --hs-area-bg: var(--dr-s4); background: var(--dr-s4); border-color: var(--dr-s7); } + body.dark-refined-mode #modal-huesat .channel-label { color: var(--dr-t2); } + body.dark-refined-mode #modal-huesat .channel-btn { filter: brightness(0.9); } + body.dark-refined-mode #modal-huesat .channel-wrap.active { background: transparent; } + body.dark-refined-mode #modal-huesat .master-btn { background: var(--dr-s6); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-huesat .master-btn.active { background: var(--dr-t1); color: var(--dr-s1); border-color: var(--dr-t1); } + body.dark-refined-mode #modal-huesat .preset-row { background: var(--dr-s4); border-color: var(--dr-s7); } + body.dark-refined-mode #modal-huesat .preset-row label { color: var(--dr-t2); } + body.dark-refined-mode #modal-huesat .btn { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-huesat .btn:hover { background: var(--dr-s6); border-color: var(--dr-a1); } + body.dark-refined-mode #modal-huesat .btn-primary { background: var(--dr-a2); border-color: #1a5090; color: #fff; } + body.dark-refined-mode #modal-huesat .btn-primary:hover { background: #2070b8; } + + /* modal-resize */ + body.dark-refined-mode #modal-resize { --win-bg: var(--dr-s3); --win-text: var(--dr-t1); } + body.dark-refined-mode #modal-resize .col-label { color: var(--dr-t1); } + + + /* modal-export */ + body.dark-refined-mode #modal-export { --win-bg: var(--dr-s3); --input-border: var(--dr-s8); --input-focus: var(--dr-a1); --btn-border: var(--dr-s8); --btn-hover: var(--dr-s5); } + body.dark-refined-mode #modal-export .export-format-btn { background: var(--dr-s5); border-color: var(--dr-s7); color: var(--dr-t1); } + body.dark-refined-mode #modal-export .export-format-btn.active { background: var(--dr-a2); border-color: var(--dr-a1); color: #fff; } + + /* modal-colors */ + body.dark-refined-mode #modal-colors .window { border-color: var(--dr-s7); } + body.dark-refined-mode #modal-colors .close-btn { color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .select-hint { color: var(--dr-t2); } + body.dark-refined-mode #modal-colors .color-list { border-color: var(--dr-s7); } + body.dark-refined-mode #modal-colors .color-item:hover { background: var(--dr-s5); border-color: var(--dr-s7); } + body.dark-refined-mode #modal-colors .color-item.is-active { background: var(--dr-selected-bg); border-color: var(--dr-selected-border); } + body.dark-refined-mode #modal-colors .color-label { color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .color-element-label { color: var(--dr-t2); } + body.dark-refined-mode #modal-colors .color-input { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .color-input.invalid { border-color: #e81123; background: rgba(232,17,35,0.15); } + body.dark-refined-mode #modal-colors .preset-select, + body.dark-refined-mode #modal-colors .preset-input, + body.dark-refined-mode #modal-colors .search-input { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .btn { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .btn:hover { background: var(--dr-s6); border-color: var(--dr-a1); color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .editor-grid label { color: var(--dr-t2); } + body.dark-refined-mode #modal-colors .editor-grid input[type="number"] { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .color-item { border-bottom-color: var(--dr-s7); } + body.dark-refined-mode #modal-colors .preset-row { background: var(--dr-s4); border-color: var(--dr-s7); } + body.dark-refined-mode #modal-colors .preset-select { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + body.dark-refined-mode #modal-colors .preset-input { background: var(--dr-s5); border-color: var(--dr-s8); color: var(--dr-t1); } + + /* modal-hotkeys */ + body.dark-refined-mode .hotkey-list { background: var(--dr-s4); border-color: var(--dr-s7); } + body.dark-refined-mode .hotkey-row { border-bottom-color: var(--dr-s7); } + body.dark-refined-mode .hotkey-label { color: var(--dr-t1); } + body.dark-refined-mode .hotkeys-icon { color: var(--dr-a1); } + + /* modal-info */ + body.dark-refined-mode #modal-info .info-section { border-bottom-color: var(--dr-s7); } + body.dark-refined-mode #modal-info .info-title { color: var(--dr-a1); } + body.dark-refined-mode #modal-info li, + body.dark-refined-mode #modal-info p { color: var(--dr-t1); } + body.dark-refined-mode #modal-info .link-btn { color: var(--dr-a1); } + + /* file menu */ + body.dark-refined-mode #file-menu.file-menu-template { background: var(--dr-s3); border-color: var(--dr-s7); box-shadow: 2px 4px 16px rgba(0,0,0,0.45); } + body.dark-refined-mode #file-menu .file-menu-left { background: var(--dr-s4); border-right-color: var(--dr-s7); } + body.dark-refined-mode #file-menu .file-menu-item { color: var(--dr-t1); } + body.dark-refined-mode #file-menu .file-menu-item:hover { background: var(--dr-s5); border-color: var(--dr-s7); } + body.dark-refined-mode #file-menu .file-menu-separator { background: var(--dr-s7); } + body.dark-refined-mode #file-menu .file-menu-right { background: var(--dr-s3); } + body.dark-refined-mode #file-menu .file-menu-recent-toggle { background: var(--dr-s4); border-color: var(--dr-s7); color: var(--dr-t2); } + body.dark-refined-mode #file-menu .file-menu-recent-toggle:hover { background: var(--dr-s5); color: var(--dr-t1); } + body.dark-refined-mode #file-menu.file-menu-template.recent-collapsed .file-menu-recent-toggle { border-right: 1px solid var(--dr-s7); border-left: none; } + body.dark-refined-mode #file-menu .file-menu-recent-header { color: var(--dr-t1); border-bottom-color: var(--dr-s7); } + body.dark-refined-mode #file-menu .file-menu-recent-item { color: var(--dr-t2); } + body.dark-refined-mode #file-menu .file-menu-recent-item:hover { background: var(--dr-s5); color: var(--dr-t1); } + + /* select menu */ + body.dark-refined-mode .select-menu { background: var(--dr-s4); border-color: var(--dr-s7); box-shadow: 0 4px 16px rgba(0,0,0,0.45); } + body.dark-refined-mode .select-menu-header { background: var(--dr-s4); color: var(--dr-t2); border-bottom-color: var(--dr-s7); } + body.dark-refined-mode .select-menu-item { color: var(--dr-t1); } + body.dark-refined-mode .select-menu-item:hover { background: var(--dr-s5); } + + /* sliders */ + body.dark-refined-mode input[type="range"]::-webkit-slider-runnable-track, + body.dark-refined-mode input[type="range"]::-moz-range-track { background: var(--dr-s6); } + body.dark-refined-mode input[type="range"]::-moz-range-progress { background: var(--dr-a2); } + body.dark-refined-mode input[type="range"]::-webkit-slider-thumb { background: var(--dr-a1); } + body.dark-refined-mode input[type="range"]::-moz-range-thumb { background: var(--dr-a1); } + + /* save reminder / close confirm */ + body.dark-refined-mode #save-reminder-modal .window, + body.dark-refined-mode #close-confirm-modal .window { background: var(--dr-s3); border-color: var(--dr-s7); color: var(--dr-t1); } + + /* layer panel */ + body.dark-refined-mode #lsys-panel { background: var(--dr-s3); border-color: var(--dr-s7); } + body.dark-refined-mode #lsys-list { background: var(--dr-s4); } + body.dark-refined-mode .lsys-layer { background: var(--dr-s4); border-bottom-color: var(--dr-s7); color: var(--dr-t1); } + body.dark-refined-mode .lsys-layer.active { background: var(--dr-s5); } + body.dark-refined-mode .lsys-layer-name { color: var(--dr-t1); } + body.dark-refined-mode #lsys-toolbar { background: var(--dr-s3); border-top-color: var(--dr-s7); } + body.dark-refined-mode .lsys-btn { color: var(--dr-t1); } + body.dark-refined-mode .lsys-btn:hover { background: var(--dr-s5); } + + /* status bar */ + body.dark-refined-mode #status-bar, + body.dark-refined-mode .status-bar { background: var(--dr-s2); border-top-color: var(--dr-s7); color: var(--dr-t2); } + + /* palette area */ + body.dark-refined-mode .mini-swatch.empty { box-shadow: none; } + body.dark-refined-mode #palette-bar, + body.dark-refined-mode .palette-bar { background: var(--dr-s3); border-top-color: var(--dr-s7); } + + /* modal-wincolor */ + body.dark-refined-mode #modal-wincolor .window { background: var(--dr-s3); border-color: var(--dr-s7); } + body.dark-refined-mode #modal-wincolor .tab-btn { background: var(--dr-s5); border-color: var(--dr-s7); color: var(--dr-t1); } + body.dark-refined-mode #modal-wincolor .tab-btn.active { background: var(--dr-a2); color: #fff; } + + /* swap-colors button */ + body.dark-refined-mode #swap-colors-btn { background: var(--dr-s4) !important; border-color: var(--dr-s7) !important; color: var(--dr-t1); } + body.dark-refined-mode #swap-colors-btn:hover { background: var(--dr-s5) !important; border-color: var(--dr-s8) !important; } + + /* ── active color slot selection highlight ── */ + body.test-mode .active-color-col.selected { background: var(--tm-selected-bg); border-color: var(--tm-selected-border); } + body.test-mode .active-color-col:not(.selected):hover { background: var(--tm-hover-bg); border-color: var(--tm-hover-border); box-shadow: none; } + + /* ── select tool split button selection highlight ── */ + body.test-mode .split-btn-container.select-split.selection-highlight { border-color: var(--tm-selected-border); } + body.test-mode .split-btn-container.select-split.selection-highlight .split-btn-top, + body.test-mode .split-btn-container.select-split.selection-highlight .split-btn-bottom.select-dropdown { background: var(--tm-selected-bg); } + body.test-mode .split-btn-container.select-split .split-btn-bottom.select-dropdown.selection-highlight { border-top-color: var(--tm-selected-border); } + body.test-mode .split-btn-container.select-split:hover { border-color: var(--tm-hover-border); background: var(--tm-hover-bg); box-shadow: none; } + body.test-mode .split-btn-container.select-split:hover .split-btn-bottom.select-dropdown { border-top-color: var(--tm-hover-border); background: var(--tm-hover-bg); box-shadow: none; } + + /* ── select-menu dropdown (overrides its own local vars) ── */ + body.test-mode #select-menu .icon-free, + body.test-mode #select-menu .icon-poly, + body.test-mode #select-menu .icon-invert { filter: grayscale(1) brightness(1.4); } + body.test-mode #select-menu { --bg-color: var(--tm-s4); --border-outer: var(--tm-s7); --header-text: var(--tm-t2); --text-color: var(--tm-t1); --disabled-text: var(--tm-t3); --separator: var(--tm-s7); --item-hover-bg: var(--tm-s5); --item-hover-border: var(--tm-s8); --item-select-bg: var(--tm-selected-bg); --item-select-border: var(--tm-selected-border); --icon-stroke: var(--tm-a1); --select-menu-icon-delete-stroke: #c06060; box-shadow: 2px 2px 8px rgba(0,0,0,0.5); } + + /* ── depth reduction modal ── */ + body.test-mode #modal-depth .depth-radio { background: var(--tm-s5); border-color: var(--tm-s8); } + body.test-mode #modal-depth #custom-depth-val { background: transparent; color: var(--tm-t1); } + + /* ── hue/sat modal buttons ── */ + body.test-mode #modal-huesat .btn { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-huesat .btn:hover { background: var(--tm-s6); border-color: var(--tm-a1); } + body.test-mode #modal-huesat .btn-primary { background: var(--tm-s6); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-huesat .btn-primary:hover { background: var(--tm-s7); border-color: var(--tm-a1); } + + /* ── modal-colors small buttons ── */ + body.test-mode #modal-colors .btn-small { background: var(--tm-s5); border-color: var(--tm-s8); color: var(--tm-t1); } + body.test-mode #modal-colors .btn-small:hover { background: var(--tm-s6); } + body.test-mode #modal-colors .theme-select-btn.is-on { background: var(--tm-s6); border-color: var(--tm-a1); color: var(--tm-t1); } + + .hotkey-field { + display: flex; + gap: 6px; + align-items: center; + } + .hotkey-input { + width: 120px; + height: 18px; + border: 1px solid #7a7a7a; + padding: 0 4px; + background: #fff; + font-family: inherit; + font-size: 9pt; + } + .hotkey-input.capture { + border-color: #7a7a7a; + box-shadow: none; + } + .hotkey-input-wrap { position: relative; display: inline-block; --capture-progress: 1; } + .hotkey-input-wrap.capture::after { + content: ""; + position: absolute; + inset: -1px; + border: 1px solid #0078d7; + transform-origin: right center; + transform: scaleX(var(--capture-progress)); + pointer-events: none; + } + .hotkey-btn { + height: 18px; + padding: 0 4px; + background: #ffffff; + border: 1px solid #adadad; + border-radius: 2px; + font-size: 9pt; + cursor: pointer; + } + .hotkey-btn.set:hover { background: #0078d7; border-color: var(--toggle-icon-color); color: #ffffff; } + .hotkey-btn.clear:hover { background: #d83b01; border-color: #d83b01; color: #ffffff; } + .hotkey-note { + color: #666; + font-size: 11px; + } + #modal-hotkeys.modal-mask { + align-items: flex-start; + overflow: auto; + padding: 10px 0; + } + #modal-colors .window { + position: relative; + width: 860px; + height: 600px; + display: flex; + flex-direction: column; + background: var(--theme-colors-window-bg); + border: 1px solid #999; + box-shadow: 0 12px 30px rgba(0,0,0,0.22); + pointer-events: auto; + } + #modal-colors.modal-mask { + align-items: center; + justify-content: center; + overflow: auto; + background: transparent; + pointer-events: none; + } + #modal-colors .title-bar { + background: linear-gradient(180deg, var(--theme-colors-titlebar-bg-start) 0%, var(--theme-colors-titlebar-bg-end) 100%); + border-bottom: 1px solid var(--theme-colors-titlebar-border); + height: 30px; + display: flex; + align-items: center; + justify-content: space-between; + padding-left: 10px; + font-size: 12px; + font-weight: 600; + } + #modal-colors .close-btn { + width: 40px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + font-size: 13px; + color: #111; + } + #modal-colors .close-btn:hover { + background-color: #e81123; + color: white; + } + #modal-colors .content { + padding: 10px 12px 0 12px; + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + overflow: hidden; + min-height: 0; + } + #modal-colors .top-note { + font-size: 11px; + color: var(--theme-colors-top-note-color); + } + #modal-colors .search-row { + display: flex; + align-items: center; + gap: 6px; + } + #modal-colors .select-row { + display: flex; + align-items: center; + gap: 6px; + min-height: 22px; + } + #modal-colors .select-hint { + font-size: 11px; + color: #555; + line-height: 1.2; + } + #modal-colors .preset-row { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + } + #modal-colors .actions-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + min-height: 24px; + } + #modal-colors .actions-row .spacer { flex: 1; } + #modal-colors .main-layout { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: minmax(300px, 1fr) 270px; + gap: 10px; + } + #modal-colors .preset-select { + height: 22px; + padding: 0 6px; + border: 1px solid #bdbdbd; + font-size: 11px; + font-family: inherit; + min-width: 200px; + } + #modal-colors .preset-input { + height: 22px; + padding: 0 6px; + border: 1px solid #bdbdbd; + font-size: 11px; + font-family: inherit; + width: 130px; + } + #modal-colors .search-input { + flex: 1; + height: 24px; + padding: 0 6px; + border: 1px solid #bdbdbd; + font-size: 12px; + font-family: inherit; + } + #modal-colors .result-count { + font-size: 11px; + color: var(--theme-colors-result-count-color); + min-width: 56px; + text-align: right; + } + #modal-colors .color-list { + flex: 1; + overflow: auto; + border: 1px solid #d0d0d0; + background: var(--theme-colors-list-bg); + padding: 6px; + min-height: 0; + } + #modal-colors .color-item { + display: flex; + align-items: center; + gap: 4px; + padding: 5px 4px; + font-size: 11px; + flex-wrap: wrap; + border-radius: 4px; + border: 1px solid transparent; + transition: background-color 120ms ease, border-color 120ms ease; + } + #modal-colors .color-item:hover { + background: #f7fbff; + border-color: #d7e7fb; + } + #modal-colors .color-item.is-active { + background: #e8f3ff; + border-color: #93bce8; + } + #modal-colors .color-swatch { + width: 16px; + height: 16px; + border: 1px solid #666; + background: transparent; + flex-shrink: 0; + } + #modal-colors .color-label { + width: 86px; + font-family: Consolas, 'Courier New', monospace; + color: #333; + flex-shrink: 0; + } + #modal-colors .color-element-label { + flex: 1; + color: #555; + min-width: 110px; + } + #modal-colors .color-input { + width: 120px; + height: 20px; + padding: 0 6px; + border: 1px solid #bdbdbd; + font-size: 11px; + font-family: inherit; + background: #f9f9f9; + } + #modal-colors .color-input.invalid { + border-color: #e81123; + background: #fff4f4; + } + #modal-colors .footer { + padding: 8px 10px 10px 10px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 6px; + } + #modal-colors .btn { + height: 22px; + padding: 0 10px; + background-color: #ffffff; + border: 1px solid #adadad; + font-family: inherit; + font-size: 9pt; + cursor: pointer; + border-radius: 2px; + outline: none; + } + #modal-colors .btn:hover { background-color: #e5f1fb; border-color: #0078d7; } + #modal-colors .btn:active { background-color: #cce4f7; border-color: #005499; } + #modal-colors .btn:focus-visible { box-shadow: 0 0 0 2px #0078d7; } + #modal-colors .btn-small { + height: 20px; + padding: 0 6px; + font-size: 10px; + } + #modal-colors .theme-select-btn.is-on { + background-color: var(--toggle-icon-color); + border-color: #005a9e; + color: #fff; + } + #modal-colors .editor-panel { + border: 1px solid var(--theme-colors-editor-panel-border); + background: linear-gradient(180deg, var(--theme-colors-editor-panel-bg-start) 0%, var(--theme-colors-editor-panel-bg-end) 100%); + padding: 8px; + min-height: 0; + display: flex; + flex-direction: column; + gap: 8px; + } + #modal-colors .editor-title { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + color: var(--theme-colors-editor-title-color); + } + #modal-colors .editor-preview { + display: flex; + align-items: center; + gap: 8px; + } + #modal-colors .editor-swatch { + width: 38px; + height: 38px; + border: 1px solid var(--theme-colors-editor-swatch-border); + background: #ffffff; + box-shadow: inset 0 0 0 1px #ffffff; + flex-shrink: 0; + } + #modal-colors .editor-key { + font-size: 11px; + color: var(--theme-colors-editor-key-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + #modal-colors .editor-input { + width: 100%; + height: 24px; + border: 1px solid var(--theme-colors-editor-input-border); + padding: 0 6px; + font-size: 12px; + font-family: Consolas, 'Courier New', monospace; + background: var(--theme-colors-editor-input-bg); + } + #modal-colors .editor-grid { + display: grid; + grid-template-columns: 22px 1fr 44px; + gap: 4px; + align-items: center; + } + #modal-colors .editor-grid label { + font-size: 11px; + color: #555; + text-align: right; + } + #modal-colors .editor-grid input[type="range"] { width: 100%; margin: 0; } + #modal-colors .editor-grid input[type="number"] { + width: 44px; + height: 20px; + border: 1px solid #bdbdbd; + font-size: 11px; + padding: 0 2px; + text-align: right; + background: #fff; + } + #modal-colors .kbd-hint { + font-size: 10px; + color: var(--theme-colors-editor-hint-color); + line-height: 1.35; + } + #modal-colors .mini-toggle { + font-size: 11px; + color: var(--theme-colors-mini-toggle-color); + display: inline-flex; + align-items: center; + gap: 4px; + } + #modal-colors .color-item.is-picked { + background: #eef6ff; + border: 1px solid #9fc4ea; + border-radius: 2px; + } + .color-preview-highlight { + outline: 2px dashed #0078d7 !important; + outline-offset: 2px; + } + body.theme-select-mode #title-bar, + body.theme-select-mode .tab-row, + body.theme-select-mode #ribbon, + body.theme-select-mode #ribbon-view, + body.theme-select-mode #ribbon-themes, + body.theme-select-mode #ribbon-debug, + body.theme-select-mode #canvas-stage { + cursor: crosshair; + } + body.theme-select-mode .theme-select-candidate { + outline: 2px dashed #0078d7 !important; + outline-offset: 2px; + } + + /* Handles Container */ + #selection-controls { position: absolute; z-index: 1000; pointer-events: none; display: none; } + + #sel-hit-area { + position: absolute; + top: 0; left: 0; width: 100%; height: 100%; + background: transparent; + pointer-events: auto; + cursor: move; + z-index: 999; + } + #sel-rot-border { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; + display: none; + z-index: 998; + --rot-border-thickness: 1px; + --rot-border-step: 4px; + background-image: + repeating-linear-gradient(90deg, #ffffff 0, #ffffff calc(var(--rot-border-step) / 2), #1b6fe5 calc(var(--rot-border-step) / 2), #1b6fe5 var(--rot-border-step)), + repeating-linear-gradient(90deg, #ffffff 0, #ffffff calc(var(--rot-border-step) / 2), #1b6fe5 calc(var(--rot-border-step) / 2), #1b6fe5 var(--rot-border-step)), + repeating-linear-gradient(0deg, #ffffff 0, #ffffff calc(var(--rot-border-step) / 2), #1b6fe5 calc(var(--rot-border-step) / 2), #1b6fe5 var(--rot-border-step)), + repeating-linear-gradient(0deg, #ffffff 0, #ffffff calc(var(--rot-border-step) / 2), #1b6fe5 calc(var(--rot-border-step) / 2), #1b6fe5 var(--rot-border-step)); + background-repeat: no-repeat; + background-size: + 100% var(--rot-border-thickness), + 100% var(--rot-border-thickness), + var(--rot-border-thickness) 100%, + var(--rot-border-thickness) 100%; + background-position: + left top, + left calc(100% - var(--rot-border-thickness)), + left top, + calc(100% - var(--rot-border-thickness)) top; + } + #selection-controls.selection-rotated #sel-rot-border { display: block; } + #sel-rot-center { + position: absolute; + left: 0; + top: 0; + width: 6px; + height: 6px; + background: #e81123; + border: 1px solid #ffffff; + border-radius: 50%; + pointer-events: none; + display: none; + z-index: 1001; + box-shadow: 0 0 0 1px rgba(0,0,0,0.25); + } + #selection-controls.selection-rotating #sel-rot-center { display: none; } + #picker-hotspot { + position: fixed; + width: 1px; + height: 1px; + background: #ff0000; + z-index: 10002; + pointer-events: none; + display: none; + } + + + .sel-handle { + position: absolute; + width: 6px; + height: 6px; + background: transparent; + border: none; + pointer-events: auto; + z-index: 1001; + overflow: hidden; + } + .sel-handle img { + width: 100%; + height: 100%; + display: block; + pointer-events: none; + image-rendering: pixelated; + image-rendering: crisp-edges; + -moz-image-rendering: crisp; + } + .sel-rotate-handle { + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 0; + pointer-events: auto; + cursor: grab; + z-index: 1002; + overflow: visible; + image-rendering: pixelated; + -moz-image-rendering: crisp; + } + .sel-rotate-handle:active { cursor: grabbing; } + .sel-rotate-handle.anchor-mode { + background: rgba(232, 17, 35, 0.18); + border-radius: 50%; + outline: 2px solid #e81123; + outline-offset: 1px; + cursor: grab; + } + .sel-rotate-handle.anchor-mode:active { cursor: grabbing; } + #selection-controls.sel-anchor-mode .sel-handle { pointer-events: none; opacity: 0.35; } + #selection-controls.sel-anchor-mode #sel-hit-area { pointer-events: none; cursor: default; } + .sel-rotate-handle img { + width: auto; + height: auto; + display: block; + pointer-events: none; + } + #selection-controls.no-handles .sel-handle, + #selection-controls.no-handles .sel-rotate-handle { display: none; } + #selection-controls.no-handles .path-handle { display: none; } + #selection-controls.no-handles #sel-hit-area { pointer-events: none; } + + .h-nw { top: 0; left: 0; cursor: nwse-resize; } .h-n { top: 0; left: 50%; cursor: ns-resize; } .h-ne { top: 0; left: 100%; cursor: nesw-resize; } + + .h-w { top: 50%; left: 0; cursor: ew-resize; } .h-e { top: 50%; left: 100%; cursor: ew-resize; } .h-sw { top: 100%; left: 0; cursor: nesw-resize; } + .h-s { top: 100%; left: 50%; cursor: ns-resize; } .h-se { top: 100%; left: 100%; cursor: nwse-resize; } + + .creating .sel-handle, + .creating .sel-rotate-handle, + .creating #sel-rot-border { display: none; } + .creating .path-handle { display: none; } + + .path-handle { + position: absolute; + width: 6px; + height: 6px; + background: #3aa0ff; + border: 1px solid #0b3d6d; + box-shadow: 0 0 0 1px rgba(255,255,255,0.6) inset; + pointer-events: auto; + z-index: 1002; + } + .path-handle.ctrl { + background: #ffffff; + border: 1px solid #3aa0ff; + box-shadow: none; + } + #path-handles { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; + } + + + #canvas-resize-handles { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1002; } + #canvas-resize-handles.smooth .resizer { transition: left .24s cubic-bezier(.16,1,.3,1), top .24s cubic-bezier(.16,1,.3,1); } + .resizer { position: absolute; width: 6px; height: 6px; background: white; border: 1px solid black; z-index: 40; pointer-events: auto; } + .r-right { cursor: ew-resize; } + .r-left { cursor: ew-resize; } + .r-top { cursor: ns-resize; } + .r-bottom { cursor: ns-resize; } + .r-corner { cursor: nwse-resize; } + .r-tl { cursor: nwse-resize; } + .r-tr { cursor: nesw-resize; } + .r-bl { cursor: nesw-resize; } + .resizer.free-only { display: none; } + #viewport.free-canvas .resizer.free-only { display: block; } + + /* Common */ + .color-area { display: flex; gap: 6px; height: 60px; align-items: flex-start; } + + .active-color-col { display: flex; flex-direction: column; align-items: center; gap: 0px; cursor: pointer; padding: 4px; border: 1px solid transparent; border-radius: 2px; width: 48px; height: 72px; } + + .active-color-col.selected { background: #cce8ff; border: 1px solid #66a7e8; } + .active-color-col:not(.selected):hover { background: #eaf4ff; border: 1px solid #9fc4ea; box-shadow: inset 0 0 2px rgba(0,120,215,0.15); } + .selection-highlight { background: #cce8ff; border-color: #66a7e8; } + .big-swatch { + width: 30px; + height: 30px; + border: 1px solid #a0a0a0; + box-shadow: inset 0 0 0 1px #ffffff; + background: black; + } + .palette-container { display:flex; flex-direction: column; gap:2px; margin-left: 4px; } + .palette-grid { display: grid; grid-template-columns: repeat(10, 16px); gap: 4px; } + #palette-std { margin-top: 5px; } + .mini-swatch { + width: 16px !important; + height: 16px !important; + background: white; + cursor: pointer; + border: 1px solid var(--palette-mini-swatch-border); + box-shadow: inset 0 0 0 1px var(--palette-mini-swatch-inner); + box-sizing: content-box; + } + .mini-swatch:hover { + border: 1px solid var(--palette-mini-swatch-hover-border); + box-shadow: inset 0 0 0 1px var(--palette-mini-swatch-hover-inner); + } + .mini-swatch.empty { + background: transparent; + pointer-events: none; + } + .mini-swatch.empty:hover { + border: 1px solid var(--palette-mini-swatch-border); + box-shadow: inset 0 0 0 1px var(--palette-mini-swatch-inner); + } + + #status-bar { height: 24px; background: #f0f0f0; border-top: 1px solid #ccc; display: flex; align-items: center; padding: 0 10px; gap: 15px; font-size: 11px; z-index: 9001; } + #status-primary { display: flex; align-items: center; gap: 0; } + .status-segment { + width: 155px; + min-width: 155px; + max-width: 155px; + height: 18px; + padding: 0 6px; + display: flex; + align-items: center; + box-sizing: border-box; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + .status-segment + .status-segment { border-left: 1px solid #c8c8c8; } + #status-right { display: flex; align-items: center; gap: 15px; } + #status-reminder { color: #a40000; font-weight: 600; } + #grid-overlay { + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + display: none; + z-index: 5; + } + #grid-overlay path { + fill: none; + stroke-width: 1px; + stroke-opacity: 1; + vector-effect: non-scaling-stroke; + shape-rendering: crispEdges; + } + #tile-overlay { + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + display: none; + z-index: 4; + } + .gridline-color-swatch { + width: 12px; + height: 12px; + border: 1px solid #666; + display: inline-block; + vertical-align: middle; + margin-left: 4px; + } + .gridline { + fill: none; + vector-effect: non-scaling-stroke; + shape-rendering: crispEdges; + } + #save-reminder-toast { + position: fixed; + right: 12px; + bottom: 36px; + padding: 8px 12px; + background: #fff3cd; + border: 1px solid #d4b106; + color: #5c4700; + font-size: 12px; + font-weight: 600; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + z-index: 12000; + display: none; + pointer-events: none; + } + #save-reminder-modal { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + z-index: 12000; + background: rgba(0,0,0,0.15); + } + #save-reminder-modal .window { + width: 420px; + background: #f2f2f2; + border: 1px solid #b0b0b0; + box-shadow: 0 6px 16px rgba(0,0,0,0.25); + padding: 14px 16px 12px 16px; + position: absolute; + } + #save-reminder-modal .title-bar { + height: 28px; + background: #ffffff; + border-bottom: 1px solid #ccc; + display: flex; + align-items: center; + padding-left: 8px; + margin: -14px -16px 8px -16px; + font-weight: normal; + cursor: move; + } + #save-reminder-modal .close-btn { + margin-left: auto; + width: 32px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + } + #save-reminder-modal .close-btn:hover { + background: #e81123; + color: #fff; + } + #save-reminder-modal .body-text { margin: 8px 0 6px 0; font-size: 15px; padding: 4px 0; color: #8b0000; } + #save-reminder-modal .body-subtext { color: #777; font-size: 11px; } + #save-reminder-modal .btn-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } + #save-reminder-modal .btn-row button { + padding: 4px 12px; + border: 1px solid #b0b0b0; + background: #f5f5f5; + cursor: pointer; + border-radius: 4px; + color: #000; + } + #save-reminder-modal .btn-row button:hover { background: #e6e6e6; } + #save-reminder-modal .btn-primary { background: #0078d7; color: #fff; border: 1px solid #005a9e; } + #save-reminder-modal .btn-primary:hover { background: #0063b1; } + #save-reminder-modal .link-btn { color: #005a9e; text-decoration: underline; cursor: pointer; } + + #close-confirm-modal { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + z-index: 13000; + background: transparent; + } + #close-confirm-modal .window { + width: 350px; + background: #f2f2f2; + border: 1px solid #b0b0b0; + box-shadow: 0 6px 16px rgba(0,0,0,0.25); + padding: 0; + position: absolute; + } + #close-confirm-modal .title-bar { + height: 30px; + background: #eeeeee; + display: flex; + align-items: center; + padding-left: 8px; + font-weight: normal; + } + #close-confirm-modal .close-btn { + margin-left: auto; + width: 32px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + } + #close-confirm-modal .close-btn:hover { + background: #e81123; + color: #fff; + } + #close-confirm-modal .message-area { + height: 61px; + background: #ffffff; + display: flex; + align-items: center; + padding: 0 16px; + } + #close-confirm-modal .body-text { + margin: 0; + font-size: 16px; + color: #0057d8; + font-weight: normal; + } + #close-confirm-modal .separator { + height: 1px; + background: #c8c8c8; + } + #close-confirm-modal .button-area { + background: #eeeeee; + height: 40px; + box-sizing: border-box; + padding: 0 11px 0 12px; + display: flex; + align-items: center; + justify-content: flex-end; + } + #close-confirm-modal .btn-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 0; } + #close-confirm-modal .btn-row button { + height: 21px; + padding: 0 18px; + box-sizing: border-box; + border: 1px solid #b0b0b0; + background: #ffffff; + cursor: pointer; + border-radius: 4px; + color: #000; + font-size: 12px; + font-family: "Segoe UI", Tahoma, sans-serif; + white-space: nowrap; + } + #close-confirm-modal .btn-row button:nth-child(1) { width: 70px; min-width: 70px; max-width: 70px; } + #close-confirm-modal .btn-row button:nth-child(2) { width: 90px; min-width: 90px; max-width: 90px; text-align: center; padding-left: 16px; padding-right: 20px; } + #close-confirm-modal .btn-row button:nth-child(3) { width: 70px; min-width: 70px; max-width: 70px; } + #close-confirm-modal .btn-row button:hover:not(:disabled) { + background-color: #e5f1fb; + border-color: var(--toggle-icon-color); + } + #close-confirm-modal .btn-row button:focus { + outline: 1px solid #2b88d8; + outline-offset: -1px; + } + + #modal-info { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + z-index: 12000; + background: rgba(0,0,0,0.2); + } + #modal-info .window { + width: 720px; + max-width: calc(100vw - 40px); + height: 520px; + max-height: calc(100vh - 40px); + background: #f2f2f2; + border: 1px solid #b0b0b0; + box-shadow: 0 8px 18px rgba(0,0,0,0.25); + display: flex; + flex-direction: column; + } + #modal-info .title-bar { + height: 28px; + background: #ffffff; + border-bottom: 1px solid #ccc; + display: flex; + align-items: center; + padding-left: 10px; + font-size: 12px; + color: #000; + } + #modal-info .close-btn { + margin-left: auto; + width: 32px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + } + #modal-info .close-btn:hover { background: #e81123; color: white; } + #modal-info .content { + padding: 10px 12px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 12px; + font-size: 12px; + color: #222; + } + #modal-info .info-section { + background: #ffffff; + border: 1px solid #d0d0d0; + padding: 8px 10px; + } + #modal-info .info-title { + font-weight: 600; + margin-bottom: 6px; + } + #modal-info .info-list { + margin: 0; + padding-left: 16px; + } + #modal-info .info-list li { margin-bottom: 4px; } + + + .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: transparent; z-index: 10000; display: none; align-items: center; justify-content: center; } + body.theme-colors-open .modal-mask { pointer-events: none; } + body.theme-colors-open .modal-mask .window, + body.theme-colors-open .modal-mask .modal-window { pointer-events: auto; } + + .modal-window { background: white; border: 1px solid var(--win-blue); box-shadow: 0 0 20px rgba(0,0,0,0.3); width: 380px; padding: 15px; } + .modal-btns { display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px; } + button { padding: 4px 15px; cursor: pointer; } + + .resize-options { display: flex; gap: 10px; margin-bottom: 15px; align-items: center; } + .radio-group-row { display: flex; gap: 15px; } + .resize-grid { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; } + .resize-row { display: flex; align-items: center; gap: 10px; } + + .icon-box { width: 32px; height: 32px; border: 1px solid #ccc; display: flex; align-items: center; justify-content: center; background: #f9f9f9; } + .resize-row label { width: 70px; } .resize-row input { width: 60px; } + .resize-footer { margin-top: 10px; padding-top: 5px; border-top: 1px solid #eee; } + + fieldset { border: 1px solid #ccc; margin: 0 0 10px 0; padding: 8px; border-radius: 2px; } + legend { color: var(--win-blue); font-weight: bold; font-size: 11px; } + .radio-group { display: flex; flex-direction: column; gap: 4px; } + .radio-row { display: flex; align-items: center; gap: 6px; cursor: pointer; } + + .advanced-row { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; } + .slider-container { display: flex; align-items: center; gap: 5px; flex: 1; } + + .seed-container { display: flex; align-items: center; gap: 4px; border-left: 1px solid #ccc; padding-left: 8px; margin-left: 8px; } + .small-btn { min-width: 24px; padding: 0 4px; height: 22px; font-size: 11px; } + .disabled { opacity: 0.5; pointer-events: none; } + .form-group { margin-bottom: 10px; } + .form-group label { display: block; font-weight: bold; margin-bottom: 4px; } + .form-group select, .form-group input { width: 100%; padding: 4px; } + .ribbon-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + padding: 4px 6px; + min-width: 130px; + border: 1px solid #d7d7d7; + border-radius: 3px; + background: #fafafa; + } + .ribbon-card.tile-compact { + flex-direction: column; + align-items: center; + padding: 10px 6px; + gap: 3px; + min-width: 160px; + } + .ribbon-card.tile-compact .card-icon svg { width: 18px; height: 18px; } + .ribbon-card.tile-compact .card-label { font-size: 9px; line-height: 1; white-space: nowrap; } + .ribbon-card.tile-compact .card-controls { gap: 3px; } + .ribbon-card.tile-compact .mini-label { font-size: 9px; } + .ribbon-card.tile-compact .compact-input { + width: 34px; + height: 16px; + padding: 0 3px; + border-left: 0; + border-right: 0; + border-top: 0; + border-bottom: 1px solid #b5b5b5; + border-radius: 0; + } + .ribbon-card.tile-compact .compact-input:focus { + outline: none; + border-bottom-color: #1b6fe5; + } + .ribbon-card .card-icon svg { + width: 22px; + height: 22px; + display: block; + } + .ribbon-card .card-label { + font-weight: 600; + color: #333; + font-size: 10px; + } + .ribbon-card .card-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + flex-wrap: nowrap; + } + .ribbon-card .card-controls + .card-controls { + margin-top: 3px; + } + .ribbon-card .mini-label { font-size: 10px; color: #666; } + .ribbon-card .compact-input { + width: 36px; + height: 18px; + padding: 0 4px; + border-left: 0; + border-right: 0; + border-top: 0; + border-bottom: 1px solid #b5b5b5; + border-radius: 0; + text-align: center; + -moz-appearance: textfield; + -webkit-appearance: textfield; + appearance: textfield; + } + .ribbon-card .compact-input:focus { + outline: none; + border-bottom-color: #1b6fe5; + } + .ribbon-card .compact-input[type="number"]::-webkit-inner-spin-button, + .ribbon-card .compact-input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + #pen-size-input { + border-left: 0; + border-right: 0; + border-top: 0; + border-bottom: 1px solid #b5b5b5; + border-radius: 0; + text-align: center; + -moz-appearance: textfield; + -webkit-appearance: textfield; + appearance: textfield; + } + #pen-size-input:focus { + outline: none; + border-bottom-color: #1b6fe5; + } + #pen-size-input::-webkit-inner-spin-button, + #pen-size-input::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + #line-width-menu.line-width-menu { + width: 132px; + height: 166px; + box-sizing: border-box; + border: 1px solid #d9d9d9; + background-color: #ffffff; + padding: 2px 1px; + display: none; + flex-direction: column; + min-width: 132px; + z-index: 10010; + } + #line-width-menu .line-width-item { + width: 128px; + height: 40px; + box-sizing: border-box; + border: 1px solid transparent; + display: flex; + justify-content: center; + align-items: center; + cursor: default; + } + #line-width-menu .line-width-item.selected { + background-color: #eef4fc; + border-color: #a0c5f5; + } + #line-width-menu .line-width-item:hover { + background-color: #cde3fa; + border-color: #72a9ea; + } + #line-width-menu .line-width-preview { + width: 108px; + background-color: #324254; + pointer-events: none; + } + #line-width-menu .line-width-1 { height: 1px; } + #line-width-menu .line-width-2 { height: 2px; } + #line-width-menu .line-width-3 { height: 3px; } + #line-width-menu .line-width-4 { height: 4px; } + .ribbon-card .compact-toggle { display: inline-flex; align-items: center; gap: 3px; font-weight: normal; } + .ribbon-card .compact-btn { padding: 1px 5px; font-size: 10px; } + svg { fill: none; } + #freehand-reopen-btn svg path, + #freehand-collapse-btn svg path, + #paintbrush-collapse-btn svg path, + #pb-reopen-btn svg path, + #project-collapse svg path, + #pb-more-toggle .pb-arrow path { fill: #000000; } + + /* Export Studio Styles */ + .export-palette-grid { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 4px; + margin-top: 4px; + max-height: 180px; + overflow-y: auto; + } + .export-swatch { + aspect-ratio: 1; + border: 1px solid #888; + border-radius: 2px; + cursor: grab; + position: relative; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.4); + } + .export-swatch:active { cursor: grabbing; } + .export-swatch[data-index="0"] { + border: 2px solid #ed1c24; + } + .export-swatch[data-index="0"]::after { + content: "T"; + position: absolute; + top: -2px; left: 1px; + font-size: 9px; + font-weight: 900; + color: white; + text-shadow: 0 1px 2px black; + pointer-events: none; + } + .export-swatch .swatch-idx { + position: absolute; + bottom: 1px; right: 2px; + font-size: 8px; + color: rgba(0,0,0,0.5); + pointer-events: none; + } + .export-swatch.dragging { + opacity: 0.4; + border: 2px dashed #333; + } + .export-swatch.drag-over { + border: 2px solid #0078d7; + transform: scale(1.1); + z-index: 10; + } + +/* ════════════════════════════════════════════════════════════════ + * PRIMEVAL FOREST - deep mossy green-teal dark theme + * ════════════════════════════════════════════════════════════════ */ +body.primeval-forest-mode { + --pf-s0:#090F0C;--pf-s1:#0D1610;--pf-s2:#0D1A14;--pf-s3:#1A2E22;--pf-s4:#1F3828;--pf-s5:#27452F;--pf-s6:#2E5238;--pf-s7:#3A6645;--pf-s8:#4A7A56; + --pf-t1:#D6EDD9;--pf-t2:#7AAE84;--pf-t3:#4A7055;--pf-a1:#5EC47A;--pf-a2:#1F6B3A; + --pf-hover-bg:rgba(94,196,122,0.10);--pf-hover-border:rgba(94,196,122,0.30); + --pf-selected-bg:rgba(94,196,122,0.15);--pf-selected-border:rgba(94,196,122,0.40); + --bg-ribbon:var(--pf-s3);--border-ribbon:var(--pf-s7);--app-text-color:var(--pf-t1);--app-bg:var(--pf-s1); + --titlebar-bg:var(--pf-s2);--titlebar-action-icon:var(--pf-t2);--titlebar-action-hover-bg:var(--pf-s5);--titlebar-separator:var(--pf-s7); + --titlebar-filename:var(--pf-t1);--titlebar-control-icon:var(--pf-t1);--tab-row-bg: var(--pf-s2);--section-divider:var(--pf-s7);--section-title-color:var(--pf-t2); + --btn-hover-bg:var(--pf-hover-bg);--btn-hover-border:var(--pf-hover-border);--btn-hover-shadow:rgba(94,196,122,0.15); + --btn-active-bg:var(--pf-selected-bg);--btn-active-border:var(--pf-selected-border);--btn-active-shadow:rgba(94,196,122,0.25); + --dropdown-bg:var(--pf-s4);--dropdown-border:var(--pf-s8);--dropdown-shadow:rgba(0,0,0,0.55);--dropdown-item-hover-bg:var(--pf-s5); + --dropdown-arrow-color:var(--pf-t2);--dropdown-separator:var(--pf-s7);--btn-small-bg:var(--pf-s4);--btn-small-border:var(--pf-s7); + --threshold-label-color:var(--pf-t2);--threshold-values-color:var(--pf-t1);--toggle-icon-color:var(--pf-a1);--toggle-status-color:var(--pf-t2); + --hotkeys-icon-color:var(--pf-a1);--anchor-toggle-icon-color:var(--pf-a1);--theme-mode-toggle-icon-color:var(--pf-a1); + --select-menu-header-color:var(--pf-t2);--select-menu-header-bg:var(--pf-s4);--select-menu-icon-all-fill:rgba(94,196,122,0.18); + --select-menu-icon-all-fill-disabled:var(--pf-s5);--select-menu-icon-disabled-stroke:var(--pf-t3); + --theme-colors-window-bg:var(--pf-s3);--theme-colors-titlebar-bg-start:var(--pf-s4);--theme-colors-titlebar-bg-end:var(--pf-s3); + --theme-colors-titlebar-border:var(--pf-s7);--theme-colors-top-note-color:var(--pf-t2);--theme-colors-result-count-color:var(--pf-t2); + --theme-colors-list-bg:var(--pf-s3);--theme-colors-editor-panel-bg-start:var(--pf-s4);--theme-colors-editor-panel-bg-end:var(--pf-s3); + --theme-colors-editor-panel-border:var(--pf-s7);--theme-colors-editor-title-color:var(--pf-t1);--theme-colors-editor-key-color:var(--pf-t1); + --theme-colors-editor-input-bg:var(--pf-s5);--theme-colors-editor-input-border:var(--pf-s8);--theme-colors-editor-swatch-border:var(--pf-s8); + --theme-colors-editor-hint-color:var(--pf-t3);--theme-colors-mini-toggle-color:var(--pf-t1); + --palette-mini-swatch-border:#020604;--palette-mini-swatch-inner:#fff; + --palette-mini-swatch-hover-border:var(--pf-a1);--palette-mini-swatch-hover-inner:rgba(94,196,122,0.2); + --view-center-icon-color:var(--pf-a1);--view-edgeclean-icon-color:var(--pf-a1);--view-theme-icon-color:var(--pf-a1); + --select-icon-free-color:var(--pf-a1);--select-icon-poly-main-color:var(--pf-a1);--select-icon-poly-accent-color:#80d898; + --bg-hover:var(--pf-hover-bg);--border-hover:var(--pf-hover-border);--bg-selected:var(--pf-selected-bg);--border-selected:var(--pf-selected-border); + --bg-canvas:#263D2C;--win-blue:var(--pf-a1); +} +body.primeval-forest-mode{background:var(--pf-s1);color:var(--pf-t1);} +body.primeval-forest-mode #title-bar{background:var(--pf-s2);border-bottom:none;} +body.primeval-forest-mode .tab-row{background:var(--pf-s2);border-bottom-color:var(--pf-s7);border-top:1px solid var(--pf-s2);color:var(--pf-t1);} +body.primeval-forest-mode .tab{color:var(--pf-t2);border-color:transparent;} +body.primeval-forest-mode .tab:not(.file):not(.active):hover{color:var(--pf-t1);background:var(--pf-s5);border-color:var(--pf-s7);} +body.primeval-forest-mode .tab.active{background:var(--pf-s3);border-color:var(--pf-s7);border-bottom-color:var(--pf-s3);color:var(--pf-t1);} +body.primeval-forest-mode .tab-row .dd-arrow{color:var(--pf-t1);} +body.primeval-forest-mode .ribbon{background:var(--pf-s3);border-bottom:1px solid var(--pf-s7);} +body.primeval-forest-mode .section{border-right-color:var(--pf-s7);} +body.primeval-forest-mode .section-title{color:var(--pf-t3);} +body.primeval-forest-mode .btn-large{color:var(--pf-t1);} +body.primeval-forest-mode .btn-large:hover{background:var(--pf-s5);border-color:var(--pf-s7);} +body.primeval-forest-mode .btn-large.active,body.primeval-forest-mode .btn-large.is-on{background:var(--pf-selected-bg);border-color:var(--pf-selected-border);} +body.primeval-forest-mode .toggle-status{color:var(--pf-t2);} +body.primeval-forest-mode .toggle-btn .toggle-icons,body.primeval-forest-mode #anchor-toggle-btn .toggle-icons,body.primeval-forest-mode #theme-mode-btn .toggle-icons{color:var(--pf-a1);} +body.primeval-forest-mode svg [fill="#0078d7"]{fill:var(--pf-a1);}body.primeval-forest-mode svg [stroke="#0078d7"]{stroke:var(--pf-a1);} +body.primeval-forest-mode .ribbon-card{background:var(--pf-s4);border-color:var(--pf-s7);color:var(--pf-t1);} +body.primeval-forest-mode .ribbon-card .card-label{color:var(--pf-t1);}body.primeval-forest-mode .ribbon-card .mini-label{color:var(--pf-t2);} +body.primeval-forest-mode .compact-input{background:var(--pf-s4);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode .ribbon-card.tile-compact .compact-input{background:var(--pf-s4);border-bottom-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode .ribbon-card.tile-compact .compact-input:focus{border-bottom-color:var(--pf-a1);} +body.primeval-forest-mode .ribbon-card .compact-input:focus{border-bottom-color:var(--pf-a1);} +body.primeval-forest-mode #pen-size-input{background:var(--pf-s4);border-bottom-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #pen-size-input:focus{border-bottom-color:var(--pf-a1);} +body.primeval-forest-mode #size-control-wrap label{color:var(--pf-t2) !important;} +/* ── C1/C2 active color slot — green highlight ── */ +body.primeval-forest-mode .active-color-col.selected{background:var(--pf-selected-bg);border-color:var(--pf-selected-border);} +body.primeval-forest-mode .active-color-col:not(.selected):hover{background:var(--pf-hover-bg);border-color:var(--pf-hover-border);box-shadow:none;} +/* ── select tool split button — green highlight ── */ +body.primeval-forest-mode .split-btn-container.select-split.selection-highlight{border-color:var(--pf-selected-border);} +body.primeval-forest-mode .split-btn-container.select-split.selection-highlight .split-btn-top, +body.primeval-forest-mode .split-btn-container.select-split.selection-highlight .split-btn-bottom.select-dropdown{background:var(--pf-selected-bg);} +body.primeval-forest-mode .split-btn-container.select-split .split-btn-bottom.select-dropdown.selection-highlight{border-top-color:var(--pf-selected-border);} +body.primeval-forest-mode .split-btn-container.select-split:hover{border-color:var(--pf-hover-border);background:var(--pf-hover-bg);box-shadow:none;} +body.primeval-forest-mode .split-btn-container.select-split:hover .split-btn-bottom.select-dropdown{border-top-color:var(--pf-hover-border);background:var(--pf-hover-bg);box-shadow:none;} +/* ── select-menu dropdown ── */ +body.primeval-forest-mode #select-menu .icon-free, +body.primeval-forest-mode #select-menu .icon-poly, +body.primeval-forest-mode #select-menu .icon-invert{filter:grayscale(1) brightness(1.2) sepia(1) saturate(2.5) hue-rotate(95deg);} +body.primeval-forest-mode #select-menu{--bg-color:var(--pf-s4);--border-outer:var(--pf-s7);--header-text:var(--pf-t2);--text-color:var(--pf-t1);--disabled-text:var(--pf-t3);--separator:var(--pf-s7);--item-hover-bg:var(--pf-s5);--item-hover-border:var(--pf-s8);--item-select-bg:var(--pf-selected-bg);--item-select-border:var(--pf-selected-border);--icon-stroke:var(--pf-a1);box-shadow:2px 2px 8px rgba(0,0,0,0.55);} +/* ── color-preview-highlight and theme-select-candidate — green outline ── */ +body.primeval-forest-mode .color-preview-highlight{outline-color:var(--pf-a1) !important;} +body.primeval-forest-mode .theme-select-candidate{outline-color:var(--pf-a1) !important;} +body.primeval-forest-mode .dropdown-panel,body.primeval-forest-mode .paint-dropdown{background:var(--pf-s4);border-color:var(--pf-s7);box-shadow:0 4px 16px rgba(0,0,0,0.55);} +body.primeval-forest-mode .dropdown-item,body.primeval-forest-mode .paint-dropdown-item{color:var(--pf-t1);} +body.primeval-forest-mode .dropdown-item:hover,body.primeval-forest-mode .paint-dropdown-item:hover{background:var(--pf-s5);} +body.primeval-forest-mode .dropdown-separator{background:var(--pf-s7);} +body.primeval-forest-mode .modal-mask{background:rgba(0,0,0,0.6);} +body.primeval-forest-mode #modal-resize .window,body.primeval-forest-mode #modal-depth .window,body.primeval-forest-mode #modal-export .window,body.primeval-forest-mode #modal-huesat .window,body.primeval-forest-mode #modal-hotkeys .window,body.primeval-forest-mode #modal-info .window,body.primeval-forest-mode #modal-colors .window,body.primeval-forest-mode #modal-confirm-reset .window,body.primeval-forest-mode #modal-toolbar .window,body.primeval-forest-mode #modal-wincolor .window,body.primeval-forest-mode #modal-tool-customizer .window,body.primeval-forest-mode .modal-window{background:var(--pf-s3);border-color:var(--pf-s7);color:var(--pf-t1);box-shadow:0 12px 40px rgba(0,0,0,0.65);} +body.primeval-forest-mode #modal-resize .title-bar,body.primeval-forest-mode #modal-depth .title-bar,body.primeval-forest-mode #modal-export .title-bar,body.primeval-forest-mode #modal-huesat .title-bar,body.primeval-forest-mode #modal-hotkeys .title-bar,body.primeval-forest-mode #modal-info .title-bar,body.primeval-forest-mode #modal-colors .title-bar,body.primeval-forest-mode #modal-confirm-reset .title-bar,body.primeval-forest-mode #modal-toolbar .title-bar,body.primeval-forest-mode #modal-wincolor .title-bar,body.primeval-forest-mode #modal-tool-customizer .title-bar{background:var(--pf-s4);border-bottom-color:var(--pf-s7);color:var(--pf-t1);} +body.primeval-forest-mode .close-btn{color:var(--pf-t2);}body.primeval-forest-mode .close-btn:hover{background:#c42b1c;color:#fff;} +body.primeval-forest-mode #modal-resize .footer,body.primeval-forest-mode #modal-depth .footer,body.primeval-forest-mode #modal-export .footer,body.primeval-forest-mode #modal-huesat .footer,body.primeval-forest-mode #modal-hotkeys .footer,body.primeval-forest-mode #modal-colors .footer{border-top-color:var(--pf-s7);} +body.primeval-forest-mode .modal-mask .btn,body.primeval-forest-mode .modal-mask button.btn{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode .modal-mask .btn:hover{background:var(--pf-s6);border-color:var(--pf-a2);} +body.primeval-forest-mode .modal-mask .btn-primary,body.primeval-forest-mode .modal-mask .btn-ok{background:var(--pf-a2);border-color:#145C2E;color:#fff;} +body.primeval-forest-mode .modal-mask .btn-primary:hover,body.primeval-forest-mode .modal-mask .btn-ok:hover{background:#227A3E;} +body.primeval-forest-mode .modal-mask input[type="text"],body.primeval-forest-mode .modal-mask input[type="number"],body.primeval-forest-mode .modal-mask select,body.primeval-forest-mode .modal-mask textarea{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode .modal-mask input[type="text"]:focus,body.primeval-forest-mode .modal-mask input[type="number"]:focus,body.primeval-forest-mode .modal-mask select:focus{border-color:var(--pf-a1);outline:none;} +body.primeval-forest-mode .modal-mask label,body.primeval-forest-mode .modal-mask .row label{color:var(--pf-t1);} +body.primeval-forest-mode .modal-mask fieldset{border-color:var(--pf-s7);}body.primeval-forest-mode .modal-mask legend{color:var(--pf-t2);} +body.primeval-forest-mode .modal-mask .section-header{color:var(--pf-t2);border-bottom-color:var(--pf-s7);} +body.primeval-forest-mode .hotkey-list{background:var(--pf-s4);border-color:var(--pf-s7);}body.primeval-forest-mode .hotkey-row{border-bottom-color:var(--pf-s7);} +body.primeval-forest-mode .hotkey-label{color:var(--pf-t1);}body.primeval-forest-mode .hotkeys-icon{color:var(--pf-a1);} +body.primeval-forest-mode #modal-info .info-section{border-bottom-color:var(--pf-s7);}body.primeval-forest-mode #modal-info .info-title{color:var(--pf-a1);} +body.primeval-forest-mode #modal-info li,body.primeval-forest-mode #modal-info p{color:var(--pf-t1);}body.primeval-forest-mode #modal-info .link-btn{color:var(--pf-a1);} +body.primeval-forest-mode #modal-colors .color-item{border-bottom-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .color-item:hover{background:var(--pf-s5);border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .color-item.is-active{background:var(--pf-selected-bg);border-color:var(--pf-selected-border);} +body.primeval-forest-mode #modal-colors .color-label,body.primeval-forest-mode #modal-colors .close-btn{color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .color-element-label,body.primeval-forest-mode #modal-colors .select-hint{color:var(--pf-t2);} +body.primeval-forest-mode #modal-colors .color-input,body.primeval-forest-mode #modal-colors .preset-select,body.primeval-forest-mode #modal-colors .preset-input,body.primeval-forest-mode #modal-colors .search-input{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .color-input.invalid{border-color:#e81123;background:rgba(232,17,35,0.15);} +body.primeval-forest-mode #modal-colors .btn{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .btn:hover{background:var(--pf-s6);border-color:var(--pf-a1);} +body.primeval-forest-mode #modal-colors .color-list{border-color:var(--pf-s7);}body.primeval-forest-mode #modal-colors .preset-row{background:var(--pf-s4);border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .editor-grid label{color:var(--pf-t2);}body.primeval-forest-mode #modal-colors .editor-grid input[type="number"]{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #file-menu.file-menu-template{background:var(--pf-s3);border-color:var(--pf-s7);box-shadow:2px 4px 16px rgba(0,0,0,0.55);} +body.primeval-forest-mode #file-menu .file-menu-left{background:var(--pf-s4);border-right-color:var(--pf-s7);} +body.primeval-forest-mode #file-menu .file-menu-item{color:var(--pf-t1);}body.primeval-forest-mode #file-menu .file-menu-item:hover{background:var(--pf-s5);border-color:var(--pf-s7);} +body.primeval-forest-mode #file-menu .file-menu-separator{background:var(--pf-s7);}body.primeval-forest-mode #file-menu .file-menu-right{background:var(--pf-s3);} +body.primeval-forest-mode #file-menu .file-menu-recent-toggle{background:var(--pf-s4);border-color:var(--pf-s7);color:var(--pf-t2);} +body.primeval-forest-mode #file-menu .file-menu-recent-toggle:hover{background:var(--pf-s5);color:var(--pf-t1);} +body.primeval-forest-mode #file-menu .file-menu-recent-header{color:var(--pf-t1);border-bottom-color:var(--pf-s7);} +body.primeval-forest-mode #file-menu .file-menu-recent-item{color:var(--pf-t2);}body.primeval-forest-mode #file-menu .file-menu-recent-item:hover{background:var(--pf-s5);color:var(--pf-t1);} +body.primeval-forest-mode .select-menu{background:var(--pf-s4);border-color:var(--pf-s7);box-shadow:0 4px 16px rgba(0,0,0,0.55);} +body.primeval-forest-mode .select-menu-header{background:var(--pf-s4);color:var(--pf-t2);border-bottom-color:var(--pf-s7);} +body.primeval-forest-mode .select-menu-item{color:var(--pf-t1);}body.primeval-forest-mode .select-menu-item:hover{background:var(--pf-s5);} +body.primeval-forest-mode input[type="range"]::-webkit-slider-runnable-track,body.primeval-forest-mode input[type="range"]::-moz-range-track{background:var(--pf-s6);} +body.primeval-forest-mode input[type="range"]::-webkit-slider-thumb{background:var(--pf-a1);}body.primeval-forest-mode input[type="range"]::-moz-range-thumb{background:var(--pf-a1);} +body.primeval-forest-mode #save-reminder-modal .window,body.primeval-forest-mode #close-confirm-modal .window{background:var(--pf-s3);border-color:var(--pf-s7);color:var(--pf-t1);} +body.primeval-forest-mode #lsys-panel{background:var(--pf-s3);border-color:var(--pf-s7);}body.primeval-forest-mode #lsys-list{background:var(--pf-s4);} +body.primeval-forest-mode .lsys-layer{background:var(--pf-s4);border-bottom-color:var(--pf-s7);color:var(--pf-t1);}body.primeval-forest-mode .lsys-layer.active{background:var(--pf-s5);} +body.primeval-forest-mode .lsys-layer-name{color:var(--pf-t1);}body.primeval-forest-mode #lsys-toolbar{background:var(--pf-s3);border-top-color:var(--pf-s7);} +body.primeval-forest-mode .lsys-btn{color:var(--pf-t1);}body.primeval-forest-mode .lsys-btn:hover{background:var(--pf-s5);} +body.primeval-forest-mode #status-bar,body.primeval-forest-mode .status-bar{background:var(--pf-s2);border-top-color:var(--pf-s7);color:var(--pf-t2);} +body.primeval-forest-mode #palette-bar,body.primeval-forest-mode .palette-bar{background:var(--pf-s3);border-top-color:var(--pf-s7);} +body.primeval-forest-mode .mini-swatch.empty{box-shadow:none;} +body.primeval-forest-mode #modal-wincolor .tab-btn{background:var(--pf-s5);border-color:var(--pf-s7);color:var(--pf-t1);}body.primeval-forest-mode #modal-wincolor .tab-btn.active{background:var(--pf-a2);color:#fff;} +body.primeval-forest-mode #swap-colors-btn{background:var(--pf-s4) !important;border-color:var(--pf-s7) !important;color:var(--pf-t1);} +body.primeval-forest-mode #swap-colors-btn:hover{background:var(--pf-s5) !important;border-color:var(--pf-s8) !important;} + +/* ── sliders (Firefox fill track) ── */ +body.primeval-forest-mode input[type="range"]::-moz-range-progress{background:var(--pf-a2);} + +/* ── extra toggle buttons ── */ +body.primeval-forest-mode #test-mode-btn .toggle-icons{color:var(--pf-a1);} +body.primeval-forest-mode #dark-refined-btn .toggle-icons{color:var(--pf-a1);} + +/* ── modal-depth specific ── */ +body.primeval-forest-mode #modal-depth{--win-bg:var(--pf-s3);--win-text:var(--pf-t1);--input-border:var(--pf-s8);--input-focus:var(--pf-a1);--btn-border:var(--pf-s8);--btn-hover:var(--pf-s5);} +body.primeval-forest-mode #modal-depth .btn{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-depth .btn:hover{background:var(--pf-s6);border-color:var(--pf-a1);} +body.primeval-forest-mode #modal-depth .btn-primary{background:var(--pf-a2);border-color:#145C2E;color:#fff;} +body.primeval-forest-mode #modal-depth .btn-primary:hover{background:#227A3E;} +body.primeval-forest-mode #modal-depth .depth-radio{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-depth .depth-color-list input[type="radio"]:checked + .depth-radio{background:var(--pf-a2);border-color:var(--pf-a1);color:#fff;} +body.primeval-forest-mode #modal-depth .depth-group-label{color:var(--pf-t2);} + +/* ── modal-huesat specific ── */ +body.primeval-forest-mode #modal-huesat{--win-bg:var(--pf-s3);--win-text:var(--pf-t1);--input-border:var(--pf-s8);--input-focus:var(--pf-a1);--btn-border:var(--pf-s8);--btn-hover:var(--pf-s5);} +body.primeval-forest-mode #modal-huesat .channel-area{background:var(--pf-s4);border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-huesat .channel-label{color:var(--pf-t2);} +body.primeval-forest-mode #modal-huesat .channel-btn{filter:brightness(0.88);} +body.primeval-forest-mode #modal-huesat .channel-wrap.active{background:transparent;} +body.primeval-forest-mode #modal-huesat .master-btn{background:var(--pf-s6);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-huesat .master-btn.active{background:var(--pf-t1);color:var(--pf-s1);border-color:var(--pf-t1);} +body.primeval-forest-mode #modal-huesat .preset-row{background:var(--pf-s4);border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-huesat .preset-row label{color:var(--pf-t2);} + +/* ── modal-resize specific ── */ +body.primeval-forest-mode #modal-resize{--win-bg:var(--pf-s3);--win-text:var(--pf-t1);} +body.primeval-forest-mode #modal-resize .col-label{color:var(--pf-t1);} + + +/* ── modal-export specific ── */ +body.primeval-forest-mode #modal-export{--win-bg:var(--pf-s3);--input-border:var(--pf-s8);--input-focus:var(--pf-a1);--btn-border:var(--pf-s8);--btn-hover:var(--pf-s5);} +body.primeval-forest-mode #modal-export .export-format-btn{background:var(--pf-s5);border-color:var(--pf-s7);color:var(--pf-t1);} +body.primeval-forest-mode #modal-export .export-format-btn.active{background:var(--pf-a2);border-color:var(--pf-a1);color:#fff;} + +/* ── modal-colors (theme colors editor) extended ── */ +body.primeval-forest-mode #modal-colors .window{border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .close-btn{color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .select-hint{color:var(--pf-t2);} +body.primeval-forest-mode #modal-colors .color-list{border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .color-item:hover{background:var(--pf-s5);border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .color-item.is-active{background:var(--pf-selected-bg);border-color:var(--pf-selected-border);} +body.primeval-forest-mode #modal-colors .color-label{color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .color-element-label{color:var(--pf-t2);} +body.primeval-forest-mode #modal-colors .color-input{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .color-input.invalid{border-color:#e81123;background:rgba(232,17,35,0.15);} +body.primeval-forest-mode #modal-colors .preset-select, +body.primeval-forest-mode #modal-colors .preset-input, +body.primeval-forest-mode #modal-colors .search-input{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .btn{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .btn:hover{background:var(--pf-s6);border-color:var(--pf-a1);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .editor-grid label{color:var(--pf-t2);} +body.primeval-forest-mode #modal-colors .editor-grid input[type="number"]{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .color-item{border-bottom-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .preset-row{background:var(--pf-s4);border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-colors .preset-select{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} +body.primeval-forest-mode #modal-colors .preset-input{background:var(--pf-s5);border-color:var(--pf-s8);color:var(--pf-t1);} + +/* ── modal-wincolor extended ── */ +body.primeval-forest-mode #modal-wincolor .window{background:var(--pf-s3);border-color:var(--pf-s7);} +body.primeval-forest-mode #modal-wincolor .tab-btn{background:var(--pf-s5);border-color:var(--pf-s7);color:var(--pf-t1);} +body.primeval-forest-mode #modal-wincolor .tab-btn.active{background:var(--pf-a2);color:#fff;} + +/* ════════════════════════════════════════════════════════════════ + * ABYSSAL OCEAN - deep midnight navy dark theme + * ════════════════════════════════════════════════════════════════ */ +body.abyssal-ocean-mode { + --ao-s0:#060810;--ao-s1:#090D1A;--ao-s2:#0A0F1E;--ao-s3:#0F1E3A;--ao-s4:#132448;--ao-s5:#192D58;--ao-s6:#1F3668;--ao-s7:#2B487E;--ao-s8:#3A5E96; + --ao-t1:#C8D8F4;--ao-t2:#6888C0;--ao-t3:#3A5284;--ao-a1:#5AA8F0;--ao-a2:#1A4D8C; + --ao-hover-bg:rgba(90,168,240,0.10);--ao-hover-border:rgba(90,168,240,0.30); + --ao-selected-bg:rgba(90,168,240,0.15);--ao-selected-border:rgba(90,168,240,0.40); + --bg-ribbon:var(--ao-s3);--border-ribbon:var(--ao-s7);--app-text-color:var(--ao-t1);--app-bg:var(--ao-s1); + --titlebar-bg:var(--ao-s2);--titlebar-action-icon:var(--ao-t2);--titlebar-action-hover-bg:var(--ao-s5);--titlebar-separator:var(--ao-s7); + --titlebar-filename:var(--ao-t1);--titlebar-control-icon:var(--ao-t1);--tab-row-bg:var(--ao-s2);--section-divider:var(--ao-s7);--section-title-color:var(--ao-t2); + --btn-hover-bg:var(--ao-hover-bg);--btn-hover-border:var(--ao-hover-border);--btn-hover-shadow:rgba(90,168,240,0.15); + --btn-active-bg:var(--ao-selected-bg);--btn-active-border:var(--ao-selected-border);--btn-active-shadow:rgba(90,168,240,0.25); + --dropdown-bg:var(--ao-s4);--dropdown-border:var(--ao-s8);--dropdown-shadow:rgba(0,0,0,0.6);--dropdown-item-hover-bg:var(--ao-s5); + --dropdown-arrow-color:var(--ao-t2);--dropdown-separator:var(--ao-s7);--btn-small-bg:var(--ao-s4);--btn-small-border:var(--ao-s7); + --threshold-label-color:var(--ao-t2);--threshold-values-color:var(--ao-t1);--toggle-icon-color:var(--ao-a1);--toggle-status-color:var(--ao-t2); + --hotkeys-icon-color:var(--ao-a1);--anchor-toggle-icon-color:var(--ao-a1);--theme-mode-toggle-icon-color:var(--ao-a1); + --select-menu-header-color:var(--ao-t2);--select-menu-header-bg:var(--ao-s4);--select-menu-icon-all-fill:rgba(90,168,240,0.18); + --select-menu-icon-all-fill-disabled:var(--ao-s5);--select-menu-icon-disabled-stroke:var(--ao-t3); + --theme-colors-window-bg:var(--ao-s3);--theme-colors-titlebar-bg-start:var(--ao-s4);--theme-colors-titlebar-bg-end:var(--ao-s3); + --theme-colors-titlebar-border:var(--ao-s7);--theme-colors-top-note-color:var(--ao-t2);--theme-colors-result-count-color:var(--ao-t2); + --theme-colors-list-bg:var(--ao-s3);--theme-colors-editor-panel-bg-start:var(--ao-s4);--theme-colors-editor-panel-bg-end:var(--ao-s3); + --theme-colors-editor-panel-border:var(--ao-s7);--theme-colors-editor-title-color:var(--ao-t1);--theme-colors-editor-key-color:var(--ao-t1); + --theme-colors-editor-input-bg:var(--ao-s5);--theme-colors-editor-input-border:var(--ao-s8);--theme-colors-editor-swatch-border:var(--ao-s8); + --theme-colors-editor-hint-color:var(--ao-t3);--theme-colors-mini-toggle-color:var(--ao-t1); + --palette-mini-swatch-border:#02040A;--palette-mini-swatch-inner:#fff; + --palette-mini-swatch-hover-border:var(--ao-a1);--palette-mini-swatch-hover-inner:rgba(90,168,240,0.2); + --view-center-icon-color:var(--ao-a1);--view-edgeclean-icon-color:var(--ao-a1);--view-theme-icon-color:var(--ao-a1); + --select-icon-free-color:var(--ao-a1);--select-icon-poly-main-color:var(--ao-a1);--select-icon-poly-accent-color:#7AC4FF; + --bg-hover:var(--ao-hover-bg);--border-hover:var(--ao-hover-border);--bg-selected:var(--ao-selected-bg);--border-selected:var(--ao-selected-border); + --bg-canvas:#192A4E;--win-blue:var(--ao-a1); +} +body.abyssal-ocean-mode{background:var(--ao-s1);color:var(--ao-t1);} +body.abyssal-ocean-mode #title-bar{background:var(--ao-s2);border-bottom:none;} +body.abyssal-ocean-mode .tab-row{background:var(--ao-s2);border-bottom-color:var(--ao-s7);border-top:1px solid var(--ao-s2);color:var(--ao-t1);} +body.abyssal-ocean-mode .tab{color:var(--ao-t2);border-color:transparent;} +body.abyssal-ocean-mode .tab:not(.file):not(.active):hover{color:var(--ao-t1);background:var(--ao-s5);border-color:var(--ao-s7);} +body.abyssal-ocean-mode .tab.active{background:var(--ao-s3);border-color:var(--ao-s7);border-bottom-color:var(--ao-s3);color:var(--ao-t1);} +body.abyssal-ocean-mode .tab-row .dd-arrow{color:var(--ao-t1);} +body.abyssal-ocean-mode .ribbon{background:var(--ao-s3);border-bottom:1px solid var(--ao-s7);} +body.abyssal-ocean-mode .section{border-right-color:var(--ao-s7);}body.abyssal-ocean-mode .section-title{color:var(--ao-t3);} +body.abyssal-ocean-mode .btn-large{color:var(--ao-t1);}body.abyssal-ocean-mode .btn-large:hover{background:var(--ao-s5);border-color:var(--ao-s7);} +body.abyssal-ocean-mode .btn-large.active,body.abyssal-ocean-mode .btn-large.is-on{background:var(--ao-selected-bg);border-color:var(--ao-selected-border);} +body.abyssal-ocean-mode .toggle-status{color:var(--ao-t2);} +body.abyssal-ocean-mode .toggle-btn .toggle-icons,body.abyssal-ocean-mode #anchor-toggle-btn .toggle-icons,body.abyssal-ocean-mode #theme-mode-btn .toggle-icons{color:var(--ao-a1);} +body.abyssal-ocean-mode svg [fill="#0078d7"]{fill:var(--ao-a1);}body.abyssal-ocean-mode svg [stroke="#0078d7"]{stroke:var(--ao-a1);} +body.abyssal-ocean-mode .ribbon-card{background:var(--ao-s4);border-color:var(--ao-s7);color:var(--ao-t1);} +body.abyssal-ocean-mode .ribbon-card .card-label{color:var(--ao-t1);}body.abyssal-ocean-mode .ribbon-card .mini-label{color:var(--ao-t2);} +body.abyssal-ocean-mode .compact-input{background:var(--ao-s5);border-color:var(--ao-s8);color:var(--ao-t1);} +body.abyssal-ocean-mode .dropdown-panel,body.abyssal-ocean-mode .paint-dropdown{background:var(--ao-s4);border-color:var(--ao-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.abyssal-ocean-mode .dropdown-item,body.abyssal-ocean-mode .paint-dropdown-item{color:var(--ao-t1);} +body.abyssal-ocean-mode .dropdown-item:hover,body.abyssal-ocean-mode .paint-dropdown-item:hover{background:var(--ao-s5);} +body.abyssal-ocean-mode .dropdown-separator{background:var(--ao-s7);} +body.abyssal-ocean-mode .modal-mask{background:rgba(0,0,0,0.65);} +body.abyssal-ocean-mode #modal-resize .window,body.abyssal-ocean-mode #modal-depth .window,body.abyssal-ocean-mode #modal-export .window,body.abyssal-ocean-mode #modal-huesat .window,body.abyssal-ocean-mode #modal-hotkeys .window,body.abyssal-ocean-mode #modal-info .window,body.abyssal-ocean-mode #modal-colors .window,body.abyssal-ocean-mode #modal-confirm-reset .window,body.abyssal-ocean-mode #modal-toolbar .window,body.abyssal-ocean-mode #modal-wincolor .window,body.abyssal-ocean-mode #modal-tool-customizer .window,body.abyssal-ocean-mode .modal-window{background:var(--ao-s3);border-color:var(--ao-s7);color:var(--ao-t1);box-shadow:0 12px 40px rgba(0,0,0,0.7);} +body.abyssal-ocean-mode #modal-resize .title-bar,body.abyssal-ocean-mode #modal-depth .title-bar,body.abyssal-ocean-mode #modal-export .title-bar,body.abyssal-ocean-mode #modal-huesat .title-bar,body.abyssal-ocean-mode #modal-hotkeys .title-bar,body.abyssal-ocean-mode #modal-info .title-bar,body.abyssal-ocean-mode #modal-colors .title-bar,body.abyssal-ocean-mode #modal-confirm-reset .title-bar,body.abyssal-ocean-mode #modal-toolbar .title-bar,body.abyssal-ocean-mode #modal-wincolor .title-bar,body.abyssal-ocean-mode #modal-tool-customizer .title-bar{background:var(--ao-s4);border-bottom-color:var(--ao-s7);color:var(--ao-t1);} +body.abyssal-ocean-mode .close-btn{color:var(--ao-t2);}body.abyssal-ocean-mode .close-btn:hover{background:#c42b1c;color:#fff;} +body.abyssal-ocean-mode #modal-resize .footer,body.abyssal-ocean-mode #modal-depth .footer,body.abyssal-ocean-mode #modal-export .footer,body.abyssal-ocean-mode #modal-huesat .footer,body.abyssal-ocean-mode #modal-hotkeys .footer,body.abyssal-ocean-mode #modal-colors .footer{border-top-color:var(--ao-s7);} +body.abyssal-ocean-mode .modal-mask .btn,body.abyssal-ocean-mode .modal-mask button.btn{background:var(--ao-s5);border-color:var(--ao-s8);color:var(--ao-t1);} +body.abyssal-ocean-mode .modal-mask .btn:hover{background:var(--ao-s6);border-color:var(--ao-a2);} +body.abyssal-ocean-mode .modal-mask .btn-primary,body.abyssal-ocean-mode .modal-mask .btn-ok{background:var(--ao-a2);border-color:#0E3870;color:#fff;} +body.abyssal-ocean-mode .modal-mask .btn-primary:hover,body.abyssal-ocean-mode .modal-mask .btn-ok:hover{background:#205898;} +body.abyssal-ocean-mode .modal-mask input[type="text"],body.abyssal-ocean-mode .modal-mask input[type="number"],body.abyssal-ocean-mode .modal-mask select,body.abyssal-ocean-mode .modal-mask textarea{background:var(--ao-s5);border-color:var(--ao-s8);color:var(--ao-t1);} +body.abyssal-ocean-mode .modal-mask input[type="text"]:focus,body.abyssal-ocean-mode .modal-mask input[type="number"]:focus,body.abyssal-ocean-mode .modal-mask select:focus{border-color:var(--ao-a1);outline:none;} +body.abyssal-ocean-mode .modal-mask label,body.abyssal-ocean-mode .modal-mask .row label{color:var(--ao-t1);} +body.abyssal-ocean-mode .modal-mask fieldset{border-color:var(--ao-s7);}body.abyssal-ocean-mode .modal-mask legend{color:var(--ao-t2);} +body.abyssal-ocean-mode .modal-mask .section-header{color:var(--ao-t2);border-bottom-color:var(--ao-s7);} +body.abyssal-ocean-mode .hotkey-list{background:var(--ao-s4);border-color:var(--ao-s7);}body.abyssal-ocean-mode .hotkey-row{border-bottom-color:var(--ao-s7);} +body.abyssal-ocean-mode .hotkey-label{color:var(--ao-t1);}body.abyssal-ocean-mode .hotkeys-icon{color:var(--ao-a1);} +body.abyssal-ocean-mode #modal-info .info-section{border-bottom-color:var(--ao-s7);}body.abyssal-ocean-mode #modal-info .info-title{color:var(--ao-a1);} +body.abyssal-ocean-mode #modal-info li,body.abyssal-ocean-mode #modal-info p{color:var(--ao-t1);}body.abyssal-ocean-mode #modal-info .link-btn{color:var(--ao-a1);} +body.abyssal-ocean-mode #modal-colors .color-item{border-bottom-color:var(--ao-s7);} +body.abyssal-ocean-mode #modal-colors .color-item:hover{background:var(--ao-s5);border-color:var(--ao-s7);} +body.abyssal-ocean-mode #modal-colors .color-item.is-active{background:var(--ao-selected-bg);border-color:var(--ao-selected-border);} +body.abyssal-ocean-mode #modal-colors .color-label,body.abyssal-ocean-mode #modal-colors .close-btn{color:var(--ao-t1);} +body.abyssal-ocean-mode #modal-colors .color-element-label,body.abyssal-ocean-mode #modal-colors .select-hint{color:var(--ao-t2);} +body.abyssal-ocean-mode #modal-colors .color-input,body.abyssal-ocean-mode .preset-select,body.abyssal-ocean-mode .preset-input,body.abyssal-ocean-mode .search-input{background:var(--ao-s5);border-color:var(--ao-s8);color:var(--ao-t1);} +body.abyssal-ocean-mode #modal-colors .color-input.invalid{border-color:#e81123;background:rgba(232,17,35,0.15);} +body.abyssal-ocean-mode #modal-colors .btn{background:var(--ao-s5);border-color:var(--ao-s8);color:var(--ao-t1);} +body.abyssal-ocean-mode #modal-colors .btn:hover{background:var(--ao-s6);border-color:var(--ao-a1);} +body.abyssal-ocean-mode #modal-colors .color-list{border-color:var(--ao-s7);}body.abyssal-ocean-mode #modal-colors .preset-row{background:var(--ao-s4);border-color:var(--ao-s7);} +body.abyssal-ocean-mode #modal-colors .editor-grid label{color:var(--ao-t2);}body.abyssal-ocean-mode #modal-colors .editor-grid input[type="number"]{background:var(--ao-s5);border-color:var(--ao-s8);color:var(--ao-t1);} +body.abyssal-ocean-mode #file-menu.file-menu-template{background:var(--ao-s3);border-color:var(--ao-s7);box-shadow:2px 4px 16px rgba(0,0,0,0.6);} +body.abyssal-ocean-mode #file-menu .file-menu-left{background:var(--ao-s4);border-right-color:var(--ao-s7);} +body.abyssal-ocean-mode #file-menu .file-menu-item{color:var(--ao-t1);}body.abyssal-ocean-mode #file-menu .file-menu-item:hover{background:var(--ao-s5);border-color:var(--ao-s7);} +body.abyssal-ocean-mode #file-menu .file-menu-separator{background:var(--ao-s7);}body.abyssal-ocean-mode #file-menu .file-menu-right{background:var(--ao-s3);} +body.abyssal-ocean-mode #file-menu .file-menu-recent-toggle{background:var(--ao-s4);border-color:var(--ao-s7);color:var(--ao-t2);} +body.abyssal-ocean-mode #file-menu .file-menu-recent-toggle:hover{background:var(--ao-s5);color:var(--ao-t1);} +body.abyssal-ocean-mode #file-menu .file-menu-recent-header{color:var(--ao-t1);border-bottom-color:var(--ao-s7);} +body.abyssal-ocean-mode #file-menu .file-menu-recent-item{color:var(--ao-t2);}body.abyssal-ocean-mode #file-menu .file-menu-recent-item:hover{background:var(--ao-s5);color:var(--ao-t1);} +body.abyssal-ocean-mode .select-menu{background:var(--ao-s4);border-color:var(--ao-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.abyssal-ocean-mode .select-menu-header{background:var(--ao-s4);color:var(--ao-t2);border-bottom-color:var(--ao-s7);} +body.abyssal-ocean-mode .select-menu-item{color:var(--ao-t1);}body.abyssal-ocean-mode .select-menu-item:hover{background:var(--ao-s5);} +body.abyssal-ocean-mode input[type="range"]::-webkit-slider-runnable-track,body.abyssal-ocean-mode input[type="range"]::-moz-range-track{background:var(--ao-s6);} +body.abyssal-ocean-mode input[type="range"]::-webkit-slider-thumb{background:var(--ao-a1);}body.abyssal-ocean-mode input[type="range"]::-moz-range-thumb{background:var(--ao-a1);} +body.abyssal-ocean-mode #save-reminder-modal .window,body.abyssal-ocean-mode #close-confirm-modal .window{background:var(--ao-s3);border-color:var(--ao-s7);color:var(--ao-t1);} +body.abyssal-ocean-mode #lsys-panel{background:var(--ao-s3);border-color:var(--ao-s7);}body.abyssal-ocean-mode #lsys-list{background:var(--ao-s4);} +body.abyssal-ocean-mode .lsys-layer{background:var(--ao-s4);border-bottom-color:var(--ao-s7);color:var(--ao-t1);}body.abyssal-ocean-mode .lsys-layer.active{background:var(--ao-s5);} +body.abyssal-ocean-mode .lsys-layer-name{color:var(--ao-t1);}body.abyssal-ocean-mode #lsys-toolbar{background:var(--ao-s3);border-top-color:var(--ao-s7);} +body.abyssal-ocean-mode .lsys-btn{color:var(--ao-t1);}body.abyssal-ocean-mode .lsys-btn:hover{background:var(--ao-s5);} +body.abyssal-ocean-mode #status-bar,body.abyssal-ocean-mode .status-bar{background:var(--ao-s2);border-top-color:var(--ao-s7);color:var(--ao-t2);} +body.abyssal-ocean-mode #palette-bar,body.abyssal-ocean-mode .palette-bar{background:var(--ao-s3);border-top-color:var(--ao-s7);} +body.abyssal-ocean-mode .mini-swatch.empty{box-shadow:none;} +body.abyssal-ocean-mode #modal-wincolor .tab-btn{background:var(--ao-s5);border-color:var(--ao-s7);color:var(--ao-t1);}body.abyssal-ocean-mode #modal-wincolor .tab-btn.active{background:var(--ao-a2);color:#fff;} +body.abyssal-ocean-mode #swap-colors-btn{background:var(--ao-s4) !important;border-color:var(--ao-s7) !important;color:var(--ao-t1);} +body.abyssal-ocean-mode #swap-colors-btn:hover{background:var(--ao-s5) !important;border-color:var(--ao-s8) !important;} + +/* ════════════════════════════════════════════════════════════════ + * CRIMSON DUSK - dark warm burgundy with deep ember accents + * ════════════════════════════════════════════════════════════════ */ +body.crimson-dusk-mode { + --cd-s0:#0F0707;--cd-s1:#180B0B;--cd-s2:#1E0E0E;--cd-s3:#321414;--cd-s4:#3D1818;--cd-s5:#4A1E1E;--cd-s6:#582424;--cd-s7:#6E3030;--cd-s8:#874040; + --cd-t1:#F0D8D8;--cd-t2:#B07070;--cd-t3:#784848;--cd-a1:#F07070;--cd-a2:#8C1C1C; + --cd-hover-bg:rgba(240,112,112,0.10);--cd-hover-border:rgba(240,112,112,0.30); + --cd-selected-bg:rgba(240,112,112,0.15);--cd-selected-border:rgba(240,112,112,0.40); + --bg-ribbon:var(--cd-s3);--border-ribbon:var(--cd-s7);--app-text-color:var(--cd-t1);--app-bg:var(--cd-s1); + --titlebar-bg:var(--cd-s2);--titlebar-action-icon:var(--cd-t2);--titlebar-action-hover-bg:var(--cd-s5);--titlebar-separator:var(--cd-s7); + --titlebar-filename:var(--cd-t1);--titlebar-control-icon:var(--cd-t1);--tab-row-bg:var(--cd-s2);--section-divider:var(--cd-s7);--section-title-color:var(--cd-t2); + --btn-hover-bg:var(--cd-hover-bg);--btn-hover-border:var(--cd-hover-border);--btn-hover-shadow:rgba(240,112,112,0.15); + --btn-active-bg:var(--cd-selected-bg);--btn-active-border:var(--cd-selected-border);--btn-active-shadow:rgba(240,112,112,0.25); + --dropdown-bg:var(--cd-s4);--dropdown-border:var(--cd-s8);--dropdown-shadow:rgba(0,0,0,0.6);--dropdown-item-hover-bg:var(--cd-s5); + --dropdown-arrow-color:var(--cd-t2);--dropdown-separator:var(--cd-s7);--btn-small-bg:var(--cd-s4);--btn-small-border:var(--cd-s7); + --threshold-label-color:var(--cd-t2);--threshold-values-color:var(--cd-t1);--toggle-icon-color:var(--cd-a1);--toggle-status-color:var(--cd-t2); + --hotkeys-icon-color:var(--cd-a1);--anchor-toggle-icon-color:var(--cd-a1);--theme-mode-toggle-icon-color:var(--cd-a1); + --select-menu-header-color:var(--cd-t2);--select-menu-header-bg:var(--cd-s4);--select-menu-icon-all-fill:rgba(240,112,112,0.18); + --select-menu-icon-all-fill-disabled:var(--cd-s5);--select-menu-icon-disabled-stroke:var(--cd-t3); + --theme-colors-window-bg:var(--cd-s3);--theme-colors-titlebar-bg-start:var(--cd-s4);--theme-colors-titlebar-bg-end:var(--cd-s3); + --theme-colors-titlebar-border:var(--cd-s7);--theme-colors-top-note-color:var(--cd-t2);--theme-colors-result-count-color:var(--cd-t2); + --theme-colors-list-bg:var(--cd-s3);--theme-colors-editor-panel-bg-start:var(--cd-s4);--theme-colors-editor-panel-bg-end:var(--cd-s3); + --theme-colors-editor-panel-border:var(--cd-s7);--theme-colors-editor-title-color:var(--cd-t1);--theme-colors-editor-key-color:var(--cd-t1); + --theme-colors-editor-input-bg:var(--cd-s5);--theme-colors-editor-input-border:var(--cd-s8);--theme-colors-editor-swatch-border:var(--cd-s8); + --theme-colors-editor-hint-color:var(--cd-t3);--theme-colors-mini-toggle-color:var(--cd-t1); + --palette-mini-swatch-border:#0A0303;--palette-mini-swatch-inner:#fff; + --palette-mini-swatch-hover-border:var(--cd-a1);--palette-mini-swatch-hover-inner:rgba(240,112,112,0.2); + --view-center-icon-color:var(--cd-a1);--view-edgeclean-icon-color:var(--cd-a1);--view-theme-icon-color:var(--cd-a1); + --select-icon-free-color:var(--cd-a1);--select-icon-poly-main-color:var(--cd-a1);--select-icon-poly-accent-color:#FF9090; + --bg-hover:var(--cd-hover-bg);--border-hover:var(--cd-hover-border);--bg-selected:var(--cd-selected-bg);--border-selected:var(--cd-selected-border); + --bg-canvas:#3E1E1E;--win-blue:var(--cd-a1); +} +body.crimson-dusk-mode{background:var(--cd-s1);color:var(--cd-t1);} +body.crimson-dusk-mode #title-bar{background:var(--cd-s2);border-bottom:none;} +body.crimson-dusk-mode .tab-row{background:var(--cd-s2);border-bottom-color:var(--cd-s7);border-top:1px solid var(--cd-s2);color:var(--cd-t1);} +body.crimson-dusk-mode .tab{color:var(--cd-t2);border-color:transparent;} +body.crimson-dusk-mode .tab:not(.file):not(.active):hover{color:var(--cd-t1);background:var(--cd-s5);border-color:var(--cd-s7);} +body.crimson-dusk-mode .tab.active{background:var(--cd-s3);border-color:var(--cd-s7);border-bottom-color:var(--cd-s3);color:var(--cd-t1);} +body.crimson-dusk-mode .tab-row .dd-arrow{color:var(--cd-t1);} +body.crimson-dusk-mode .ribbon{background:var(--cd-s3);border-bottom:1px solid var(--cd-s7);} +body.crimson-dusk-mode .section{border-right-color:var(--cd-s7);}body.crimson-dusk-mode .section-title{color:var(--cd-t3);} +body.crimson-dusk-mode .btn-large{color:var(--cd-t1);}body.crimson-dusk-mode .btn-large:hover{background:var(--cd-s5);border-color:var(--cd-s7);} +body.crimson-dusk-mode .btn-large.active,body.crimson-dusk-mode .btn-large.is-on{background:var(--cd-selected-bg);border-color:var(--cd-selected-border);} +body.crimson-dusk-mode .toggle-status{color:var(--cd-t2);} +body.crimson-dusk-mode .toggle-btn .toggle-icons,body.crimson-dusk-mode #anchor-toggle-btn .toggle-icons,body.crimson-dusk-mode #theme-mode-btn .toggle-icons{color:var(--cd-a1);} +body.crimson-dusk-mode svg [fill="#0078d7"]{fill:var(--cd-a1);}body.crimson-dusk-mode svg [stroke="#0078d7"]{stroke:var(--cd-a1);} +body.crimson-dusk-mode .ribbon-card{background:var(--cd-s4);border-color:var(--cd-s7);color:var(--cd-t1);} +body.crimson-dusk-mode .ribbon-card .card-label{color:var(--cd-t1);}body.crimson-dusk-mode .ribbon-card .mini-label{color:var(--cd-t2);} +body.crimson-dusk-mode .compact-input{background:var(--cd-s5);border-color:var(--cd-s8);color:var(--cd-t1);} +body.crimson-dusk-mode .dropdown-panel,body.crimson-dusk-mode .paint-dropdown{background:var(--cd-s4);border-color:var(--cd-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.crimson-dusk-mode .dropdown-item,body.crimson-dusk-mode .paint-dropdown-item{color:var(--cd-t1);} +body.crimson-dusk-mode .dropdown-item:hover,body.crimson-dusk-mode .paint-dropdown-item:hover{background:var(--cd-s5);} +body.crimson-dusk-mode .dropdown-separator{background:var(--cd-s7);} +body.crimson-dusk-mode .modal-mask{background:rgba(0,0,0,0.65);} +body.crimson-dusk-mode #modal-resize .window,body.crimson-dusk-mode #modal-depth .window,body.crimson-dusk-mode #modal-export .window,body.crimson-dusk-mode #modal-huesat .window,body.crimson-dusk-mode #modal-hotkeys .window,body.crimson-dusk-mode #modal-info .window,body.crimson-dusk-mode #modal-colors .window,body.crimson-dusk-mode #modal-confirm-reset .window,body.crimson-dusk-mode #modal-toolbar .window,body.crimson-dusk-mode #modal-wincolor .window,body.crimson-dusk-mode #modal-tool-customizer .window,body.crimson-dusk-mode .modal-window{background:var(--cd-s3);border-color:var(--cd-s7);color:var(--cd-t1);box-shadow:0 12px 40px rgba(0,0,0,0.7);} +body.crimson-dusk-mode #modal-resize .title-bar,body.crimson-dusk-mode #modal-depth .title-bar,body.crimson-dusk-mode #modal-export .title-bar,body.crimson-dusk-mode #modal-huesat .title-bar,body.crimson-dusk-mode #modal-hotkeys .title-bar,body.crimson-dusk-mode #modal-info .title-bar,body.crimson-dusk-mode #modal-colors .title-bar,body.crimson-dusk-mode #modal-confirm-reset .title-bar,body.crimson-dusk-mode #modal-toolbar .title-bar,body.crimson-dusk-mode #modal-wincolor .title-bar,body.crimson-dusk-mode #modal-tool-customizer .title-bar{background:var(--cd-s4);border-bottom-color:var(--cd-s7);color:var(--cd-t1);} +body.crimson-dusk-mode .close-btn{color:var(--cd-t2);}body.crimson-dusk-mode .close-btn:hover{background:#c42b1c;color:#fff;} +body.crimson-dusk-mode #modal-resize .footer,body.crimson-dusk-mode #modal-depth .footer,body.crimson-dusk-mode #modal-export .footer,body.crimson-dusk-mode #modal-huesat .footer,body.crimson-dusk-mode #modal-hotkeys .footer,body.crimson-dusk-mode #modal-colors .footer{border-top-color:var(--cd-s7);} +body.crimson-dusk-mode .modal-mask .btn,body.crimson-dusk-mode .modal-mask button.btn{background:var(--cd-s5);border-color:var(--cd-s8);color:var(--cd-t1);} +body.crimson-dusk-mode .modal-mask .btn:hover{background:var(--cd-s6);border-color:var(--cd-a2);} +body.crimson-dusk-mode .modal-mask .btn-primary,body.crimson-dusk-mode .modal-mask .btn-ok{background:var(--cd-a2);border-color:#6A1010;color:#fff;} +body.crimson-dusk-mode .modal-mask .btn-primary:hover,body.crimson-dusk-mode .modal-mask .btn-ok:hover{background:#A82020;} +body.crimson-dusk-mode .modal-mask input[type="text"],body.crimson-dusk-mode .modal-mask input[type="number"],body.crimson-dusk-mode .modal-mask select,body.crimson-dusk-mode .modal-mask textarea{background:var(--cd-s5);border-color:var(--cd-s8);color:var(--cd-t1);} +body.crimson-dusk-mode .modal-mask input[type="text"]:focus,body.crimson-dusk-mode .modal-mask input[type="number"]:focus,body.crimson-dusk-mode .modal-mask select:focus{border-color:var(--cd-a1);outline:none;} +body.crimson-dusk-mode .modal-mask label,body.crimson-dusk-mode .modal-mask .row label{color:var(--cd-t1);} +body.crimson-dusk-mode .modal-mask fieldset{border-color:var(--cd-s7);}body.crimson-dusk-mode .modal-mask legend,body.crimson-dusk-mode .modal-mask .section-header{color:var(--cd-t2);} +body.crimson-dusk-mode .modal-mask .section-header{border-bottom-color:var(--cd-s7);} +body.crimson-dusk-mode .hotkey-list{background:var(--cd-s4);border-color:var(--cd-s7);}body.crimson-dusk-mode .hotkey-row{border-bottom-color:var(--cd-s7);} +body.crimson-dusk-mode .hotkey-label{color:var(--cd-t1);}body.crimson-dusk-mode .hotkeys-icon{color:var(--cd-a1);} +body.crimson-dusk-mode #modal-info .info-section{border-bottom-color:var(--cd-s7);}body.crimson-dusk-mode #modal-info .info-title{color:var(--cd-a1);} +body.crimson-dusk-mode #modal-info li,body.crimson-dusk-mode #modal-info p{color:var(--cd-t1);}body.crimson-dusk-mode #modal-info .link-btn{color:var(--cd-a1);} +body.crimson-dusk-mode #modal-colors .color-item{border-bottom-color:var(--cd-s7);} +body.crimson-dusk-mode #modal-colors .color-item:hover{background:var(--cd-s5);border-color:var(--cd-s7);} +body.crimson-dusk-mode #modal-colors .color-item.is-active{background:var(--cd-selected-bg);border-color:var(--cd-selected-border);} +body.crimson-dusk-mode #modal-colors .color-label,body.crimson-dusk-mode #modal-colors .close-btn{color:var(--cd-t1);} +body.crimson-dusk-mode #modal-colors .color-element-label,body.crimson-dusk-mode #modal-colors .select-hint{color:var(--cd-t2);} +body.crimson-dusk-mode #modal-colors .color-input,body.crimson-dusk-mode .preset-select,body.crimson-dusk-mode .preset-input,body.crimson-dusk-mode .search-input{background:var(--cd-s5);border-color:var(--cd-s8);color:var(--cd-t1);} +body.crimson-dusk-mode #modal-colors .color-input.invalid{border-color:#ff6060;background:rgba(255,96,96,0.15);} +body.crimson-dusk-mode #modal-colors .btn{background:var(--cd-s5);border-color:var(--cd-s8);color:var(--cd-t1);} +body.crimson-dusk-mode #modal-colors .btn:hover{background:var(--cd-s6);border-color:var(--cd-a1);} +body.crimson-dusk-mode #modal-colors .color-list{border-color:var(--cd-s7);}body.crimson-dusk-mode #modal-colors .preset-row{background:var(--cd-s4);border-color:var(--cd-s7);} +body.crimson-dusk-mode #modal-colors .editor-grid label{color:var(--cd-t2);}body.crimson-dusk-mode #modal-colors .editor-grid input[type="number"]{background:var(--cd-s5);border-color:var(--cd-s8);color:var(--cd-t1);} +body.crimson-dusk-mode #file-menu.file-menu-template{background:var(--cd-s3);border-color:var(--cd-s7);box-shadow:2px 4px 16px rgba(0,0,0,0.6);} +body.crimson-dusk-mode #file-menu .file-menu-left{background:var(--cd-s4);border-right-color:var(--cd-s7);} +body.crimson-dusk-mode #file-menu .file-menu-item{color:var(--cd-t1);}body.crimson-dusk-mode #file-menu .file-menu-item:hover{background:var(--cd-s5);border-color:var(--cd-s7);} +body.crimson-dusk-mode #file-menu .file-menu-separator{background:var(--cd-s7);}body.crimson-dusk-mode #file-menu .file-menu-right{background:var(--cd-s3);} +body.crimson-dusk-mode #file-menu .file-menu-recent-toggle{background:var(--cd-s4);border-color:var(--cd-s7);color:var(--cd-t2);} +body.crimson-dusk-mode #file-menu .file-menu-recent-toggle:hover{background:var(--cd-s5);color:var(--cd-t1);} +body.crimson-dusk-mode #file-menu .file-menu-recent-header{color:var(--cd-t1);border-bottom-color:var(--cd-s7);} +body.crimson-dusk-mode #file-menu .file-menu-recent-item{color:var(--cd-t2);}body.crimson-dusk-mode #file-menu .file-menu-recent-item:hover{background:var(--cd-s5);color:var(--cd-t1);} +body.crimson-dusk-mode .select-menu{background:var(--cd-s4);border-color:var(--cd-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.crimson-dusk-mode .select-menu-header{background:var(--cd-s4);color:var(--cd-t2);border-bottom-color:var(--cd-s7);} +body.crimson-dusk-mode .select-menu-item{color:var(--cd-t1);}body.crimson-dusk-mode .select-menu-item:hover{background:var(--cd-s5);} +body.crimson-dusk-mode input[type="range"]::-webkit-slider-runnable-track,body.crimson-dusk-mode input[type="range"]::-moz-range-track{background:var(--cd-s6);} +body.crimson-dusk-mode input[type="range"]::-webkit-slider-thumb{background:var(--cd-a1);}body.crimson-dusk-mode input[type="range"]::-moz-range-thumb{background:var(--cd-a1);} +body.crimson-dusk-mode #save-reminder-modal .window,body.crimson-dusk-mode #close-confirm-modal .window{background:var(--cd-s3);border-color:var(--cd-s7);color:var(--cd-t1);} +body.crimson-dusk-mode #lsys-panel{background:var(--cd-s3);border-color:var(--cd-s7);}body.crimson-dusk-mode #lsys-list{background:var(--cd-s4);} +body.crimson-dusk-mode .lsys-layer{background:var(--cd-s4);border-bottom-color:var(--cd-s7);color:var(--cd-t1);}body.crimson-dusk-mode .lsys-layer.active{background:var(--cd-s5);} +body.crimson-dusk-mode .lsys-layer-name{color:var(--cd-t1);}body.crimson-dusk-mode #lsys-toolbar{background:var(--cd-s3);border-top-color:var(--cd-s7);} +body.crimson-dusk-mode .lsys-btn{color:var(--cd-t1);}body.crimson-dusk-mode .lsys-btn:hover{background:var(--cd-s5);} +body.crimson-dusk-mode #status-bar,body.crimson-dusk-mode .status-bar{background:var(--cd-s2);border-top-color:var(--cd-s7);color:var(--cd-t2);} +body.crimson-dusk-mode #palette-bar,body.crimson-dusk-mode .palette-bar{background:var(--cd-s3);border-top-color:var(--cd-s7);} +body.crimson-dusk-mode .mini-swatch.empty{box-shadow:none;} +body.crimson-dusk-mode #modal-wincolor .tab-btn{background:var(--cd-s5);border-color:var(--cd-s7);color:var(--cd-t1);}body.crimson-dusk-mode #modal-wincolor .tab-btn.active{background:var(--cd-a2);color:#fff;} +body.crimson-dusk-mode #swap-colors-btn{background:var(--cd-s4) !important;border-color:var(--cd-s7) !important;color:var(--cd-t1);} +body.crimson-dusk-mode #swap-colors-btn:hover{background:var(--cd-s5) !important;border-color:var(--cd-s8) !important;} + +/* ════════════════════════════════════════════════════════════════ + * GILDED OBSIDIAN - near-black with warm amber-gold accents + * ════════════════════════════════════════════════════════════════ */ +body.gilded-obsidian-mode { + --go-s0:#0C0A06;--go-s1:#130F08;--go-s2:#1A140A;--go-s3:#26200F;--go-s4:#302813;--go-s5:#3C3018;--go-s6:#48391E;--go-s7:#5C4A28;--go-s8:#725E36; + --go-t1:#F2E6C8;--go-t2:#A88A50;--go-t3:#705C30;--go-a1:#D4A845;--go-a2:#7A5C18; + --go-hover-bg:rgba(212,168,69,0.10);--go-hover-border:rgba(212,168,69,0.30); + --go-selected-bg:rgba(212,168,69,0.15);--go-selected-border:rgba(212,168,69,0.40); + --bg-ribbon:var(--go-s3);--border-ribbon:var(--go-s7);--app-text-color:var(--go-t1);--app-bg:var(--go-s1); + --titlebar-bg:var(--go-s2);--titlebar-action-icon:var(--go-t2);--titlebar-action-hover-bg:var(--go-s5);--titlebar-separator:var(--go-s7); + --titlebar-filename:var(--go-t1);--titlebar-control-icon:var(--go-t1);--tab-row-bg:var(--go-s2);--section-divider:var(--go-s7);--section-title-color:var(--go-t2); + --btn-hover-bg:var(--go-hover-bg);--btn-hover-border:var(--go-hover-border);--btn-hover-shadow:rgba(212,168,69,0.15); + --btn-active-bg:var(--go-selected-bg);--btn-active-border:var(--go-selected-border);--btn-active-shadow:rgba(212,168,69,0.25); + --dropdown-bg:var(--go-s4);--dropdown-border:var(--go-s8);--dropdown-shadow:rgba(0,0,0,0.6);--dropdown-item-hover-bg:var(--go-s5); + --dropdown-arrow-color:var(--go-t2);--dropdown-separator:var(--go-s7);--btn-small-bg:var(--go-s4);--btn-small-border:var(--go-s7); + --threshold-label-color:var(--go-t2);--threshold-values-color:var(--go-t1);--toggle-icon-color:var(--go-a1);--toggle-status-color:var(--go-t2); + --hotkeys-icon-color:var(--go-a1);--anchor-toggle-icon-color:var(--go-a1);--theme-mode-toggle-icon-color:var(--go-a1); + --select-menu-header-color:var(--go-t2);--select-menu-header-bg:var(--go-s4);--select-menu-icon-all-fill:rgba(212,168,69,0.18); + --select-menu-icon-all-fill-disabled:var(--go-s5);--select-menu-icon-disabled-stroke:var(--go-t3); + --theme-colors-window-bg:var(--go-s3);--theme-colors-titlebar-bg-start:var(--go-s4);--theme-colors-titlebar-bg-end:var(--go-s3); + --theme-colors-titlebar-border:var(--go-s7);--theme-colors-top-note-color:var(--go-t2);--theme-colors-result-count-color:var(--go-t2); + --theme-colors-list-bg:var(--go-s3);--theme-colors-editor-panel-bg-start:var(--go-s4);--theme-colors-editor-panel-bg-end:var(--go-s3); + --theme-colors-editor-panel-border:var(--go-s7);--theme-colors-editor-title-color:var(--go-t1);--theme-colors-editor-key-color:var(--go-t1); + --theme-colors-editor-input-bg:var(--go-s5);--theme-colors-editor-input-border:var(--go-s8);--theme-colors-editor-swatch-border:var(--go-s8); + --theme-colors-editor-hint-color:var(--go-t3);--theme-colors-mini-toggle-color:var(--go-t1); + --palette-mini-swatch-border:#080604;--palette-mini-swatch-inner:#fff; + --palette-mini-swatch-hover-border:var(--go-a1);--palette-mini-swatch-hover-inner:rgba(212,168,69,0.2); + --view-center-icon-color:var(--go-a1);--view-edgeclean-icon-color:var(--go-a1);--view-theme-icon-color:var(--go-a1); + --select-icon-free-color:var(--go-a1);--select-icon-poly-main-color:var(--go-a1);--select-icon-poly-accent-color:#E8C870; + --bg-hover:var(--go-hover-bg);--border-hover:var(--go-hover-border);--bg-selected:var(--go-selected-bg);--border-selected:var(--go-selected-border); + --bg-canvas:#322A18;--win-blue:var(--go-a1); +} +body.gilded-obsidian-mode{background:var(--go-s1);color:var(--go-t1);} +body.gilded-obsidian-mode #title-bar{background:var(--go-s2);border-bottom:none;} +body.gilded-obsidian-mode .tab-row{background:var(--go-s2);border-bottom-color:var(--go-s7);border-top:1px solid var(--go-s2);color:var(--go-t1);} +body.gilded-obsidian-mode .tab{color:var(--go-t2);border-color:transparent;} +body.gilded-obsidian-mode .tab:not(.file):not(.active):hover{color:var(--go-t1);background:var(--go-s5);border-color:var(--go-s7);} +body.gilded-obsidian-mode .tab.active{background:var(--go-s3);border-color:var(--go-s7);border-bottom-color:var(--go-s3);color:var(--go-t1);} +body.gilded-obsidian-mode .tab-row .dd-arrow{color:var(--go-t1);} +body.gilded-obsidian-mode .ribbon{background:var(--go-s3);border-bottom:1px solid var(--go-s7);} +body.gilded-obsidian-mode .section{border-right-color:var(--go-s7);}body.gilded-obsidian-mode .section-title{color:var(--go-t3);} +body.gilded-obsidian-mode .btn-large{color:var(--go-t1);}body.gilded-obsidian-mode .btn-large:hover{background:var(--go-s5);border-color:var(--go-s7);} +body.gilded-obsidian-mode .btn-large.active,body.gilded-obsidian-mode .btn-large.is-on{background:var(--go-selected-bg);border-color:var(--go-selected-border);} +body.gilded-obsidian-mode .toggle-status{color:var(--go-t2);} +body.gilded-obsidian-mode .toggle-btn .toggle-icons,body.gilded-obsidian-mode #anchor-toggle-btn .toggle-icons,body.gilded-obsidian-mode #theme-mode-btn .toggle-icons{color:var(--go-a1);} +body.gilded-obsidian-mode svg [fill="#0078d7"]{fill:var(--go-a1);}body.gilded-obsidian-mode svg [stroke="#0078d7"]{stroke:var(--go-a1);} +body.gilded-obsidian-mode .ribbon-card{background:var(--go-s4);border-color:var(--go-s7);color:var(--go-t1);} +body.gilded-obsidian-mode .ribbon-card .card-label{color:var(--go-t1);}body.gilded-obsidian-mode .ribbon-card .mini-label{color:var(--go-t2);} +body.gilded-obsidian-mode .compact-input{background:var(--go-s5);border-color:var(--go-s8);color:var(--go-t1);} +body.gilded-obsidian-mode .dropdown-panel,body.gilded-obsidian-mode .paint-dropdown{background:var(--go-s4);border-color:var(--go-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.gilded-obsidian-mode .dropdown-item,body.gilded-obsidian-mode .paint-dropdown-item{color:var(--go-t1);} +body.gilded-obsidian-mode .dropdown-item:hover,body.gilded-obsidian-mode .paint-dropdown-item:hover{background:var(--go-s5);} +body.gilded-obsidian-mode .dropdown-separator{background:var(--go-s7);} +body.gilded-obsidian-mode .modal-mask{background:rgba(0,0,0,0.65);} +body.gilded-obsidian-mode #modal-resize .window,body.gilded-obsidian-mode #modal-depth .window,body.gilded-obsidian-mode #modal-export .window,body.gilded-obsidian-mode #modal-huesat .window,body.gilded-obsidian-mode #modal-hotkeys .window,body.gilded-obsidian-mode #modal-info .window,body.gilded-obsidian-mode #modal-colors .window,body.gilded-obsidian-mode #modal-confirm-reset .window,body.gilded-obsidian-mode #modal-toolbar .window,body.gilded-obsidian-mode #modal-wincolor .window,body.gilded-obsidian-mode #modal-tool-customizer .window,body.gilded-obsidian-mode .modal-window{background:var(--go-s3);border-color:var(--go-s7);color:var(--go-t1);box-shadow:0 12px 40px rgba(0,0,0,0.7);} +body.gilded-obsidian-mode #modal-resize .title-bar,body.gilded-obsidian-mode #modal-depth .title-bar,body.gilded-obsidian-mode #modal-export .title-bar,body.gilded-obsidian-mode #modal-huesat .title-bar,body.gilded-obsidian-mode #modal-hotkeys .title-bar,body.gilded-obsidian-mode #modal-info .title-bar,body.gilded-obsidian-mode #modal-colors .title-bar,body.gilded-obsidian-mode #modal-confirm-reset .title-bar,body.gilded-obsidian-mode #modal-toolbar .title-bar,body.gilded-obsidian-mode #modal-wincolor .title-bar,body.gilded-obsidian-mode #modal-tool-customizer .title-bar{background:var(--go-s4);border-bottom-color:var(--go-s7);color:var(--go-t1);} +body.gilded-obsidian-mode .close-btn{color:var(--go-t2);}body.gilded-obsidian-mode .close-btn:hover{background:#c42b1c;color:#fff;} +body.gilded-obsidian-mode #modal-resize .footer,body.gilded-obsidian-mode #modal-depth .footer,body.gilded-obsidian-mode #modal-export .footer,body.gilded-obsidian-mode #modal-huesat .footer,body.gilded-obsidian-mode #modal-hotkeys .footer,body.gilded-obsidian-mode #modal-colors .footer{border-top-color:var(--go-s7);} +body.gilded-obsidian-mode .modal-mask .btn,body.gilded-obsidian-mode .modal-mask button.btn{background:var(--go-s5);border-color:var(--go-s8);color:var(--go-t1);} +body.gilded-obsidian-mode .modal-mask .btn:hover{background:var(--go-s6);border-color:var(--go-a2);} +body.gilded-obsidian-mode .modal-mask .btn-primary,body.gilded-obsidian-mode .modal-mask .btn-ok{background:var(--go-a2);border-color:#5A4010;color:#fff;} +body.gilded-obsidian-mode .modal-mask .btn-primary:hover,body.gilded-obsidian-mode .modal-mask .btn-ok:hover{background:#9A7820;} +body.gilded-obsidian-mode .modal-mask input[type="text"],body.gilded-obsidian-mode .modal-mask input[type="number"],body.gilded-obsidian-mode .modal-mask select,body.gilded-obsidian-mode .modal-mask textarea{background:var(--go-s5);border-color:var(--go-s8);color:var(--go-t1);} +body.gilded-obsidian-mode .modal-mask input[type="text"]:focus,body.gilded-obsidian-mode .modal-mask input[type="number"]:focus,body.gilded-obsidian-mode .modal-mask select:focus{border-color:var(--go-a1);outline:none;} +body.gilded-obsidian-mode .modal-mask label,body.gilded-obsidian-mode .modal-mask .row label{color:var(--go-t1);} +body.gilded-obsidian-mode .modal-mask fieldset{border-color:var(--go-s7);}body.gilded-obsidian-mode .modal-mask legend,body.gilded-obsidian-mode .modal-mask .section-header{color:var(--go-t2);} +body.gilded-obsidian-mode .modal-mask .section-header{border-bottom-color:var(--go-s7);} +body.gilded-obsidian-mode .hotkey-list{background:var(--go-s4);border-color:var(--go-s7);}body.gilded-obsidian-mode .hotkey-row{border-bottom-color:var(--go-s7);} +body.gilded-obsidian-mode .hotkey-label{color:var(--go-t1);}body.gilded-obsidian-mode .hotkeys-icon{color:var(--go-a1);} +body.gilded-obsidian-mode #modal-info .info-section{border-bottom-color:var(--go-s7);}body.gilded-obsidian-mode #modal-info .info-title{color:var(--go-a1);} +body.gilded-obsidian-mode #modal-info li,body.gilded-obsidian-mode #modal-info p{color:var(--go-t1);}body.gilded-obsidian-mode #modal-info .link-btn{color:var(--go-a1);} +body.gilded-obsidian-mode #modal-colors .color-item{border-bottom-color:var(--go-s7);} +body.gilded-obsidian-mode #modal-colors .color-item:hover{background:var(--go-s5);border-color:var(--go-s7);} +body.gilded-obsidian-mode #modal-colors .color-item.is-active{background:var(--go-selected-bg);border-color:var(--go-selected-border);} +body.gilded-obsidian-mode #modal-colors .color-label,body.gilded-obsidian-mode #modal-colors .close-btn{color:var(--go-t1);} +body.gilded-obsidian-mode #modal-colors .color-element-label,body.gilded-obsidian-mode #modal-colors .select-hint{color:var(--go-t2);} +body.gilded-obsidian-mode #modal-colors .color-input,body.gilded-obsidian-mode .preset-select,body.gilded-obsidian-mode .preset-input,body.gilded-obsidian-mode .search-input{background:var(--go-s5);border-color:var(--go-s8);color:var(--go-t1);} +body.gilded-obsidian-mode #modal-colors .color-input.invalid{border-color:#e81123;background:rgba(232,17,35,0.15);} +body.gilded-obsidian-mode #modal-colors .btn{background:var(--go-s5);border-color:var(--go-s8);color:var(--go-t1);} +body.gilded-obsidian-mode #modal-colors .btn:hover{background:var(--go-s6);border-color:var(--go-a1);} +body.gilded-obsidian-mode #modal-colors .color-list{border-color:var(--go-s7);}body.gilded-obsidian-mode #modal-colors .preset-row{background:var(--go-s4);border-color:var(--go-s7);} +body.gilded-obsidian-mode #modal-colors .editor-grid label{color:var(--go-t2);}body.gilded-obsidian-mode #modal-colors .editor-grid input[type="number"]{background:var(--go-s5);border-color:var(--go-s8);color:var(--go-t1);} +body.gilded-obsidian-mode #file-menu.file-menu-template{background:var(--go-s3);border-color:var(--go-s7);box-shadow:2px 4px 16px rgba(0,0,0,0.6);} +body.gilded-obsidian-mode #file-menu .file-menu-left{background:var(--go-s4);border-right-color:var(--go-s7);} +body.gilded-obsidian-mode #file-menu .file-menu-item{color:var(--go-t1);}body.gilded-obsidian-mode #file-menu .file-menu-item:hover{background:var(--go-s5);border-color:var(--go-s7);} +body.gilded-obsidian-mode #file-menu .file-menu-separator{background:var(--go-s7);}body.gilded-obsidian-mode #file-menu .file-menu-right{background:var(--go-s3);} +body.gilded-obsidian-mode #file-menu .file-menu-recent-toggle{background:var(--go-s4);border-color:var(--go-s7);color:var(--go-t2);} +body.gilded-obsidian-mode #file-menu .file-menu-recent-toggle:hover{background:var(--go-s5);color:var(--go-t1);} +body.gilded-obsidian-mode #file-menu .file-menu-recent-header{color:var(--go-t1);border-bottom-color:var(--go-s7);} +body.gilded-obsidian-mode #file-menu .file-menu-recent-item{color:var(--go-t2);}body.gilded-obsidian-mode #file-menu .file-menu-recent-item:hover{background:var(--go-s5);color:var(--go-t1);} +body.gilded-obsidian-mode .select-menu{background:var(--go-s4);border-color:var(--go-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.gilded-obsidian-mode .select-menu-header{background:var(--go-s4);color:var(--go-t2);border-bottom-color:var(--go-s7);} +body.gilded-obsidian-mode .select-menu-item{color:var(--go-t1);}body.gilded-obsidian-mode .select-menu-item:hover{background:var(--go-s5);} +body.gilded-obsidian-mode input[type="range"]::-webkit-slider-runnable-track,body.gilded-obsidian-mode input[type="range"]::-moz-range-track{background:var(--go-s6);} +body.gilded-obsidian-mode input[type="range"]::-webkit-slider-thumb{background:var(--go-a1);}body.gilded-obsidian-mode input[type="range"]::-moz-range-thumb{background:var(--go-a1);} +body.gilded-obsidian-mode #save-reminder-modal .window,body.gilded-obsidian-mode #close-confirm-modal .window{background:var(--go-s3);border-color:var(--go-s7);color:var(--go-t1);} +body.gilded-obsidian-mode #lsys-panel{background:var(--go-s3);border-color:var(--go-s7);}body.gilded-obsidian-mode #lsys-list{background:var(--go-s4);} +body.gilded-obsidian-mode .lsys-layer{background:var(--go-s4);border-bottom-color:var(--go-s7);color:var(--go-t1);}body.gilded-obsidian-mode .lsys-layer.active{background:var(--go-s5);} +body.gilded-obsidian-mode .lsys-layer-name{color:var(--go-t1);}body.gilded-obsidian-mode #lsys-toolbar{background:var(--go-s3);border-top-color:var(--go-s7);} +body.gilded-obsidian-mode .lsys-btn{color:var(--go-t1);}body.gilded-obsidian-mode .lsys-btn:hover{background:var(--go-s5);} +body.gilded-obsidian-mode #status-bar,body.gilded-obsidian-mode .status-bar{background:var(--go-s2);border-top-color:var(--go-s7);color:var(--go-t2);} +body.gilded-obsidian-mode #palette-bar,body.gilded-obsidian-mode .palette-bar{background:var(--go-s3);border-top-color:var(--go-s7);} +body.gilded-obsidian-mode .mini-swatch.empty{box-shadow:none;} +body.gilded-obsidian-mode #modal-wincolor .tab-btn{background:var(--go-s5);border-color:var(--go-s7);color:var(--go-t1);}body.gilded-obsidian-mode #modal-wincolor .tab-btn.active{background:var(--go-a2);color:#fff;} +body.gilded-obsidian-mode #swap-colors-btn{background:var(--go-s4) !important;border-color:var(--go-s7) !important;color:var(--go-t1);} +body.gilded-obsidian-mode #swap-colors-btn:hover{background:var(--go-s5) !important;border-color:var(--go-s8) !important;} + +/* ════════════════════════════════════════════════════════════════ + * VIOLET HAZE - deep purple-indigo with soft violet accents + * ════════════════════════════════════════════════════════════════ */ +body.violet-haze-mode { + --vh-s0:#0A0810;--vh-s1:#0E0B18;--vh-s2:#130E20;--vh-s3:#1E1635;--vh-s4:#251C42;--vh-s5:#2E2250;--vh-s6:#382860;--vh-s7:#4A3878;--vh-s8:#5E4C90; + --vh-t1:#E0D8F8;--vh-t2:#8878C0;--vh-t3:#584880;--vh-a1:#A888F0;--vh-a2:#4C2A9C; + --vh-hover-bg:rgba(168,136,240,0.10);--vh-hover-border:rgba(168,136,240,0.30); + --vh-selected-bg:rgba(168,136,240,0.15);--vh-selected-border:rgba(168,136,240,0.40); + --bg-ribbon:var(--vh-s3);--border-ribbon:var(--vh-s7);--app-text-color:var(--vh-t1);--app-bg:var(--vh-s1); + --titlebar-bg:var(--vh-s2);--titlebar-action-icon:var(--vh-t2);--titlebar-action-hover-bg:var(--vh-s5);--titlebar-separator:var(--vh-s7); + --titlebar-filename:var(--vh-t1);--titlebar-control-icon:var(--vh-t1);--tab-row-bg:var(--vh-s2);--section-divider:var(--vh-s7);--section-title-color:var(--vh-t2); + --btn-hover-bg:var(--vh-hover-bg);--btn-hover-border:var(--vh-hover-border);--btn-hover-shadow:rgba(168,136,240,0.15); + --btn-active-bg:var(--vh-selected-bg);--btn-active-border:var(--vh-selected-border);--btn-active-shadow:rgba(168,136,240,0.25); + --dropdown-bg:var(--vh-s4);--dropdown-border:var(--vh-s8);--dropdown-shadow:rgba(0,0,0,0.6);--dropdown-item-hover-bg:var(--vh-s5); + --dropdown-arrow-color:var(--vh-t2);--dropdown-separator:var(--vh-s7);--btn-small-bg:var(--vh-s4);--btn-small-border:var(--vh-s7); + --threshold-label-color:var(--vh-t2);--threshold-values-color:var(--vh-t1);--toggle-icon-color:var(--vh-a1);--toggle-status-color:var(--vh-t2); + --hotkeys-icon-color:var(--vh-a1);--anchor-toggle-icon-color:var(--vh-a1);--theme-mode-toggle-icon-color:var(--vh-a1); + --select-menu-header-color:var(--vh-t2);--select-menu-header-bg:var(--vh-s4);--select-menu-icon-all-fill:rgba(168,136,240,0.18); + --select-menu-icon-all-fill-disabled:var(--vh-s5);--select-menu-icon-disabled-stroke:var(--vh-t3); + --theme-colors-window-bg:var(--vh-s3);--theme-colors-titlebar-bg-start:var(--vh-s4);--theme-colors-titlebar-bg-end:var(--vh-s3); + --theme-colors-titlebar-border:var(--vh-s7);--theme-colors-top-note-color:var(--vh-t2);--theme-colors-result-count-color:var(--vh-t2); + --theme-colors-list-bg:var(--vh-s3);--theme-colors-editor-panel-bg-start:var(--vh-s4);--theme-colors-editor-panel-bg-end:var(--vh-s3); + --theme-colors-editor-panel-border:var(--vh-s7);--theme-colors-editor-title-color:var(--vh-t1);--theme-colors-editor-key-color:var(--vh-t1); + --theme-colors-editor-input-bg:var(--vh-s5);--theme-colors-editor-input-border:var(--vh-s8);--theme-colors-editor-swatch-border:var(--vh-s8); + --theme-colors-editor-hint-color:var(--vh-t3);--theme-colors-mini-toggle-color:var(--vh-t1); + --palette-mini-swatch-border:#06040C;--palette-mini-swatch-inner:#fff; + --palette-mini-swatch-hover-border:var(--vh-a1);--palette-mini-swatch-hover-inner:rgba(168,136,240,0.2); + --view-center-icon-color:var(--vh-a1);--view-edgeclean-icon-color:var(--vh-a1);--view-theme-icon-color:var(--vh-a1); + --select-icon-free-color:var(--vh-a1);--select-icon-poly-main-color:var(--vh-a1);--select-icon-poly-accent-color:#C4A8FF; + --bg-hover:var(--vh-hover-bg);--border-hover:var(--vh-hover-border);--bg-selected:var(--vh-selected-bg);--border-selected:var(--vh-selected-border); + --bg-canvas:#2A2047;--win-blue:var(--vh-a1); +} +body.violet-haze-mode{background:var(--vh-s1);color:var(--vh-t1);} +body.violet-haze-mode #title-bar{background:var(--vh-s2);border-bottom:none;} +body.violet-haze-mode .tab-row{background:var(--vh-s2);border-bottom-color:var(--vh-s7);border-top:1px solid var(--vh-s2);color:var(--vh-t1);} +body.violet-haze-mode .tab{color:var(--vh-t2);border-color:transparent;} +body.violet-haze-mode .tab:not(.file):not(.active):hover{color:var(--vh-t1);background:var(--vh-s5);border-color:var(--vh-s7);} +body.violet-haze-mode .tab.active{background:var(--vh-s3);border-color:var(--vh-s7);border-bottom-color:var(--vh-s3);color:var(--vh-t1);} +body.violet-haze-mode .tab-row .dd-arrow{color:var(--vh-t1);} +body.violet-haze-mode .ribbon{background:var(--vh-s3);border-bottom:1px solid var(--vh-s7);} +body.violet-haze-mode .section{border-right-color:var(--vh-s7);}body.violet-haze-mode .section-title{color:var(--vh-t3);} +body.violet-haze-mode .btn-large{color:var(--vh-t1);}body.violet-haze-mode .btn-large:hover{background:var(--vh-s5);border-color:var(--vh-s7);} +body.violet-haze-mode .btn-large.active,body.violet-haze-mode .btn-large.is-on{background:var(--vh-selected-bg);border-color:var(--vh-selected-border);} +body.violet-haze-mode .toggle-status{color:var(--vh-t2);} +body.violet-haze-mode .toggle-btn .toggle-icons,body.violet-haze-mode #anchor-toggle-btn .toggle-icons,body.violet-haze-mode #theme-mode-btn .toggle-icons{color:var(--vh-a1);} +body.violet-haze-mode svg [fill="#0078d7"]{fill:var(--vh-a1);}body.violet-haze-mode svg [stroke="#0078d7"]{stroke:var(--vh-a1);} +body.violet-haze-mode .ribbon-card{background:var(--vh-s4);border-color:var(--vh-s7);color:var(--vh-t1);} +body.violet-haze-mode .ribbon-card .card-label{color:var(--vh-t1);}body.violet-haze-mode .ribbon-card .mini-label{color:var(--vh-t2);} +body.violet-haze-mode .compact-input{background:var(--vh-s5);border-color:var(--vh-s8);color:var(--vh-t1);} +body.violet-haze-mode .dropdown-panel,body.violet-haze-mode .paint-dropdown{background:var(--vh-s4);border-color:var(--vh-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.violet-haze-mode .dropdown-item,body.violet-haze-mode .paint-dropdown-item{color:var(--vh-t1);} +body.violet-haze-mode .dropdown-item:hover,body.violet-haze-mode .paint-dropdown-item:hover{background:var(--vh-s5);} +body.violet-haze-mode .dropdown-separator{background:var(--vh-s7);} +body.violet-haze-mode .modal-mask{background:rgba(0,0,0,0.65);} +body.violet-haze-mode #modal-resize .window,body.violet-haze-mode #modal-depth .window,body.violet-haze-mode #modal-export .window,body.violet-haze-mode #modal-huesat .window,body.violet-haze-mode #modal-hotkeys .window,body.violet-haze-mode #modal-info .window,body.violet-haze-mode #modal-colors .window,body.violet-haze-mode #modal-confirm-reset .window,body.violet-haze-mode #modal-toolbar .window,body.violet-haze-mode #modal-wincolor .window,body.violet-haze-mode #modal-tool-customizer .window,body.violet-haze-mode .modal-window{background:var(--vh-s3);border-color:var(--vh-s7);color:var(--vh-t1);box-shadow:0 12px 40px rgba(0,0,0,0.7);} +body.violet-haze-mode #modal-resize .title-bar,body.violet-haze-mode #modal-depth .title-bar,body.violet-haze-mode #modal-export .title-bar,body.violet-haze-mode #modal-huesat .title-bar,body.violet-haze-mode #modal-hotkeys .title-bar,body.violet-haze-mode #modal-info .title-bar,body.violet-haze-mode #modal-colors .title-bar,body.violet-haze-mode #modal-confirm-reset .title-bar,body.violet-haze-mode #modal-toolbar .title-bar,body.violet-haze-mode #modal-wincolor .title-bar,body.violet-haze-mode #modal-tool-customizer .title-bar{background:var(--vh-s4);border-bottom-color:var(--vh-s7);color:var(--vh-t1);} +body.violet-haze-mode .close-btn{color:var(--vh-t2);}body.violet-haze-mode .close-btn:hover{background:#c42b1c;color:#fff;} +body.violet-haze-mode #modal-resize .footer,body.violet-haze-mode #modal-depth .footer,body.violet-haze-mode #modal-export .footer,body.violet-haze-mode #modal-huesat .footer,body.violet-haze-mode #modal-hotkeys .footer,body.violet-haze-mode #modal-colors .footer{border-top-color:var(--vh-s7);} +body.violet-haze-mode .modal-mask .btn,body.violet-haze-mode .modal-mask button.btn{background:var(--vh-s5);border-color:var(--vh-s8);color:var(--vh-t1);} +body.violet-haze-mode .modal-mask .btn:hover{background:var(--vh-s6);border-color:var(--vh-a2);} +body.violet-haze-mode .modal-mask .btn-primary,body.violet-haze-mode .modal-mask .btn-ok{background:var(--vh-a2);border-color:#361A78;color:#fff;} +body.violet-haze-mode .modal-mask .btn-primary:hover,body.violet-haze-mode .modal-mask .btn-ok:hover{background:#6038C0;} +body.violet-haze-mode .modal-mask input[type="text"],body.violet-haze-mode .modal-mask input[type="number"],body.violet-haze-mode .modal-mask select,body.violet-haze-mode .modal-mask textarea{background:var(--vh-s5);border-color:var(--vh-s8);color:var(--vh-t1);} +body.violet-haze-mode .modal-mask input[type="text"]:focus,body.violet-haze-mode .modal-mask input[type="number"]:focus,body.violet-haze-mode .modal-mask select:focus{border-color:var(--vh-a1);outline:none;} +body.violet-haze-mode .modal-mask label,body.violet-haze-mode .modal-mask .row label{color:var(--vh-t1);} +body.violet-haze-mode .modal-mask fieldset{border-color:var(--vh-s7);}body.violet-haze-mode .modal-mask legend,body.violet-haze-mode .modal-mask .section-header{color:var(--vh-t2);} +body.violet-haze-mode .modal-mask .section-header{border-bottom-color:var(--vh-s7);} +body.violet-haze-mode .hotkey-list{background:var(--vh-s4);border-color:var(--vh-s7);}body.violet-haze-mode .hotkey-row{border-bottom-color:var(--vh-s7);} +body.violet-haze-mode .hotkey-label{color:var(--vh-t1);}body.violet-haze-mode .hotkeys-icon{color:var(--vh-a1);} +body.violet-haze-mode #modal-info .info-section{border-bottom-color:var(--vh-s7);}body.violet-haze-mode #modal-info .info-title{color:var(--vh-a1);} +body.violet-haze-mode #modal-info li,body.violet-haze-mode #modal-info p{color:var(--vh-t1);}body.violet-haze-mode #modal-info .link-btn{color:var(--vh-a1);} +body.violet-haze-mode #modal-colors .color-item{border-bottom-color:var(--vh-s7);} +body.violet-haze-mode #modal-colors .color-item:hover{background:var(--vh-s5);border-color:var(--vh-s7);} +body.violet-haze-mode #modal-colors .color-item.is-active{background:var(--vh-selected-bg);border-color:var(--vh-selected-border);} +body.violet-haze-mode #modal-colors .color-label,body.violet-haze-mode #modal-colors .close-btn{color:var(--vh-t1);} +body.violet-haze-mode #modal-colors .color-element-label,body.violet-haze-mode #modal-colors .select-hint{color:var(--vh-t2);} +body.violet-haze-mode #modal-colors .color-input,body.violet-haze-mode .preset-select,body.violet-haze-mode .preset-input,body.violet-haze-mode .search-input{background:var(--vh-s5);border-color:var(--vh-s8);color:var(--vh-t1);} +body.violet-haze-mode #modal-colors .color-input.invalid{border-color:#e81123;background:rgba(232,17,35,0.15);} +body.violet-haze-mode #modal-colors .btn{background:var(--vh-s5);border-color:var(--vh-s8);color:var(--vh-t1);} +body.violet-haze-mode #modal-colors .btn:hover{background:var(--vh-s6);border-color:var(--vh-a1);} +body.violet-haze-mode #modal-colors .color-list{border-color:var(--vh-s7);}body.violet-haze-mode #modal-colors .preset-row{background:var(--vh-s4);border-color:var(--vh-s7);} +body.violet-haze-mode #modal-colors .editor-grid label{color:var(--vh-t2);}body.violet-haze-mode #modal-colors .editor-grid input[type="number"]{background:var(--vh-s5);border-color:var(--vh-s8);color:var(--vh-t1);} +body.violet-haze-mode #file-menu.file-menu-template{background:var(--vh-s3);border-color:var(--vh-s7);box-shadow:2px 4px 16px rgba(0,0,0,0.6);} +body.violet-haze-mode #file-menu .file-menu-left{background:var(--vh-s4);border-right-color:var(--vh-s7);} +body.violet-haze-mode #file-menu .file-menu-item{color:var(--vh-t1);}body.violet-haze-mode #file-menu .file-menu-item:hover{background:var(--vh-s5);border-color:var(--vh-s7);} +body.violet-haze-mode #file-menu .file-menu-separator{background:var(--vh-s7);}body.violet-haze-mode #file-menu .file-menu-right{background:var(--vh-s3);} +body.violet-haze-mode #file-menu .file-menu-recent-toggle{background:var(--vh-s4);border-color:var(--vh-s7);color:var(--vh-t2);} +body.violet-haze-mode #file-menu .file-menu-recent-toggle:hover{background:var(--vh-s5);color:var(--vh-t1);} +body.violet-haze-mode #file-menu .file-menu-recent-header{color:var(--vh-t1);border-bottom-color:var(--vh-s7);} +body.violet-haze-mode #file-menu .file-menu-recent-item{color:var(--vh-t2);}body.violet-haze-mode #file-menu .file-menu-recent-item:hover{background:var(--vh-s5);color:var(--vh-t1);} +body.violet-haze-mode .select-menu{background:var(--vh-s4);border-color:var(--vh-s7);box-shadow:0 4px 16px rgba(0,0,0,0.6);} +body.violet-haze-mode .select-menu-header{background:var(--vh-s4);color:var(--vh-t2);border-bottom-color:var(--vh-s7);} +body.violet-haze-mode .select-menu-item{color:var(--vh-t1);}body.violet-haze-mode .select-menu-item:hover{background:var(--vh-s5);} +body.violet-haze-mode input[type="range"]::-webkit-slider-runnable-track,body.violet-haze-mode input[type="range"]::-moz-range-track{background:var(--vh-s6);} +body.violet-haze-mode input[type="range"]::-webkit-slider-thumb{background:var(--vh-a1);}body.violet-haze-mode input[type="range"]::-moz-range-thumb{background:var(--vh-a1);} +body.violet-haze-mode #save-reminder-modal .window,body.violet-haze-mode #close-confirm-modal .window{background:var(--vh-s3);border-color:var(--vh-s7);color:var(--vh-t1);} +body.violet-haze-mode #lsys-panel{background:var(--vh-s3);border-color:var(--vh-s7);}body.violet-haze-mode #lsys-list{background:var(--vh-s4);} +body.violet-haze-mode .lsys-layer{background:var(--vh-s4);border-bottom-color:var(--vh-s7);color:var(--vh-t1);}body.violet-haze-mode .lsys-layer.active{background:var(--vh-s5);} +body.violet-haze-mode .lsys-layer-name{color:var(--vh-t1);}body.violet-haze-mode #lsys-toolbar{background:var(--vh-s3);border-top-color:var(--vh-s7);} +body.violet-haze-mode .lsys-btn{color:var(--vh-t1);}body.violet-haze-mode .lsys-btn:hover{background:var(--vh-s5);} +body.violet-haze-mode #status-bar,body.violet-haze-mode .status-bar{background:var(--vh-s2);border-top-color:var(--vh-s7);color:var(--vh-t2);} +body.violet-haze-mode #palette-bar,body.violet-haze-mode .palette-bar{background:var(--vh-s3);border-top-color:var(--vh-s7);} +body.violet-haze-mode .mini-swatch.empty{box-shadow:none;} +body.violet-haze-mode #modal-wincolor .tab-btn{background:var(--vh-s5);border-color:var(--vh-s7);color:var(--vh-t1);}body.violet-haze-mode #modal-wincolor .tab-btn.active{background:var(--vh-a2);color:#fff;} +body.violet-haze-mode #swap-colors-btn{background:var(--vh-s4) !important;border-color:var(--vh-s7) !important;color:var(--vh-t1);} +body.violet-haze-mode #swap-colors-btn:hover{background:var(--vh-s5) !important;border-color:var(--vh-s8) !important;} + +#toast-container{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;} +.toast{padding:8px 16px;border-radius:4px;font-size:13px;font-weight:500;box-shadow:0 2px 8px rgba(0,0,0,0.2);animation:toast-in 0.2s ease-out;pointer-events:auto;max-width:420px;text-align:center;line-height:1.4;} +.toast--info{background:#e8f4fd;color:#1a5276;border:1px solid #aed6f1;} +.toast--error{background:#fdedec;color:#922b21;border:1px solid #f5b7b1;} +.toast--warning{background:#fef9e7;color:#7d6608;border:1px solid #f9e79f;} +@keyframes toast-in{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}} + +/* pokeemerald project asset browser */ +#project-panel{position:fixed;left:-336px;top:145px;bottom:24px;width:320px;z-index:9988;display:flex;flex-direction:column;background:#f5f6f7;color:var(--app-text-color);border-right:1px solid var(--border-ribbon,#dadbdc);border-radius:0;font-size:12px;overflow:hidden;transition:left .24s cubic-bezier(.16,1,.3,1);} +#project-panel:not(.project-collapsed){left:0;} +#project-panel.project-closed{display:none;} +#project-expand{position:fixed;left:0;top:145px;z-index:60;display:none;width:24px;height:24px;border:1px solid var(--border-ribbon,#dadbdc);border-left:none;background:#f5f6f7;color:var(--section-title-color);cursor:pointer;border-radius:0 4px 4px 0;padding:0;transition:background .1s,color .1s;align-items:center;justify-content:center;} +#project-expand:hover{background:var(--win-blue);color:#fff;} +body.project-panel-collapsed #project-expand{display:flex;} +#project-header{display:flex;align-items:center;justify-content:space-between;padding:6px 6px 6px 10px;font-weight:600;color:var(--app-text-color);border-bottom:1px solid color-mix(in srgb,var(--section-divider,#e0e0e0) 80%, transparent);flex-shrink:0;} +#project-title{font-weight:600;letter-spacing:0.4px;font-size:12.5px;} +#project-actions{display:flex;gap:2px;align-items:center;} +#project-actions button{width:24px;height:24px;border:none;background:transparent;cursor:pointer;font-size:13px;line-height:1;border-radius:2px;display:flex;align-items:center;justify-content:center;margin-left:2px;transition:background .12s ease,color .12s ease;} +#project-collapse svg{width:11px;height:11px;flex:none;} +#project-collapse{width:24px;height:24px;margin-left:0;color:var(--section-title-color);} +#project-collapse:hover{background:color-mix(in srgb,var(--app-text-color,#111) 8%, transparent);color:var(--app-text-color);} +#project-close{color:#e81123;} +#project-close:hover{background:#e81123;color:#fff;} +#project-close:active{background:#c20b1c;color:#fff;} +.proj-actionbar{display:flex;gap:4px;padding:4px 6px;border-bottom:1px solid color-mix(in srgb,var(--section-divider,#e0e0e0) 80%, transparent);flex-shrink:0;} +.proj-actionbar button{font-size:10px;padding:2px 6px;border:1px solid var(--btn-small-border);background:var(--btn-small-bg);color:var(--app-text-color);border-radius:3px;cursor:pointer;line-height:1.4;transition:background .12s ease,border-color .12s ease;} +.proj-actionbar button:hover:not(:disabled){background:var(--btn-hover-bg);border-color:var(--btn-hover-border);} +.proj-actionbar button:disabled{opacity:.5;cursor:default;color:var(--section-title-color);} +#project-tree{flex:1;overflow-y:auto;padding:4px 2px;} +#project-tree::-webkit-scrollbar{width:9px;} +#project-tree::-webkit-scrollbar-thumb{background:var(--s6,#cfcfcf);border-radius:5px;border:2px solid var(--s3,#f3f3f3);} +#project-tree::-webkit-scrollbar-thumb:hover{background:var(--s7,#999);} +#project-status{padding:5px 8px;border-top:1px solid var(--s6,#cfcfcf);background:var(--s4,#e8e8e8);font-size:10.5px;word-break:break-all;color:var(--t2,#777);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;} +.proj-empty{padding:10px 8px;color:var(--t2,#777);font-style:italic;} +.proj-dir-header{display:flex;align-items:center;gap:4px;padding:4px 4px;cursor:pointer;border-radius:4px;transition:background .1s;} +.proj-dir-header:hover{background:var(--s5,#dadada);} +.proj-dir-toggle{width:12px;text-align:center;color:var(--a1,#0078d7);flex:none;} +.proj-dir-name{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} +.proj-dir.match > .proj-dir-header .proj-dir-name{color:var(--win-blue);font-weight:700;} +.proj-dir-children{padding-left:12px;} +.proj-row{display:flex;align-items:center;gap:8px;padding:6px 6px;border-radius:4px;cursor:pointer;transition:background .1s;} +.proj-row + .proj-row{border-top:1px solid color-mix(in srgb,var(--section-divider,#e0e0e0) 60%, transparent);} +.proj-row:hover{background:var(--s5,#dadada);} +.proj-row-pal{font-style:italic;} +.proj-thumb-wrap{width:60px;height:60px;flex:none;display:flex;align-items:center;justify-content:center;background:repeating-conic-gradient(#fff 0% 25%,#ddd 0% 50%) 50%/12px 12px;overflow:hidden;border-radius:3px;border:1px solid var(--s6,#cfcfcf);} +.proj-thumb{max-width:60px;max-height:60px;width:60px;height:60px;object-fit:contain;image-rendering:pixelated;display:block;} +.proj-thumb-missing{opacity:0.25;} +.proj-pal-dot{width:12px;height:12px;border-radius:50%;background:var(--a1,#0078d7);flex:none;border:1px solid var(--s7,#999);} +.proj-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} +.proj-pal-badges{display:flex;gap:3px;flex:none;} +.proj-pal-badge{font-size:10px;font-weight:700;padding:1px 5px;border:1px solid var(--s7,#999);background:var(--a1,#0078d7);color:#fff;border-radius:4px;cursor:pointer;line-height:1.4;transition:filter .1s;} +.proj-pal-badge:hover{filter:brightness(1.12);} +.proj-search{margin:6px 8px;border:1px solid var(--s6,#cfcfcf);background:var(--s5,#fff);color:var(--t1,#222);border-radius:5px;padding:5px 7px;font-size:12px;width:calc(100% - 16px);box-sizing:border-box;transition:border-color .1s,box-shadow .1s;} +.proj-search:focus{outline:none;border-color:var(--a1,#0078d7);box-shadow:0 0 0 2px rgba(0,120,215,0.15);} +.proj-overlay{position:absolute;top:105px;left:0;right:0;bottom:27px;display:none;flex-direction:column;align-items:center;justify-content:center;gap:10px;background:color-mix(in srgb,var(--app-text-color,#111) 6%, transparent);z-index:5;animation:proj-pulse 1.2s ease-in-out infinite;pointer-events:none;} +.project-loading .proj-overlay{display:flex;} +.proj-overlay .proj-spinner{width:26px;height:26px;border:3px solid color-mix(in srgb,var(--win-blue) 30%, transparent);border-top-color:var(--win-blue);border-radius:50%;animation:save-spin .7s linear infinite;} +.proj-overlay .proj-loading-text{font-size:11px;color:var(--app-text-color);opacity:.8;} +@keyframes proj-pulse{0%,100%{opacity:1;}50%{opacity:.55;}} + +#palette-panel{position:fixed;right:16px;top:150px;width:276px;max-height:80vh;z-index:70;display:flex;flex-direction:column;background:var(--s3,#f3f3f3);color:var(--t1,#222);border:1px solid var(--s6,#cfcfcf);border-radius:8px;box-shadow:0 6px 24px rgba(0,0,0,0.22);font-size:12px;overflow:hidden;transition:opacity .15s ease;} +#palette-panel.project-collapsed{display:none;} +.pp-header{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-bottom:1px solid var(--s6,#cfcfcf);background:var(--s4,#e8e8e8);cursor:move;user-select:none;} +.pp-title{font-weight:600;letter-spacing:0.3px;} +.pp-close{border:none;background:transparent;font-size:16px;line-height:1;cursor:pointer;color:var(--t1,#222);border-radius:4px;padding:2px 4px;transition:background .1s,color .1s;} +.pp-close:hover{background:#fdecea;color:#a33;} +.pp-add-palette{margin:6px 8px;border:1px dashed var(--s7,#999);background:transparent;color:var(--t1,#222);border-radius:4px;padding:4px;cursor:pointer;font-size:11px;} +.pp-add-palette:hover{background:var(--s4,#e8e8e8);} +.pp-list{flex:1;overflow-y:auto;padding:4px 8px 10px;} +.pp-list::-webkit-scrollbar{width:9px;} +.pp-list::-webkit-scrollbar-thumb{background:var(--s6,#cfcfcf);border-radius:5px;border:2px solid var(--s3,#f3f3f3);} +.pp-list::-webkit-scrollbar-thumb:hover{background:var(--s7,#999);} +.pp-card{border:1px solid var(--s6,#cfcfcf);border-radius:6px;padding:6px;margin-bottom:8px;background:var(--s5,#fff);} +.pp-card.pp-active{border-color:var(--a1,#0078d7);box-shadow:0 0 0 1px var(--a1,#0078d7);} +.pp-card.pp-previewing{border-color:#c77d00;box-shadow:0 0 0 1px #c77d00;} +.pp-actions{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px;} +.pp-actions .pp-btn{flex:1;min-width:44px;text-align:center;} +.pp-name{flex:1;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} +.pp-btn{font-size:10px;padding:2px 5px;border:1px solid var(--s6,#cfcfcf);background:var(--s5,#fff);color:var(--t1,#222);border-radius:3px;cursor:pointer;line-height:1.3;} +.pp-btn:hover{background:var(--s6,#dcdcdc);} +.pp-btn-danger{border-color:#d99;color:#a33;font-weight:700;} +.pp-swatches{display:grid;grid-template-columns:repeat(auto-fill,27px);gap:4px;padding:2px 0;} +.pp-swatches .mini-swatch{width:100% !important;height:auto !important;aspect-ratio:1/1;box-sizing:border-box;border-radius:4px;cursor:grab;} +.pp-swatches .mini-swatch:active{cursor:grabbing;} +#sprite-preview{position:fixed;left:50%;transform:translateX(-50%);bottom:34px;max-width:90vw;z-index:9990;display:flex;flex-direction:column;background:var(--s3,#f3f3f3);color:var(--t1,#222);border:1px solid var(--s6,#cfcfcf);border-radius:8px;box-shadow:0 6px 24px rgba(0,0,0,.22);font-size:12px;overflow:hidden;} +#sprite-preview.sp-collapsed{display:none;} +.sp-header{display:flex;align-items:center;gap:8px;padding:5px 8px;cursor:move;background:var(--s5,#fff);border-bottom:1px solid var(--s6,#cfcfcf);} +.sp-header .sp-title{font-weight:600;} +.sp-header .sp-close{margin-left:auto;border:0;background:transparent;cursor:pointer;font-size:14px;} +.sp-row{display:flex;gap:10px;padding:8px;overflow-x:auto;} +.sp-item{display:flex;flex-direction:column;align-items:center;gap:4px;flex:0 0 auto;} +.sp-name{font-size:10px;color:var(--t1,#444);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} +.sp-canvas{position:relative;image-rendering:pixelated;background:#000;border:1px solid var(--s6,#cfcfcf);border-radius:4px;display:block;} +.pp-swatches .mini-swatch.empty{background:transparent;pointer-events:none;cursor:default;box-shadow:none;border:1px solid transparent;} +.pp-swatches .mini-swatch.empty:hover{box-shadow:none;border:1px solid transparent;} +.pp-swatches .mini-swatch.empty::after{content:'';display:block;width:55%;height:55%;margin:22.5% auto;box-sizing:border-box;border:2px dashed var(--palette-mini-swatch-border,#888);border-radius:50%;opacity:.4;} +.pp-color-input{position:fixed;left:-9999px;top:-9999px;} \ No newline at end of file diff --git a/src/freehand-path-engine.js b/src/freehand-path-engine.js new file mode 100644 index 0000000..8e5bb54 --- /dev/null +++ b/src/freehand-path-engine.js @@ -0,0 +1,571 @@ +// ─── Freehand Path Engine v2 ──────────────────────────────────────────────── +// +// Three-stage smoothing pipeline for high-quality freehand drawing: +// 1. Live draft preview — rAF-scheduled raw lineTo (zero perceptual latency) +// 2. RDP simplification — culls redundant collinear points +// 3. Centripetal CR fit — converts survivors to smooth cubic Béziers +// • α = 0.5 eliminates cusps from uneven sampling rates +// • Corner detection preserves intentional sharp turns +// +// Rendering: +// • Uniform width → single batched Path2D stroke (fast path) +// • Variable width → pressure-driven filled polygon with rounded end-caps +// +// Changes from v1: +// • catmullRomToBezier() upgraded from uniform to centripetal parameterization. +// Uniform CR overshoots when drawing speed changes (pause → fast sweep → pause), +// producing visible cusps or loops. Centripetal weights knot intervals by √chord +// length, keeping the curve well-behaved regardless of input cadence. +// • detectCorners() added — C0 joins at turns sharper than _cornerAngle (~72° default). +// Passed into catmullRomToBezier() so tangents are zeroed at hard corners; the +// user's intentional angle is preserved rather than smoothed away. +// • _renderOutline() added — when minWidth ≠ lineWidth, the committed stroke is +// rendered as a filled polygon whose half-width at each sample is linearly +// interpolated from the stylus pressure of the two surrounding knot points. +// Rounded end-caps are added via arcs at stroke start and end. +// • Draft rendering is rAF-throttled: N pointermove events in one display frame +// collapse to one clearRect + redraw instead of N redundant ones. +// • _rawPoints capped at MAX_RAW_POINTS to prevent unbounded memory on very long +// strokes (4000 pts × 3 px/pt ≈ 12 m at 96 dpi — never hit in practice). +// • onPointerUp fixed: rdpSimplify was called twice (once in _finalize, once to +// build the result object). Now called once; simplified points returned by _finalize. +// • Uniform commit uses a single Path2D (one GPU draw call) instead of per-segment +// beginPath/stroke, which flushed the pipeline for every Bézier segment. +// • init() accepts minWidth, tension, cornerAngle, usePressure for fine-tuning. +// • setPointerCapture(pointerId) should be called on pointerdown to ensure +// pointerup fires even when the cursor leaves the canvas element. +// +const FreehandPathEngine = (() => { + + // ── Tunables ───────────────────────────────────────────────────────── + const MIN_DISTANCE = 3; // px — micro-movement filter threshold + const RDP_TOLERANCE = 1.5; // px — max allowable perpendicular deviation + const MAX_RAW_POINTS = 4000; // hard cap on raw point buffer length + const ALPHA = 0.5; // CR knot exponent: 0=uniform, 0.5=centripetal, 1=chordal + const OUTLINE_SAMPLES = 10; // Bézier samples per segment for variable-width outline + + // ── State ───────────────────────────────────────────────────────────── + let _isDrawing = false; + let _rawPoints = []; // { x, y, pressure } + let _draftCtx = null; + let _mainCtx = null; + let _color = '#000000'; + let _lineWidth = 4; // max stroke width (pressure = 1.0) + let _minWidth = 1; // min stroke width (pressure = 0.0) + let _tension = 0.7; // spline tightness; 1.0 = standard CR, lower = looser + let _cornerAngle = Math.PI * 0.4; // ~72° — turns sharper than this become hard corners + let _usePressure = true; // false → uniform _lineWidth stroke, skip outline path + let _onCommit = null; + + // rAF draft state + let _draftDirty = false; + let _draftRafId = null; + + // ── Helper ──────────────────────────────────────────────────────────── + const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v); + + // ───────────────────────────────────────────────────────────────────── + // Stage 1: Ramer-Douglas-Peucker Simplification + // ───────────────────────────────────────────────────────────────────── + // Recursively culls collinear points while preserving true inflection + // points. The { x, y, pressure } payload is preserved on survivors so + // pressure data stays associated with the correct geometry. + // + function rdpSimplify(points, tolerance) { + if (points.length <= 2) return points.slice(); + + const first = points[0], last = points[points.length - 1]; + const dx = last.x - first.x, dy = last.y - first.y; + const den = Math.hypot(dx, dy); + + // Degenerate: all points coincide — collapse to endpoints + if (den < 1e-6) return [first, last]; + + let maxDist = 0, maxIdx = 0; + for (let i = 1; i < points.length - 1; i++) { + const p = points[i]; + const d = Math.abs(dy * p.x - dx * p.y + last.x * first.y - last.y * first.x) / den; + if (d > maxDist) { maxDist = d; maxIdx = i; } + } + + if (maxDist > tolerance) { + const left = rdpSimplify(points.slice(0, maxIdx + 1), tolerance); + const right = rdpSimplify(points.slice(maxIdx), tolerance); + left.pop(); // drop duplicate junction point + return left.concat(right); + } + return [first, last]; + } + + // ───────────────────────────────────────────────────────────────────── + // Stage 2a: Corner Detection + // ───────────────────────────────────────────────────────────────────── + // Returns a Set of point indices that should receive zero tangent (C0 + // continuity only). The endpoints are always included. An interior point + // is added when the angle between the incoming and outgoing chord vectors + // exceeds `threshold` radians. + // + // Typical threshold: π × 0.4 ≈ 72°. Krita's default is ~60°. + // + function detectCorners(points, threshold) { + const corners = new Set([0, points.length - 1]); + for (let i = 1; i < points.length - 1; i++) { + const ax = points[i].x - points[i-1].x, ay = points[i].y - points[i-1].y; + const bx = points[i+1].x - points[i].x, by = points[i+1].y - points[i].y; + const la = Math.hypot(ax, ay), lb = Math.hypot(bx, by); + if (la < 1e-6 || lb < 1e-6) continue; + const cos = clamp((ax*bx + ay*by) / (la * lb), -1, 1); + const angle = Math.acos(cos); // 0 = straight, π = hairpin U-turn + if (angle > threshold) corners.add(i); + } + return corners; + } + + // ───────────────────────────────────────────────────────────────────── + // Stage 2b: Centripetal Catmull-Rom → Cubic Bézier + // ───────────────────────────────────────────────────────────────────── + // + // WHY centripetal instead of uniform: + // Uniform CR assigns equal parameter intervals to all consecutive point + // pairs regardless of distance. When the user slows down, points bunch + // up; when they speed up, points spread out. In the bunched regions the + // control points overshoot badly, producing cusps or loops that are + // invisible in the raw lineTo draft but appear on commit. + // + // Centripetal CR (ALPHA = 0.5) weights each knot interval by √(chord + // length), which scales the tangent magnitude in proportion to the local + // density — exactly cancelling the overshoot. It is provably loop-free + // and cusp-free for any set of distinct input points. + // + // Tangent formula at Pᵢ (centripetal parameterization): + // + // d₋ = |Pᵢ − Pᵢ₋₁|^α (knot interval before i) + // d₊ = |Pᵢ₊₁ − Pᵢ|^α (knot interval after i) + // + // mᵢ = [(Pᵢ−Pᵢ₋₁)/d₋ − (Pᵢ₊₁−Pᵢ₋₁)/(d₋+d₊) + (Pᵢ₊₁−Pᵢ)/d₊] · d₊ + // + // With α = 0 (uniform), this reduces exactly to the standard + // mᵢ = (Pᵢ₊₁ − Pᵢ₋₁) / 2 + // confirming backward-compatibility. + // + // Endpoints use phantom-mirror neighbours so the same formula applies + // everywhere — no special-casing needed for i=0 or i=n−1. + // + // Bézier control points for segment [Pᵢ, Pᵢ₊₁]: + // CP1 = Pᵢ + (tension/3) · mᵢ + // CP2 = Pᵢ₊₁ − (tension/3) · mᵢ₊₁ + // + // Hard corners (from detectCorners) receive mᵢ = 0, giving a clean cusp + // (C0 join) rather than a smoothed approximation of the user's intent. + // + function catmullRomToBezier(points, tension, corners) { + tension = (tension != null) ? tension : _tension; + const n = points.length; + if (n < 2) return []; + + // Two-point degenerate: straight segment with collinear control points + if (n === 2) { + const p0 = points[0], p3 = points[1]; + const mx = (p3.x - p0.x) / 3, my = (p3.y - p0.y) / 3; + return [{ p0, + p1: { x: p0.x + mx, y: p0.y + my }, + p2: { x: p3.x - mx, y: p3.y - my }, + p3 }]; + } + + const EPS = 1e-5; + + // Precompute knot intervals for real inter-point spans + const kd = new Float64Array(n - 1); + for (let i = 0; i < n - 1; i++) { + kd[i] = Math.pow( + Math.hypot(points[i+1].x - points[i].x, + points[i+1].y - points[i].y), + ALPHA) || EPS; + } + + // Compute tangent vectors at every point + const tx = new Float64Array(n); + const ty = new Float64Array(n); + + for (let i = 0; i < n; i++) { + // Hard corners keep zero tangent → clean C0 cusp + if (corners && corners.has(i)) continue; + + // Phantom-mirror neighbours for endpoint extrapolation: + // P_phantom_prev = 2·P₀ − P₁ (mirrors P₁ through P₀) + // P_phantom_next = 2·Pₙ₋₁ − Pₙ₋₂ + // This ensures the derivative formula is identical for all i. + const px = i > 0 ? points[i-1].x : 2 * points[0].x - points[1].x; + const py = i > 0 ? points[i-1].y : 2 * points[0].y - points[1].y; + const nx_ = i < n-1 ? points[i+1].x : 2 * points[n-1].x - points[n-2].x; + const ny_ = i < n-1 ? points[i+1].y : 2 * points[n-1].y - points[n-2].y; + + const dPx = points[i].x - px, dPy = points[i].y - py; // vector from prev to i + const dNx = nx_ - points[i].x, dNy = ny_ - points[i].y; // vector from i to next + const dFx = nx_ - px, dFy = ny_ - py; // full span prev→next + + const dPrev = Math.pow(Math.hypot(dPx, dPy), ALPHA) || EPS; + const dCur = (i < n-1 ? kd[i] + : Math.pow(Math.hypot(dNx, dNy), ALPHA)) || EPS; + const dSum = dPrev + dCur; + + // Centripetal tangent formula, scaled by dCur for Bézier CP distance + tx[i] = (dPx / dPrev - dFx / dSum + dNx / dCur) * dCur; + ty[i] = (dPy / dPrev - dFy / dSum + dNy / dCur) * dCur; + } + + // Emit cubic Bézier segments + const t3 = tension / 3; + const segs = []; + for (let i = 0; i < n - 1; i++) { + segs.push({ + p0: points[i], + p1: { x: points[i].x + tx[i] * t3, + y: points[i].y + ty[i] * t3 }, + p2: { x: points[i+1].x - tx[i+1] * t3, + y: points[i+1].y - ty[i+1] * t3 }, + p3: points[i+1] + }); + } + return segs; + } + + // ───────────────────────────────────────────────────────────────────── + // Rendering — Uniform width (fast path) + // ───────────────────────────────────────────────────────────────────── + // Batches every Bézier segment into one Path2D and issues a single + // ctx.stroke() call. v1 called beginPath/stroke per segment, flushing + // the GPU command buffer on every iteration. + // + function _renderUniform(ctx, segs, color, lineWidth) { + if (!segs.length) return; + const path = new Path2D(); + path.moveTo(segs[0].p0.x, segs[0].p0.y); + for (const s of segs) { + path.bezierCurveTo(s.p1.x, s.p1.y, s.p2.x, s.p2.y, s.p3.x, s.p3.y); + } + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.stroke(path); + ctx.restore(); + } + + // ───────────────────────────────────────────────────────────────────── + // Rendering — Variable width (pressure path) + // ───────────────────────────────────────────────────────────────────── + // Samples each Bézier segment at OUTLINE_SAMPLES evenly-spaced t values. + // At each sample the perpendicular unit normal is computed from the curve + // tangent, then the centerline point is offset by ±halfWidth to produce + // upper and lower edge vertices. halfWidth is linearly interpolated from + // the pressures of the two knot points that bracket each segment. + // + // The resulting polygon is: + // + // [start cap arc] → upper edge → [end cap arc] → lower edge (reversed) + // → closePath + // + // Arc geometry (canvas y-axis points DOWN): + // The normal vector (nx, ny) = (−dy, dx)/|tangent| points 90° left of + // travel. "upper" = centre + halfW·(nx,ny), "lower" = centre − halfW·(nx,ny). + // + // Start cap: arc(centre, r, angle_to_lower, angle_to_upper, anticlockwise=true) + // → sweeps counterclockwise through the angle behind the stroke start. + // End cap : arc(centre, r, angle_to_upper, angle_to_lower, anticlockwise=true) + // → sweeps counterclockwise through the angle past the stroke end. + // + // Both caps use anticlockwise=true (decreasing angle in canvas coords). + // Verified correct for horizontal, vertical, and diagonal test strokes. + // + function _renderOutline(ctx, segs, simplified, color, minW, maxW) { + // Collect { x, y, nx, ny, halfW } for every outline sample + const pts = []; + + for (let si = 0; si < segs.length; si++) { + const seg = segs[si]; + const pressA = simplified[si] ?.pressure ?? 0.5; + const pressB = simplified[si+1] ?.pressure ?? 0.5; + const jStart = si === 0 ? 0 : 1; // skip duplicate junction on subsequent segs + + for (let j = jStart; j <= OUTLINE_SAMPLES; j++) { + const t = j / OUTLINE_SAMPLES; + const mt = 1 - t; + + // Cubic Bézier position + const bx = mt*mt*mt*seg.p0.x + 3*mt*mt*t*seg.p1.x + + 3*mt*t*t*seg.p2.x + t*t*t*seg.p3.x; + const by = mt*mt*mt*seg.p0.y + 3*mt*mt*t*seg.p1.y + + 3*mt*t*t*seg.p2.y + t*t*t*seg.p3.y; + + // First derivative — tangent direction + const dx = 3 * ( mt*mt*(seg.p1.x - seg.p0.x) + + 2*mt*t*(seg.p2.x - seg.p1.x) + + t*t*(seg.p3.x - seg.p2.x) ); + const dy = 3 * ( mt*mt*(seg.p1.y - seg.p0.y) + + 2*mt*t*(seg.p2.y - seg.p1.y) + + t*t*(seg.p3.y - seg.p2.y) ); + const tlen = Math.hypot(dx, dy); + if (tlen < 1e-6) continue; // skip degenerate tangent (zero-length seg) + + // Unit normal (90° left of travel direction) + const nx = -dy / tlen; + const ny = dx / tlen; + + // Pressure-interpolated half-width; clamp to ≥ 0.5 px for arc stability + const press = pressA + (pressB - pressA) * t; + const halfW = Math.max(0.5, (minW + (maxW - minW) * press) * 0.5); + + pts.push({ x: bx, y: by, nx, ny, halfW }); + } + } + + if (pts.length < 2) return; + + const fp = pts[0]; + const lp = pts[pts.length - 1]; + + ctx.save(); + ctx.fillStyle = color; + ctx.beginPath(); + + // ── Start cap ──────────────────────────────────────────────────── + // moveTo lower[0], then arc anticlockwise through the angle "behind" + // the stroke start to upper[0]. + ctx.moveTo(fp.x - fp.nx * fp.halfW, + fp.y - fp.ny * fp.halfW); + ctx.arc(fp.x, fp.y, fp.halfW, + Math.atan2(-fp.ny, -fp.nx), // angle to lower[0] + Math.atan2( fp.ny, fp.nx), // angle to upper[0] + true); // anticlockwise → wraps behind start + + // ── Upper edge (left-to-right along stroke) ─────────────────────── + for (let i = 1; i < pts.length; i++) { + ctx.lineTo(pts[i].x + pts[i].nx * pts[i].halfW, + pts[i].y + pts[i].ny * pts[i].halfW); + } + + // ── End cap ────────────────────────────────────────────────────── + // Arc anticlockwise from upper[-1] through the angle past the stroke + // end to lower[-1]. + ctx.arc(lp.x, lp.y, lp.halfW, + Math.atan2( lp.ny, lp.nx), // angle to upper[-1] + Math.atan2(-lp.ny, -lp.nx), // angle to lower[-1] + true); // anticlockwise → wraps past end + + // ── Lower edge (right-to-left back to lower[0]) ─────────────────── + for (let i = pts.length - 2; i >= 0; i--) { + ctx.lineTo(pts[i].x - pts[i].nx * pts[i].halfW, + pts[i].y - pts[i].ny * pts[i].halfW); + } + + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + + // ───────────────────────────────────────────────────────────────────── + // Draft Render Pipeline + // ───────────────────────────────────────────────────────────────────── + // Raw lineTo preview on the overlay canvas. Scheduled via rAF so that N + // pointermove events arriving in the same display frame collapse into a + // single clearRect + stroke, not N redundant ones. + // + // The preview uses a fixed average width rather than attempting variable- + // width rendering on every frame — the final outline is committed on + // pointerup and is always accurate. + // + function _renderDraft() { + _draftDirty = false; + _draftRafId = null; + if (!_draftCtx || !_isDrawing) return; + + const ctx = _draftCtx, cvs = ctx.canvas; + ctx.clearRect(0, 0, cvs.width, cvs.height); + if (_rawPoints.length < 2) return; + + ctx.save(); + ctx.strokeStyle = _color; + ctx.lineWidth = _lineWidth; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.globalAlpha = 0.6; // slightly ghosted — signals "preview, not committed" + ctx.beginPath(); + ctx.moveTo(_rawPoints[0].x, _rawPoints[0].y); + for (let i = 1; i < _rawPoints.length; i++) { + ctx.lineTo(_rawPoints[i].x, _rawPoints[i].y); + } + ctx.stroke(); + ctx.restore(); + } + + function _scheduleDraft() { + if (_draftDirty) return; // already queued for this frame + _draftDirty = true; + _draftRafId = requestAnimationFrame(_renderDraft); + } + + function _cancelDraftRaf() { + if (_draftRafId !== null) { + cancelAnimationFrame(_draftRafId); + _draftRafId = null; + _draftDirty = false; + } + } + + function _clearDraft() { + if (_draftCtx) { + _draftCtx.clearRect(0, 0, _draftCtx.canvas.width, _draftCtx.canvas.height); + } + } + + // ───────────────────────────────────────────────────────────────────── + // Final Render Pipeline + // ───────────────────────────────────────────────────────────────────── + // Called on pointerup. Runs: RDP → corner detection → centripetal CR + // → rendering → draft clear. Returns { simplified, bezierSegs } or null + // if the stroke is too short to fit. + // + function _finalize() { + const simplified = rdpSimplify(_rawPoints, RDP_TOLERANCE); + if (simplified.length < 2) return null; + + const corners = detectCorners(simplified, _cornerAngle); + const bezierSegs = catmullRomToBezier(simplified, _tension, corners); + if (bezierSegs.length === 0) return null; + + if (_mainCtx) { + const doPressure = _usePressure && (_lineWidth !== _minWidth); + if (doPressure) { + _renderOutline(_mainCtx, bezierSegs, simplified, _color, _minWidth, _lineWidth); + } else { + _renderUniform(_mainCtx, bezierSegs, _color, _lineWidth); + } + } + + _clearDraft(); + return { simplified, bezierSegs }; + } + + // ── Public API ──────────────────────────────────────────────────────── + + /** + * @param {object} config + * @param {CanvasRenderingContext2D} config.draftCtx Overlay canvas (cleared each frame) + * @param {CanvasRenderingContext2D} config.mainCtx Persistent canvas (receives final stroke) + * @param {function} config.onCommit Called with { rawPoints, simplifiedPoints, bezierSegs } + * @param {string} config.color CSS color string + * @param {number} config.lineWidth Max stroke width in px (pressure=1.0) + * @param {number} config.minWidth Min stroke width in px (pressure=0.0) — enables outline path + * @param {number} config.tension Spline tightness, 0.0–1.0 (default 0.7) + * @param {number} config.cornerAngle Radians; turns sharper than this → hard corner (default ~72°) + * @param {boolean} config.usePressure false → always use uniform lineWidth stroke + */ + function init(config) { + _draftCtx = config.draftCtx ?? null; + _mainCtx = config.mainCtx ?? null; + _onCommit = config.onCommit ?? null; + _color = config.color ?? '#000000'; + _lineWidth = config.lineWidth ?? 4; + _minWidth = config.minWidth ?? 1; + _tension = config.tension ?? 0.7; + _cornerAngle = config.cornerAngle ?? Math.PI * 0.4; + _usePressure = config.usePressure ?? true; + } + + /** + * Call on 'pointerdown'. Also call canvas.setPointerCapture(e.pointerId) + * in the same handler to guarantee pointerup fires if the cursor leaves + * the canvas bounds. + */ + function onPointerDown(pos) { + _isDrawing = true; + _rawPoints = [{ x: pos.x, y: pos.y, pressure: pos.pressure ?? 0.5 }]; + _scheduleDraft(); + } + + /** + * Call on 'pointermove'. Pass e.getCoalescedEvents() as coalescedEvents + * when available (high-frequency stylus events faster than the JS loop). + */ + function onPointerMove(pos, coalescedEvents) { + if (!_isDrawing) return; + + // Inline helper: distance-filter before push; respects MAX_RAW_POINTS cap + const tryPush = (cx, cy, cp) => { + if (_rawPoints.length >= MAX_RAW_POINTS) return; + const last = _rawPoints[_rawPoints.length - 1]; + if (Math.hypot(cx - last.x, cy - last.y) > MIN_DISTANCE) { + _rawPoints.push({ x: cx, y: cy, pressure: cp ?? 0.5 }); + } + }; + + if (coalescedEvents && coalescedEvents.length > 0) { + for (const ce of coalescedEvents) tryPush(ce.x, ce.y, ce.pressure); + } else { + tryPush(pos.x, pos.y, pos.pressure); + } + + _scheduleDraft(); + } + + /** + * Call on 'pointerup' and 'pointercancel'. + * Returns { rawPoints, simplifiedPoints, bezierSegs } or null for a tap + * that produced fewer than 2 distinct points. + */ + function onPointerUp() { + if (!_isDrawing) return null; + _isDrawing = false; + _cancelDraftRaf(); + + if (_rawPoints.length < 2) { + _rawPoints = []; + _clearDraft(); + return null; + } + + const finalData = _finalize(); // single RDP call; also clears draft + + const result = { + rawPoints: _rawPoints.slice(), + simplifiedPoints: finalData?.simplified ?? [], + bezierSegs: finalData?.bezierSegs ?? [] + }; + + _rawPoints = []; + + if (_onCommit && finalData) _onCommit(result); + + return result; + } + + /** Abort the in-progress stroke without committing. */ + function cancel() { + if (!_isDrawing) return; + _isDrawing = false; + _cancelDraftRaf(); + _rawPoints = []; + _clearDraft(); + } + + // Convenience setters (allow per-stroke reconfiguration without full init()) + function setColor(c) { _color = c; } + function setLineWidth(w) { _lineWidth = w; } + function setMinWidth(w) { _minWidth = w; } + function setTension(t) { _tension = t; } + function setCornerAngle(a) { _cornerAngle = a; } + function isActive() { return _isDrawing; } + function getRawPoints() { return _rawPoints.slice(); } + + return { + init, onPointerDown, onPointerMove, onPointerUp, + cancel, onPointerCancel: cancel, // alias for pointercancel handler + setColor, setLineWidth, setMinWidth, setTension, setCornerAngle, + isActive, getRawPoints, + // Exposed for testing / external reuse: + rdpSimplify, catmullRomToBezier, detectCorners + }; +})(); diff --git a/src/index.html b/src/index.html index db4eca5..07b6e9e 100644 --- a/src/index.html +++ b/src/index.html @@ -3,2395 +3,108 @@ - Paint - Professional - - - - - - + CDPaint + + - - -
-
- - - - - - - - Untitled -
-
- - - -
+ +
+ + +
+
+ + + + + + + + Untitled
- +
+ + + +
+
+
File
Home
Misc
+
Themes
Debug
- -
|
- -
+ +
|
+ +
- + Paste
Clipboard
@@ -2400,10 +113,10 @@
-
- - -
+
+ + +
Select @@ -2425,10 +138,11 @@
-
Crop
-
Resize
+
Crop
+
Resize
-
+
+ Rotate