diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 0273da5b..cb43cab8 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -1,14 +1,11 @@ name: Beta Release on: workflow_dispatch: - env: CARGO_TERM_COLOR: always - permissions: contents: write pages: write - jobs: prepare-beta: runs-on: ubuntu-latest @@ -21,7 +18,7 @@ jobs: with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} @@ -29,7 +26,7 @@ jobs: run: | git config --global user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - - uses: knope-dev/action@v2.1.0 + - uses: knope-dev/action@v2.1.2 with: version: 0.22.2 - name: Prepare beta release @@ -42,13 +39,12 @@ jobs: git push echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT echo "faderpunk_version=$(grep '^version' faderpunk/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')" >> $GITHUB_OUTPUT - build-picotool: needs: prepare-beta runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ needs.prepare-beta.outputs.sha }} - name: Install dependencies @@ -67,7 +63,7 @@ jobs: echo "picotool_hash=${picotool_hash}" >> $GITHUB_ENV echo "pico_sdk_hash=${pico_sdk_hash}" >> $GITHUB_ENV - name: Cache picotool build outputs - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: picotool/build key: ${{ runner.os }}-picotool-${{ env.picotool_hash }}-${{ env.pico_sdk_hash }} @@ -97,16 +93,15 @@ jobs: cmake .. make - name: Upload picotool artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: picotool-artifact path: picotool/build/picotool - build-firmware: needs: prepare-beta runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: ref: ${{ needs.prepare-beta.outputs.sha }} - name: Install System Dependencies @@ -121,22 +116,21 @@ jobs: - run: cargo install flip-link - run: cargo build --bin faderpunk --release --target thumbv8m.main-none-eabihf - name: Upload firmware build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: firmware-artifact path: target/thumbv8m.main-none-eabihf/release/faderpunk - package-firmware: needs: [prepare-beta, build-firmware, build-picotool] runs-on: ubuntu-latest steps: - name: Download picotool artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: picotool-artifact path: picotool_executable - name: Download firmware artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: firmware-artifact path: firmware_build @@ -148,20 +142,19 @@ jobs: VERSION="v${{ needs.prepare-beta.outputs.faderpunk_version }}" picotool_executable/picotool uf2 convert artifacts/faderpunk.elf artifacts/faderpunk-${VERSION}.uf2 - name: Upload release artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-artifacts-firmware path: artifacts - build-configurator: needs: prepare-beta runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: ref: ${{ needs.prepare-beta.outputs.sha }} - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22' - name: Setup pnpm @@ -189,57 +182,55 @@ jobs: cd configurator/dist zip -r ../../artifacts/configurator.zip . - name: Upload configurator release artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-artifacts-configurator path: artifacts - name: Upload configurator artifact for GitHub Pages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: configurator-pages-artifact path: configurator/dist - release: needs: [prepare-beta, package-firmware, build-configurator] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: ref: ${{ needs.prepare-beta.outputs.sha }} fetch-depth: 0 - name: Download firmware artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: release-artifacts-firmware path: artifacts - name: Download configurator artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: release-artifacts-configurator path: artifacts - - uses: knope-dev/action@v2.1.0 + - uses: knope-dev/action@v2.1.2 with: version: 0.22.2 - run: knope release --verbose env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - deploy-beta-configurator: needs: [prepare-beta, build-configurator] runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ needs.prepare-beta.outputs.sha }} fetch-depth: 0 - name: Checkout gh-pages branch - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: gh-pages path: gh-pages - name: Download configurator artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: configurator-pages-artifact path: configurator-dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efa2bb81..9228d664 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: name: Formatting, linting, tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -35,9 +35,9 @@ jobs: name: Generate bindings and build configurator runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22' - name: Setup pnpm @@ -64,7 +64,7 @@ jobs: needs: [lint] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install System Dependencies run: | export DEBIAN_FRONTEND=noninteractive diff --git a/.github/workflows/picotool.yml b/.github/workflows/picotool.yml index cb758c7b..cf82acf4 100644 --- a/.github/workflows/picotool.yml +++ b/.github/workflows/picotool.yml @@ -10,7 +10,7 @@ jobs: steps: # Checkout your repository code - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Install dependencies for building picotool and pico-sdk - name: Install dependencies @@ -49,7 +49,7 @@ jobs: # Upload the picotool binary as an artifact - name: Upload picotool artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: picotool path: picotool/build/picotool diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9df2d47e..18186d65 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -12,7 +12,7 @@ jobs: if: "!contains(github.event.head_commit.message, 'chore: prepare release')" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Configure Git @@ -22,7 +22,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: nightly - - uses: knope-dev/action@v2.1.0 + - uses: knope-dev/action@v2.1.2 with: version: 0.22.2 - run: knope prepare-release --verbose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb340efd..eca040ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: configurator: ${{ steps.check.outputs.configurator }} configurator_minor: ${{ steps.check.outputs.configurator_minor }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - id: check @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install dependencies run: | sudo apt update @@ -65,7 +65,7 @@ jobs: echo "picotool_hash=${picotool_hash}" >> $GITHUB_ENV echo "pico_sdk_hash=${pico_sdk_hash}" >> $GITHUB_ENV - name: Cache picotool build outputs - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: picotool/build key: ${{ runner.os }}-picotool-${{ env.picotool_hash }}-${{ env.pico_sdk_hash }} @@ -95,7 +95,7 @@ jobs: cmake .. make - name: Upload picotool artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: picotool-artifact path: picotool/build/picotool @@ -106,7 +106,7 @@ jobs: if: needs.detect-changes.outputs.faderpunk == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install System Dependencies run: | export DEBIAN_FRONTEND=noninteractive @@ -119,7 +119,7 @@ jobs: - run: cargo install flip-link - run: cargo build --bin faderpunk --release --target thumbv8m.main-none-eabihf - name: Upload firmware build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: firmware-artifact path: target/thumbv8m.main-none-eabihf/release/faderpunk @@ -131,12 +131,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Download picotool artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: picotool-artifact path: picotool_executable - name: Download firmware artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: firmware-artifact path: firmware_build @@ -148,7 +148,7 @@ jobs: VERSION="v${{ needs.detect-changes.outputs.faderpunk_version }}" picotool_executable/picotool uf2 convert artifacts/faderpunk.elf artifacts/faderpunk-${VERSION}.uf2 - name: Upload release artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-artifacts-firmware path: artifacts @@ -159,9 +159,9 @@ jobs: if: needs.detect-changes.outputs.configurator == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22' - name: Setup pnpm @@ -188,7 +188,7 @@ jobs: cd configurator/dist zip -r ../../artifacts/configurator.zip . - name: Upload configurator release artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-artifacts-configurator path: artifacts @@ -196,12 +196,12 @@ jobs: run: pnpm run build:landing working-directory: ./configurator - name: Upload configurator artifact for GitHub Pages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: configurator-pages-artifact path: configurator/dist - name: Upload landing page artifact for GitHub Pages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: landing-pages-artifact path: configurator/dist-landing @@ -212,22 +212,22 @@ jobs: if: always() && needs.detect-changes.result == 'success' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Download firmware artifacts if: needs.package-firmware.result == 'success' - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: release-artifacts-firmware path: artifacts - name: Download configurator artifacts if: needs.build-configurator.result == 'success' - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: release-artifacts-configurator path: artifacts - - uses: knope-dev/action@v2.1.0 + - uses: knope-dev/action@v2.1.2 with: version: 0.22.2 - run: knope release --verbose @@ -241,21 +241,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Checkout gh-pages branch - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: gh-pages path: gh-pages - name: Download configurator artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: configurator-pages-artifact path: configurator-dist - name: Download landing page artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: landing-pages-artifact path: landing-dist @@ -304,7 +304,7 @@ jobs: if: needs.detect-changes.outputs.libfp == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: stable diff --git a/Cargo.lock b/Cargo.lock index 3c601867..116afb63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "defmt-rtt" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cac3b8a5644a9e02b75085ebad3b6deafdbdbdec04bb25086523828aa4dfd1" +checksum = "93d5a25c99d89c40f5676bec8cefe0614f17f0f40e916f98e345dae941807f9e" dependencies = [ "critical-section", "defmt 1.0.1", @@ -934,9 +934,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] diff --git a/configurator/package.json b/configurator/package.json index b825ed2f..efe3c21b 100644 --- a/configurator/package.json +++ b/configurator/package.json @@ -38,6 +38,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.9.3", "semver": "^7.7.3", "zod": "^4.1.9", @@ -61,7 +62,7 @@ "tailwindcss": "^4.1.11", "typescript": "~5.8.3", "typescript-eslint": "^8.39.0", - "vite": "^7.1.5" + "vite": "^7.1.11" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/configurator/pnpm-lock.yaml b/configurator/pnpm-lock.yaml index bd85aefc..670c8aba 100644 --- a/configurator/pnpm-lock.yaml +++ b/configurator/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: react-hook-form: specifier: ^7.62.0 version: 7.62.0(react@19.1.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.1.9)(react@19.1.1) react-router-dom: specifier: ^7.9.3 version: 7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -101,7 +104,7 @@ importers: version: 9.33.0 '@tailwindcss/vite': specifier: ^4.1.11 - version: 4.1.11(vite@7.1.5(jiti@2.5.1)(lightningcss@1.30.1)) + version: 4.1.11(vite@7.1.11(jiti@2.5.1)(lightningcss@1.30.1)) '@types/react': specifier: ^19.1.9 version: 19.1.9 @@ -113,7 +116,7 @@ importers: version: 7.7.1 '@vitejs/plugin-react-swc': specifier: ^3.11.0 - version: 3.11.0(@swc/helpers@0.5.17)(vite@7.1.5(jiti@2.5.1)(lightningcss@1.30.1)) + version: 3.11.0(@swc/helpers@0.5.17)(vite@7.1.11(jiti@2.5.1)(lightningcss@1.30.1)) eslint: specifier: ^9.32.0 version: 9.33.0(jiti@2.5.1) @@ -148,8 +151,8 @@ importers: specifier: ^8.39.0 version: 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3) vite: - specifier: ^7.1.5 - version: 7.1.5(jiti@2.5.1)(lightningcss@1.30.1) + specifier: ^7.1.11 + version: 7.1.11(jiti@2.5.1)(lightningcss@1.30.1) packages: @@ -189,158 +192,158 @@ packages: peerDependencies: react: '>=16.8.0' - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1292,113 +1295,124 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rollup/rollup-android-arm-eabi@4.52.0': - resolution: {integrity: sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==} + '@rollup/rollup-android-arm-eabi@4.53.1': + resolution: {integrity: sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.0': - resolution: {integrity: sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==} + '@rollup/rollup-android-arm64@4.53.1': + resolution: {integrity: sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.0': - resolution: {integrity: sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==} + '@rollup/rollup-darwin-arm64@4.53.1': + resolution: {integrity: sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.0': - resolution: {integrity: sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==} + '@rollup/rollup-darwin-x64@4.53.1': + resolution: {integrity: sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.0': - resolution: {integrity: sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==} + '@rollup/rollup-freebsd-arm64@4.53.1': + resolution: {integrity: sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.0': - resolution: {integrity: sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==} + '@rollup/rollup-freebsd-x64@4.53.1': + resolution: {integrity: sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.0': - resolution: {integrity: sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.53.1': + resolution: {integrity: sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==} cpu: [arm] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.52.0': - resolution: {integrity: sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==} + '@rollup/rollup-linux-arm-musleabihf@4.53.1': + resolution: {integrity: sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==} cpu: [arm] os: [linux] + libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.52.0': - resolution: {integrity: sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==} + '@rollup/rollup-linux-arm64-gnu@4.53.1': + resolution: {integrity: sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.52.0': - resolution: {integrity: sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==} + '@rollup/rollup-linux-arm64-musl@4.53.1': + resolution: {integrity: sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==} cpu: [arm64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.52.0': - resolution: {integrity: sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==} + '@rollup/rollup-linux-loong64-gnu@4.53.1': + resolution: {integrity: sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==} cpu: [loong64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.52.0': - resolution: {integrity: sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==} + '@rollup/rollup-linux-ppc64-gnu@4.53.1': + resolution: {integrity: sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.52.0': - resolution: {integrity: sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==} + '@rollup/rollup-linux-riscv64-gnu@4.53.1': + resolution: {integrity: sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==} cpu: [riscv64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.52.0': - resolution: {integrity: sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==} + '@rollup/rollup-linux-riscv64-musl@4.53.1': + resolution: {integrity: sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==} cpu: [riscv64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.52.0': - resolution: {integrity: sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==} + '@rollup/rollup-linux-s390x-gnu@4.53.1': + resolution: {integrity: sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.52.0': - resolution: {integrity: sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==} + '@rollup/rollup-linux-x64-gnu@4.53.1': + resolution: {integrity: sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.52.0': - resolution: {integrity: sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==} + '@rollup/rollup-linux-x64-musl@4.53.1': + resolution: {integrity: sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==} cpu: [x64] os: [linux] + libc: [musl] - '@rollup/rollup-openharmony-arm64@4.52.0': - resolution: {integrity: sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==} + '@rollup/rollup-openharmony-arm64@4.53.1': + resolution: {integrity: sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.0': - resolution: {integrity: sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==} + '@rollup/rollup-win32-arm64-msvc@4.53.1': + resolution: {integrity: sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.0': - resolution: {integrity: sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==} + '@rollup/rollup-win32-ia32-msvc@4.53.1': + resolution: {integrity: sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.0': - resolution: {integrity: sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==} + '@rollup/rollup-win32-x64-gnu@4.53.1': + resolution: {integrity: sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.0': - resolution: {integrity: sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==} + '@rollup/rollup-win32-x64-msvc@4.53.1': + resolution: {integrity: sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==} cpu: [x64] os: [win32] @@ -1425,24 +1439,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.13.3': resolution: {integrity: sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.13.3': resolution: {integrity: sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.13.3': resolution: {integrity: sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.13.3': resolution: {integrity: sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==} @@ -1518,24 +1536,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.11': resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.11': resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.11': resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.11': resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} @@ -1579,12 +1601,27 @@ packages: '@tanstack/virtual-core@3.11.3': resolution: {integrity: sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/react-dom@19.1.7': resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} peerDependencies: @@ -1596,6 +1633,12 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/w3c-web-usb@1.0.12': resolution: {integrity: sha512-GD9XFhJZFtCbspsB3t1vD3SgkWVInIMoL1g1CcE0p3DD7abgLrQ2Ws22RS38CXPUCQXgyKjUAGKdy5d0CLT5jw==} @@ -1658,6 +1701,9 @@ packages: resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react-swc@3.11.0': resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} peerDependencies: @@ -1683,6 +1729,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1700,10 +1749,25 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1736,6 +1800,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -1765,6 +1832,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1772,16 +1842,23 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -1858,10 +1935,16 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1958,6 +2041,15 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1974,12 +2066,24 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + intl-messageformat@10.7.16: resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1988,10 +2092,17 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2048,24 +2159,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -2090,13 +2205,103 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2154,6 +2359,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2251,6 +2459,9 @@ packages: engines: {node: '>=14'} hasBin: true + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2269,6 +2480,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-router-dom@7.9.3: resolution: {integrity: sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==} engines: {node: '>=20.0.0'} @@ -2296,6 +2513,12 @@ packages: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2304,8 +2527,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.52.0: - resolution: {integrity: sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==} + rollup@4.53.1: + resolution: {integrity: sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2341,10 +2564,22 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2385,6 +2620,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2410,6 +2651,24 @@ packages: engines: {node: '>=14.17'} hasBin: true + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2445,8 +2704,14 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vite@7.1.5: - resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.1.11: + resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2523,6 +2788,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@ampproject/remapping@2.3.0': @@ -2564,82 +2832,82 @@ snapshots: react: 19.1.1 tslib: 2.8.1 - '@esbuild/aix-ppc64@0.25.10': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.10': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.10': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.10': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.10': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.10': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.10': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.10': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.10': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.10': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.10': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.10': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.10': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.10': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.10': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.10': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.10': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.10': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.10': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.10': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.10': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.25.10': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.10': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.10': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.10': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.25.10': + '@esbuild/win32-x64@0.25.12': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0(jiti@2.5.1))': @@ -4146,70 +4414,70 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/rollup-android-arm-eabi@4.52.0': + '@rollup/rollup-android-arm-eabi@4.53.1': optional: true - '@rollup/rollup-android-arm64@4.52.0': + '@rollup/rollup-android-arm64@4.53.1': optional: true - '@rollup/rollup-darwin-arm64@4.52.0': + '@rollup/rollup-darwin-arm64@4.53.1': optional: true - '@rollup/rollup-darwin-x64@4.52.0': + '@rollup/rollup-darwin-x64@4.53.1': optional: true - '@rollup/rollup-freebsd-arm64@4.52.0': + '@rollup/rollup-freebsd-arm64@4.53.1': optional: true - '@rollup/rollup-freebsd-x64@4.52.0': + '@rollup/rollup-freebsd-x64@4.53.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.0': + '@rollup/rollup-linux-arm-gnueabihf@4.53.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.0': + '@rollup/rollup-linux-arm-musleabihf@4.53.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.0': + '@rollup/rollup-linux-arm64-gnu@4.53.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.0': + '@rollup/rollup-linux-arm64-musl@4.53.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.0': + '@rollup/rollup-linux-loong64-gnu@4.53.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.0': + '@rollup/rollup-linux-ppc64-gnu@4.53.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.0': + '@rollup/rollup-linux-riscv64-gnu@4.53.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.0': + '@rollup/rollup-linux-riscv64-musl@4.53.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.0': + '@rollup/rollup-linux-s390x-gnu@4.53.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.0': + '@rollup/rollup-linux-x64-gnu@4.53.1': optional: true - '@rollup/rollup-linux-x64-musl@4.52.0': + '@rollup/rollup-linux-x64-musl@4.53.1': optional: true - '@rollup/rollup-openharmony-arm64@4.52.0': + '@rollup/rollup-openharmony-arm64@4.53.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.0': + '@rollup/rollup-win32-arm64-msvc@4.53.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.0': + '@rollup/rollup-win32-ia32-msvc@4.53.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.0': + '@rollup/rollup-win32-x64-gnu@4.53.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.0': + '@rollup/rollup-win32-x64-msvc@4.53.1': optional: true '@swc/core-darwin-arm64@1.13.3': @@ -4333,12 +4601,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.11(vite@7.1.5(jiti@2.5.1)(lightningcss@1.30.1))': + '@tailwindcss/vite@4.1.11(vite@7.1.11(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 7.1.5(jiti@2.5.1)(lightningcss@1.30.1) + vite: 7.1.11(jiti@2.5.1)(lightningcss@1.30.1) '@tanstack/react-virtual@3.11.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: @@ -4348,10 +4616,28 @@ snapshots: '@tanstack/virtual-core@3.11.3': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/react-dom@19.1.7(@types/react@19.1.9)': dependencies: '@types/react': 19.1.9 @@ -4362,6 +4648,10 @@ snapshots: '@types/semver@7.7.1': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/w3c-web-usb@1.0.12': {} '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': @@ -4457,11 +4747,13 @@ snapshots: '@typescript-eslint/types': 8.39.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@7.1.5(jiti@2.5.1)(lightningcss@1.30.1))': + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@7.1.11(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.13.3(@swc/helpers@0.5.17) - vite: 7.1.5(jiti@2.5.1)(lightningcss@1.30.1) + vite: 7.1.11(jiti@2.5.1)(lightningcss@1.30.1) transitivePeerDependencies: - '@swc/helpers' @@ -4484,6 +4776,8 @@ snapshots: argparse@2.0.1: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} brace-expansion@1.1.12: @@ -4501,11 +4795,21 @@ snapshots: callsites@3.1.0: {} + ccount@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chownr@3.0.0: {} classnames@2.5.1: {} @@ -4532,6 +4836,8 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + comma-separated-tokens@2.0.3: {} + compute-scroll-into-view@3.1.1: {} concat-map@0.0.1: {} @@ -4552,45 +4858,55 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-is@0.1.4: {} deepmerge@4.3.1: {} + dequal@2.0.3: {} + detect-libc@2.0.4: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 - esbuild@0.25.10: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escape-string-regexp@4.0.0: {} @@ -4682,8 +4998,12 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + esutils@2.0.3: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -4760,6 +5080,32 @@ snapshots: has-flag@4.0.0: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-url-attributes@3.0.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -4771,6 +5117,8 @@ snapshots: imurmurhash@0.1.4: {} + inline-style-parser@0.2.7: {} + intl-messageformat@10.7.16: dependencies: '@formatjs/ecma402-abstract': 2.3.4 @@ -4778,16 +5126,29 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.2 tslib: 2.8.1 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arrayish@0.3.2: {} + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + isexe@2.0.0: {} jiti@2.5.1: {} @@ -4862,12 +5223,236 @@ snapshots: lodash.merge@4.6.2: {} + longest-streak@3.1.0: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -4922,6 +5507,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -4950,6 +5545,8 @@ snapshots: prettier@3.6.2: {} + property-information@7.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -4963,6 +5560,24 @@ snapshots: dependencies: react: 19.1.1 + react-markdown@10.1.0(@types/react@19.1.9)(react@19.1.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.1.9 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-router-dom@7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 @@ -4988,36 +5603,53 @@ snapshots: react@19.1.1: {} + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + resolve-from@4.0.0: {} reusify@1.1.0: {} - rollup@4.52.0: + rollup@4.53.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.0 - '@rollup/rollup-android-arm64': 4.52.0 - '@rollup/rollup-darwin-arm64': 4.52.0 - '@rollup/rollup-darwin-x64': 4.52.0 - '@rollup/rollup-freebsd-arm64': 4.52.0 - '@rollup/rollup-freebsd-x64': 4.52.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.0 - '@rollup/rollup-linux-arm-musleabihf': 4.52.0 - '@rollup/rollup-linux-arm64-gnu': 4.52.0 - '@rollup/rollup-linux-arm64-musl': 4.52.0 - '@rollup/rollup-linux-loong64-gnu': 4.52.0 - '@rollup/rollup-linux-ppc64-gnu': 4.52.0 - '@rollup/rollup-linux-riscv64-gnu': 4.52.0 - '@rollup/rollup-linux-riscv64-musl': 4.52.0 - '@rollup/rollup-linux-s390x-gnu': 4.52.0 - '@rollup/rollup-linux-x64-gnu': 4.52.0 - '@rollup/rollup-linux-x64-musl': 4.52.0 - '@rollup/rollup-openharmony-arm64': 4.52.0 - '@rollup/rollup-win32-arm64-msvc': 4.52.0 - '@rollup/rollup-win32-ia32-msvc': 4.52.0 - '@rollup/rollup-win32-x64-gnu': 4.52.0 - '@rollup/rollup-win32-x64-msvc': 4.52.0 + '@rollup/rollup-android-arm-eabi': 4.53.1 + '@rollup/rollup-android-arm64': 4.53.1 + '@rollup/rollup-darwin-arm64': 4.53.1 + '@rollup/rollup-darwin-x64': 4.53.1 + '@rollup/rollup-freebsd-arm64': 4.53.1 + '@rollup/rollup-freebsd-x64': 4.53.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.1 + '@rollup/rollup-linux-arm-musleabihf': 4.53.1 + '@rollup/rollup-linux-arm64-gnu': 4.53.1 + '@rollup/rollup-linux-arm64-musl': 4.53.1 + '@rollup/rollup-linux-loong64-gnu': 4.53.1 + '@rollup/rollup-linux-ppc64-gnu': 4.53.1 + '@rollup/rollup-linux-riscv64-gnu': 4.53.1 + '@rollup/rollup-linux-riscv64-musl': 4.53.1 + '@rollup/rollup-linux-s390x-gnu': 4.53.1 + '@rollup/rollup-linux-x64-gnu': 4.53.1 + '@rollup/rollup-linux-x64-musl': 4.53.1 + '@rollup/rollup-openharmony-arm64': 4.53.1 + '@rollup/rollup-win32-arm64-msvc': 4.53.1 + '@rollup/rollup-win32-ia32-msvc': 4.53.1 + '@rollup/rollup-win32-x64-gnu': 4.53.1 + '@rollup/rollup-win32-x64-msvc': 4.53.1 fsevents: 2.3.3 run-parallel@1.2.0: @@ -5046,8 +5678,23 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-json-comments@3.1.1: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5086,6 +5733,10 @@ snapshots: dependencies: is-number: 7.0.0 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -5109,6 +5760,39 @@ snapshots: typescript@5.8.3: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5136,13 +5820,23 @@ snapshots: dependencies: react: 19.1.1 - vite@7.1.5(jiti@2.5.1)(lightningcss@1.30.1): + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: dependencies: - esbuild: 0.25.10 + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.1.11(jiti@2.5.1)(lightningcss@1.30.1): + dependencies: + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.0 + rollup: 4.53.1 tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 @@ -5166,3 +5860,5 @@ snapshots: '@types/react': 19.1.9 react: 19.1.1 use-sync-external-store: 1.5.0(react@19.1.1) + + zwitch@2.0.4: {} diff --git a/configurator/src/components/ManualTab.tsx b/configurator/src/components/ManualTab.tsx index 8db25c93..db988120 100644 --- a/configurator/src/components/ManualTab.tsx +++ b/configurator/src/components/ManualTab.tsx @@ -180,7 +180,7 @@ const apps: ManualAppData[] = [ "Ranges", "Octaves", ], - text: "4x16 step sequencer app featuring four independent sequencers, each represented by a distinct color. Each sequencer has two pages, and you can navigate between them using Shift + Buttons. The CV/Gate outputs are paired per sequencer: jacks 1&2 for sequencer 1, 3&4 for sequencer 2, and so on. MIDI channels for each sequencer can be set individually in the parameters. Faders are used to set note values, buttons define the gate pattern, and long button presses enable legato. Shift modifies settings for the selected sequencer: Shift + Fader 1 sets step length, Fader 2 sets gate length, Fader 3 selects octave, Fader 4 defines the sequence range (1–5 octaves), and Fader 5 sets the sequence resolution (32ndT, 32nd, 16thT, 16th, 8thT, 8th, 4thT, 4th). Buttons are used to select pages, with two pages available per sequencer. The output of each sequencer is quantized to the scale set in the global quantizer.", + text: "4x16 step sequencer app featuring four independent sequencers, each represented by a distinct color. Each sequencer has two pages, and you can navigate between them using Shift + Buttons. The CV/Gate outputs are paired per sequencer: jacks 1&2 for sequencer 1, 3&4 for sequencer 2, and so on. MIDI channels for each sequencer can be set individually in the parameters. Faders are used to set note values, buttons define the gate pattern, and long button presses enable legato. Shift modifies settings for the selected sequencer: Shift + Fader 1 sets step length, Fader 2 sets gate length, Fader 3 selects octave, Fader 4 defines the sequence range (1–5 octaves), and Fader 5 sets the sequence resolution (32ndT, 32nd, 16thT, 16th, 8thT, 8th, 4thT, 4th). In Shift mode, resolution type is color-coded on sequence LEDs: orange for triplet divisions and blue for straight divisions. Buttons are used to select pages, with two pages available per sequencer. The output of each sequencer is quantized to the scale set in the global quantizer.", channels: [ { jackTitle: "CV Output", @@ -324,7 +324,7 @@ const apps: ManualAppData[] = [ "Range", ], storage: ["Attenuation", "Length", "Register", "Resolution"], - text: "This app is inspired by the concept of a Turing machine as used in modular synthesizers—a type of probabilistic sequencer that generates evolving patterns based on controlled randomness. It can be set to send either MIDI CC or MIDI notes, while CV output is always active, sending 0–10V. The fader controls the probability of bit flips: when fully down, the sequence loops without changes; when fully up, bit flips occur constantly and the sequence length doubles; in the middle, there’s a 50/50 chance of flipping, resulting in the most randomness. Holding Shift and pressing the button a number of times sets the sequence length—for example, holding Shift and pressing three times sets a 3-step sequence, which is applied upon releasing Shift. The output is quantized for both CV and MIDI notes according to the global quantizer. Parameters include MIDI channel, base note (lowest MIDI note the Turing machine can generate), gate percentage (MIDI only), and color. Main functions include using the fader to set probability, Shift + Fader to set range, Shift + Button to set sequence length, and Button + Fader to set clock resolution (32ndT, 32nd, 16thT, 16th, 8thT, 8th, 4thT, 4th). All app state is stored in scenes, including the sequences themselves—making this, as far as we know, the only Turing machine with preset saving.", + text: "This app is inspired by the concept of a Turing machine as used in modular synthesizers—a type of probabilistic sequencer that generates evolving patterns based on controlled randomness. It can be set to send either MIDI CC or MIDI notes, while CV output is always active, sending 0–10V. The fader controls the probability of bit flips: when fully down, the sequence loops without changes; when fully up, bit flips occur constantly and the sequence length doubles; in the middle, there’s a 50/50 chance of flipping, resulting in the most randomness. Holding Shift and pressing the button a number of times sets the sequence length—for example, holding Shift and pressing three times sets a 3-step sequence, which is applied upon releasing Shift. The output is quantized for both CV and MIDI notes according to the global quantizer. Parameters include MIDI channel, base note (lowest MIDI note the Turing machine can generate), gate percentage (MIDI only), and color. Main functions include using the fader to set probability, Shift + Fader to set range, Shift + Button to set sequence length, and Button + Fader to set clock resolution (32ndT, 32nd, 16thT, 16th, 8thT, 8th, 4thT, 4th). While setting clock resolution, the bottom LED is orange for triplet divisions and blue for straight divisions. All app state is stored in scenes, including the sequences themselves—making this, as far as we know, the only Turing machine with preset saving.", channels: [ { jackTitle: "Output", @@ -395,7 +395,7 @@ const apps: ManualAppData[] = [ "Muted", "Mode", ], - text: "This app is a Euclidean sequencer with two outputs: Jack 1 delivers the main Euclidean rhythm, while Jack 2 provides either an inverted version or an end-of-rhythm pulse. In inverted mode, if Output 1 sends a pulse, Output 2 does not—and vice versa. Send MIDI triggers, with MIDI channel and MIDI notes. Main functions include Fader 1 for sequence length and Fader 2 for number of beats (fill). Button 1 toggles semitone offset, Button 2 mutes the output. Shift + Fader 1 sets rotation, Shift + Fader 2 sets probability. Button + Fader 1 changes the sequencer speed with available resolutions: 32ndT, 32nd, 16thT, 16th, 8thT, 8th, 4thT, 4th, 2nd, note, half bar, bar.", + text: "This app is a Euclidean sequencer with two outputs: Jack 1 delivers the main Euclidean rhythm, while Jack 2 provides either an inverted version or an end-of-rhythm pulse. In inverted mode, if Output 1 sends a pulse, Output 2 does not—and vice versa. Send MIDI triggers, with MIDI channel and MIDI notes. Main functions include Fader 1 for sequence length and Fader 2 for number of beats (fill). Button 1 toggles semitone offset, Button 2 mutes the output. Shift + Fader 1 sets rotation, Shift + Fader 2 sets probability. Button + Fader 1 changes the sequencer speed with available resolutions: 32ndT, 32nd, 16thT, 16th, 8thT, 8th, 4thT, 4th, 2nd, note, half bar, bar. While setting speed, LED color indicates division type: orange for triplet divisions and blue for straight divisions.", channels: [ { jackTitle: "Trigger 1 Out", @@ -411,7 +411,7 @@ const apps: ManualAppData[] = [ fnDescription: "Fn + Fader changes the sequencer speed", ledTop: "Trigger 1 activity", ledBottom: "", - ledBottomPlusFn: "Clock speed", + ledBottomPlusFn: "Clock speed (orange: triplet, blue: straight)", }, { jackTitle: "Trigger 2 Out", @@ -438,7 +438,7 @@ const apps: ManualAppData[] = [ icon: "die", params: ["MIDI Channel", "MIDI NOTE", "GATE %", "Color"], storage: ["Probability", "Muted", "Resolution"], - text: "This app sends random trigger signals on clock. It can output MIDI notes and CV triggers. The fader sets the probability of a trigger occurring at each clock pulse. The button acts as a mute toggle. Shift + Fader sets the clock resolution, allowing for rhythmic variation.", + text: "This app sends random trigger signals on clock. It can output MIDI notes and CV triggers. The fader sets the probability of a trigger occurring at each clock pulse. The button acts as a mute toggle. Shift + Fader sets the clock resolution, allowing for rhythmic variation. While adjusting resolution, the bottom LED is orange for triplet divisions and blue for straight divisions.", channels: [ { jackTitle: "Trigger Output", @@ -454,6 +454,7 @@ const apps: ManualAppData[] = [ fnPlusShiftDescription: "", ledTop: "Trigger activity indicator", ledBottom: "Flashes with clock", + ledBottomPlusShift: "Resolution type (orange: triplet, blue: straight)", }, ], }, @@ -466,7 +467,7 @@ const apps: ManualAppData[] = [ icon: "note", params: ["MIDI Channel", "Base note", "Span", "GATE %", "Out", "Color"], storage: ["Note", "Resolution", "Muted", "Clocked"], - text: "This app sends MIDI notes and V/Oct voltages in a 0–10V range. The outputted notes are filtered by the global quantizer. The note value is tied to the fader position, with the range set by the span parameter. In clocked mode, the button is a toggle and the app outputs notes on regular intervals set by Button + Fader. In direct mode, the MIDI notes are sent when the button is pressed. Main functions: Fader sets the note; Shift + Fader sets clock resolution; Shift + Button toggles mode—Bottom LED is flashing for clocked mode, off for direct mode.", + text: "This app sends MIDI notes and V/Oct voltages in a 0–10V range. The outputted notes are filtered by the global quantizer. The note value is tied to the fader position, with the range set by the span parameter. In clocked mode, the button is a toggle and the app outputs notes on regular intervals set by Button + Fader. In direct mode, the MIDI notes are sent when the button is pressed. Main functions: Fader sets the note; Shift + Fader sets clock resolution; Shift + Button toggles mode—Bottom LED is flashing for clocked mode, off for direct mode. Resolution type is color-coded as orange for triplet divisions and blue for straight divisions.", channels: [ { jackTitle: "Output", @@ -481,7 +482,7 @@ const apps: ManualAppData[] = [ fnPlusShiftTitle: "Toggles between clocked and direct mode", fnPlusShiftDescription: "", ledTop: "Note output indicator", - ledBottom: "Flashes in clocked mode", + ledBottom: "Flashes in clocked mode (orange: triplet, blue: straight)", }, ], }, @@ -737,7 +738,7 @@ const apps: ManualAppData[] = [ icon: "note-box", params: ["MIDI Channel", "MIDI Note", "GATE %", "Color"], storage: ["Division", "Muted", "Maximum division", "Minimum division"], - text: "This is a simple clock divider app that was suggested by youtuber and Discord member Synthdad. The app allows for a performative control of clock division/multiplication allowing for 'build ups and drops' for example. The maximum and minimum divisions can be user set using shift + fader and button + fader respectively. These are saved into the scenes allowing you to set different ranges depending on your needs.", + text: "This is a simple clock divider app that was suggested by youtuber and Discord member Synthdad. The app allows for a performative control of clock division/multiplication allowing for 'build ups and drops' for example. The maximum and minimum divisions can be user set using shift + fader and button + fader respectively. These are saved into the scenes allowing you to set different ranges depending on your needs. While setting divisions, LEDs indicate the selected type: orange for triplet divisions and blue for straight divisions.", channels: [ { jackTitle: "Trigger out", @@ -751,8 +752,9 @@ const apps: ManualAppData[] = [ fnDescription: "", fnPlusShiftTitle: "Mute", ledTop: "Trigger activity indicator", - ledTopPlusShift: "Maximum division", - ledBottomPlusShift: "Minimum division", + ledTopPlusShift: "Maximum division (orange: triplet, blue: straight)", + ledBottomPlusShift: + "Minimum division (orange: triplet, blue: straight)", ledBottom: "", }, ], @@ -869,6 +871,160 @@ const apps: ManualAppData[] = [ }, ], }, + { + appId: 44, + title: "CV Combine", + description: + "Precision CV adder / combiner / quantizer of two outputs from other apps", + color: "Yellow", + icon: "knob-round", + params: [ + "Enable Jack A", + "Jack A #", + "Enable Jack B", + "Jack B #", + "Color", + "Quantise enable", + "Range", + "Combine Mode", + ], + storage: [ + "Jack A Mute", + "Jack B Mute", + "Quantize", + "CV offset enabled", + "CV offset", + "CV Offset divisor", + ], + text: ` +Combines the output voltages of up to two other specified output jacks belonging to other apps in the same layout. It simulates the behaviour of Eurorack precision adder / CV math modules. + +* Two selected CV output jacks are sampled every millisecond, combined, optionally quantised, optionally applied a voltage offset, then finally re-scales the output signal to the required output voltage range. +* If the final voltage value exceed the min and max of the output Range, the summed CV will be hardclipped to the min or max of the Range. +* The fader sets an CV offset which is applied to the combined CV output after the quantiser. +* By default, the offset is an effective range of -5V to + 5V, in steps of 1V. So if you are combining two v/o pitch signals, this shifts the post-quantized signal from -5 to +5 octaves. The bottom LED shows the level of the offset, with the midpoint (off) representing zero offset, fully lit (blue) representing +5V, and fully lit (red) representing -5V. +* Shift + Fader sets a divisor for the offset (in range 1 at the bottom to 12 at the top) so you can use fractions of a volt as an offset. When the divisor is 12, and the CV is used for V/o pitch, the offset is in units of semitones. +* The offset can be toggled on and off by Shift + long-pressing the button. +* This app does not emit any MIDI events + +#### CV Combine Modes + +The app has 5 modes for combining the CV from the two sampled jacks: +1. **A + B**: Sums the CV from the two jacks together, +2. **A - B**: Subtracts the CV of the second jack from the first, +3. **Max**: Outputs the higher of the two CVs, +4. **Min**: Outputs the lower of the two CVs, +5. **Average**: Outputs the average of the two CVs (if both channels active) + +#### Tip: Steevio Sequencing +Replicate the magic sequencing techniques of the modular musician, Steevio!: +1. Set up a "Sequencer" 8-channel app with two note patterns with different lengths and tempo (e.g. 5 and 7 steps). +2. Place an "CV Combine" app somewhere else on the layout, configuring its "A" and "B" Jack channels to the first two "CV Output" jacks of the Sequencer (channels 1 & 3 on the Sequencer). +3. Set the Combine's output range to 0-10V, turn on Quantize output (to match the fixed 0-10V output range of the Sequencer app). +4. Play with the CV offset to change the played octave +5. Patch the CV Combine output CV to your favourite V/O oscillator + +#### Tip: Muting +Mute and unmute the CV Combine's "A" and "B" channels to bring in either pattern + +#### Tip: Combine Modes +Try out the different Combine Modes for different variations in how the two patterns interact. + +#### Tip: Octave and semitone shifting +* Add some octave shifts (+5 to -5 octave offsets) by moving the bipolar offset fader (no offset in the middle of its range) +* Hold Shift and experiment with different offset divisors. +* With a divisor of 12 (fader at top of its range), the offset is in semitones + +#### Tip: Mix LFOs and Control CVs +Configure the CV Combine to mix an LFO and a Control app together, or two LFOs set to different frequencies, for more complex modulation patterns. Disable the quantizer in this case to preserve the smoothness of the LFO signal. + +#### Tip: Scene Switching +Save different scene settings for a CV Combine app with different offsets or channel mutes, then switch betwen them during a performance. If you also change the scene settings of the upstream CV generating apps, you can get some really interesting switched melodies and modulation. + +#### Acknowledgements. + +* Created by Richard Smith (Discord: Phommed) + `, + channels: [ + { + jackTitle: "CV Output", + jackDescription: "Combined, quantized, offset CV signal", + faderTitle: "CV offset", + faderDescription: "Applies bipolar CV offset after the quantizer", + faderPlusShiftTitle: "Offset Divisor", + faderPlusShiftDescription: + "Divides offset per volt, from 1 (octaves) to 12 (semitones)", + fnTitle: "Mute & offset", + fnDescription: "Short Mute A input- Long Toggle offset", + fnPlusShiftTitle: "Mute", + fnPlusShiftDescription: "Mute B input", + ledTop: "CV output level", + ledTopPlusShift: "Offset divisor (1-12)", + ledBottom: "Offset", + }, + ], + }, + { + appId: 45, + title: "Gate Combine", + description: "Binary logic combination of two gate signals from other apps", + color: "Yellow", + icon: "knob-round", + params: [ + "Enable Jack A", + "Jack A #", + "Enable Jack B", + "Jack B #", + "Color", + "Combine Mode", + ], + storage: ["Jack A Mute", "Jack B Mute", "Gate probability"], + text: ` +Combines two output gates or triggers from other apps into a single combined gate signal, using binary logic. The combined gate signal is then passed through a probabilistic gate that is controlled by the app's fader: +* If the combined signal goes high, and the probablistic gate allows it through, the output gate will go high, and stay high until the combined signal goes low, +* else the output will be low. + + +#### Combine Modes + +Gate Combine has different binary logic modes to generate a separate gate signal from two gate signals that generated by other apps in the same Faderpunk layout: +1. **OR**: Gate output is high if either A or B are high +2. **AND**: Gate output is high only if A and B are both high +3. **XOR**: Gate output is high only if one of A or B are high (but not both at the same time) +4. **NOR**: Gate output is high only if both A and B are low +5. **NAND**: Gate output is low if both A and B are high +6. **XNOR**: Gate output is high if either A & B are both low or both high + +#### Tip: Combine clock-quantized drum triggers +Combine the output gate or trigger signals from two other app channels (e.g. "Euclid" and "Random Trigger") using one of the +combine modes and patch the output gate to a drum voice or envelope trigger. It's a great way of creating more rhythms from +a smaller number of trigger sources. + +#### Tip: Inverted Gate +Configure input channel's A & B to the same gate signal (e.g. from a Euclid app), then use the NAND or NOR combine mode to +get an inverted version of the original gate signal at the output, which can be used to trigger different voices or functions +at the opposite time of the original gate (e.g. trigger an envelope that controls a VCA with a Bass voice) + +#### Acknowledgements. + +* Created by Richard Smith (Discord: Phommed) + `, + channels: [ + { + jackTitle: "Gate out", + jackDescription: "Combined gate output signal", + faderTitle: "Gate chance", + faderDescription: + "Chance of firing new gate after the combine logic (0 - 100%)", + fnTitle: "Mute A", + fnDescription: "Mute A input", + fnPlusShiftTitle: "Mute B", + fnPlusShiftDescription: "Mute B input", + ledTop: "Gate output", + ledBottom: "Chance", + }, + ], + }, ]; export const ManualTab = () => { @@ -902,9 +1058,66 @@ export const ManualTab = () => {
  • Interface + +
  • + Front Panel Overview +
  • +
  • + Additional Controls +
  • +
  • + Global Parameters Access +
  • +
  • + Back Connectors +
  • +
  • + Internal Connectors +
  • +
  • + Important Points +
  • +
  • Configurator + +
  • + Compatible Browsers +
  • +
  • + Device Tab +
  • +
  • + Apps Tab +
  • +
  • + Settings Tab + +
  • + Clock +
  • +
  • + Quantizer +
  • +
  • + MIDI +
  • +
  • + I²C +
  • +
  • + AUX Jacks +
  • +
  • + Miscellaneous +
  • +
  • + Save & Recall Setup +
  • + + +
  • Apps diff --git a/configurator/src/components/manual/Configurator.tsx b/configurator/src/components/manual/Configurator.tsx index fcfe63ce..c58ca24c 100644 --- a/configurator/src/components/manual/Configurator.tsx +++ b/configurator/src/components/manual/Configurator.tsx @@ -15,7 +15,7 @@ export const Configurator = () => ( Chromium-based browser.

    -

    Compatible Browsers with WebUSB Support:

    +

    Compatible Browsers with WebUSB Support:

  • Google Chrome
  • Microsoft Edge
  • @@ -54,7 +54,7 @@ export const Configurator = () => ( -

    Device Tab

    +

    Device Tab

    ( modifications are made.

    -

    Apps Tab

    +

    Apps Tab

    ( Edit Layout pop-up.

    -

    Settings Tab

    +

    Settings Tab

    ( Faderpunk device.

    -

    Clock Section

    +

    Clock Section

    Here you can configure the clock behavior:

  • @@ -214,7 +214,7 @@ export const Configurator = () => (
  • -

    Quantizer

    +

    Quantizer

    Configure the internal quantizer used across all apps:

  • @@ -253,7 +253,7 @@ export const Configurator = () => (

    Refer to each app's manual to check if it uses the global quantizer.

    -

    MIDI

    +

    MIDI

    Here you can configure which MIDI data is transmitted to each MIDI output, essentially allowing Faderpunk to function as a MIDI router. You can also @@ -281,7 +281,7 @@ export const Configurator = () => ( the selected source are sent to this output. -

    I²C Configuration

    +

    I²C Configuration

    Faderpunk can operate as either a Leader or{" "} Follower on the I²C bus. @@ -289,7 +289,7 @@ export const Configurator = () => ( You can set this behavior in the Settings tab.

    -

    AUX Jacks

    +

    AUX Jacks

    Configure AUX jacks as clock outputs or{" "} reset outputs. @@ -310,7 +310,7 @@ export const Configurator = () => (

  • 4 bars
  • -

    Miscellaneous

    +

    Miscellaneous

  • LED Brightness: Adjust the brightness of the device's @@ -340,7 +340,7 @@ export const Configurator = () => (
  • -

    Save & Recall Setup

    +

    Save & Recall Setup

    At the bottom of the Settings tab, you'll find controls for saving and recalling your Faderpunk setup. Keep in mind that scenes are currently{" "} diff --git a/configurator/src/components/manual/Interface.tsx b/configurator/src/components/manual/Interface.tsx index 04694abf..c580c4d0 100644 --- a/configurator/src/components/manual/Interface.tsx +++ b/configurator/src/components/manual/Interface.tsx @@ -12,7 +12,7 @@ import { H2, H3, List } from "./Shared"; export const Interface = () => ( <>

    Interface

    -

    Front Panel Overview

    +

    Front Panel Overview

    Overview of the Faderpunk panel ( Apps can be loaded per channel, and some apps span multiple channels depending on their complexity.

    -

    Additional Controls (Right Side)

    +

    Additional Controls (Right Side)

  • @@ -111,7 +111,7 @@ export const Interface = () => (
  • -

    Global Parameters Access

    +

    Global Parameters Access

    You can adjust several global settings on the Faderpunk by holding the{" "} Scene button and moving specific faders: @@ -330,7 +330,7 @@ export const Interface = () => ( -

    Back Connectors

    +

    Back Connectors

    Faderpunk features a set of connectors on the rear panel, designed to support power, communication, and integration with other gear. The ability @@ -400,7 +400,7 @@ export const Interface = () => ( -

    Internal Connectors Overview

    +

    Internal Connectors Overview

    On the back of the Faderpunk PCB, you'll find a set of user-accessible connectors designed to expand functionality and integration: @@ -441,7 +441,7 @@ export const Interface = () => ( -

    Important Points

    +

    Important Points

  • Configurator Parameters diff --git a/configurator/src/components/manual/ManualApp.tsx b/configurator/src/components/manual/ManualApp.tsx index 1b7a977a..b8cc66fa 100644 --- a/configurator/src/components/manual/ManualApp.tsx +++ b/configurator/src/components/manual/ManualApp.tsx @@ -5,6 +5,7 @@ import { COLORS_CLASSES } from "../../utils/class-helpers"; import { Icon } from "../Icon"; import { AllColors } from "../../utils/types"; import { H3, List } from "./Shared"; +import { Md } from "./Md"; interface ArrowIconProps { className?: string; @@ -42,7 +43,7 @@ export interface ManualAppData { color: AllColors; params?: string[]; storage?: string[]; - text: ReactNode; + text: string; channels: Omit[]; } @@ -60,9 +61,11 @@ const FunctionField = ({ color, title, description }: FunctionFieldProps) => ( "border-b-1 px-2 py-0 font-semibold", )} > - {title} + {title} + +
    + {description}
    -
    {description}
    ); @@ -151,16 +154,19 @@ const Channel = ({ />
    -
    {ledTop}
    +
    + {ledTop} +
    {ledTopPlusShift ? (
    Shift:{" "} - {ledTopPlusShift} + {ledTopPlusShift}
    ) : null} {ledTopPlusFn ? (
    - Fn: {ledTopPlusFn} + Fn:{" "} + {ledTopPlusFn}
    ) : null}
    @@ -190,16 +196,19 @@ const Channel = ({ ) : null}
    -
    {ledBottom}
    +
    + {ledBottom} +
    {ledBottomPlusShift ? (
    Shift:{" "} - {ledBottomPlusShift} + {ledBottomPlusShift}
    ) : null} {ledBottomPlusFn ? (
    - Fn: {ledBottomPlusFn} + Fn:{" "} + {ledBottomPlusFn}
    ) : null}
    @@ -286,7 +295,9 @@ export const ManualApp = ({ app }: Props) => { ) : null} -

    {app.text}

    +

    + {app.text} +

    { + if (typeof children !== "string") return <>{children}; + + return ( + <>{children}, + h4: ({ children }) => ( +

    {children}

    + ), + strong: ({ children }) => ( + {children} + ), + em: ({ children }) => {children}, + code: ({ children }) => ( + + {children} + + ), + ul: ({ children }) => ( +
      {children}
    + ), + ol: ({ children }) => ( +
      {children}
    + ), + a: ({ href, children }) => ( + + {children} + + ), + }} + > + {children} +
    + ); +}; diff --git a/configurator/src/components/manual/Shared.tsx b/configurator/src/components/manual/Shared.tsx index 2bdfe409..d4e701e4 100644 --- a/configurator/src/components/manual/Shared.tsx +++ b/configurator/src/components/manual/Shared.tsx @@ -11,12 +11,16 @@ export const H2 = ({ children, id }: PropsWithChildren) => ( ); -export const H3 = ({ children }: PropsWithChildren) => ( -

    {children}

    +export const H3 = ({ children, id }: PropsWithChildren) => ( +

    + {children} +

    ); -export const H4 = ({ children }: PropsWithChildren) => ( -

    {children}

    +export const H4 = ({ children, id }: PropsWithChildren) => ( +

    + {children} +

    ); export const H5 = ({ children }: PropsWithChildren) => ( diff --git a/faderpunk/Cargo.toml b/faderpunk/Cargo.toml index 78a2f810..ad521226 100644 --- a/faderpunk/Cargo.toml +++ b/faderpunk/Cargo.toml @@ -38,7 +38,7 @@ cortex-m = { version = "0.7.7", features = ["inline-asm"] } cortex-m-rt = "0.7.5" critical-section = "1.2.0" defmt = "1.0.1" -defmt-rtt = "1.0.0" +defmt-rtt = "1.1.0" embedded-hal = "1.0.0" embedded-hal-async = "1.0.0" fm24v10 = "0.1.1" diff --git a/faderpunk/src/app.rs b/faderpunk/src/app.rs index 7954e036..de902673 100644 --- a/faderpunk/src/app.rs +++ b/faderpunk/src/app.rs @@ -9,20 +9,22 @@ use max11300::config::{ }; use midly::{live::LiveEvent, num::u4, MidiMessage, PitchBend}; use portable_atomic::Ordering; +use serde::{Deserialize, Serialize}; use libfp::{ latch::AnalogLatch, quantizer::{Pitch, QuantizerState}, utils::scale_bits_12_7, Brightness, ClockDivision, Color, Key, MidiCc, MidiChannel, MidiIn, MidiNote, MidiOut, Note, - Range, TakeoverMode, + Range, TakeoverMode, GLOBAL_CHANNELS, }; use crate::{ events::{EventPubSubChannel, InputEvent}, + state::{get_gate_jacks, get_in_jacks, get_out_jacks, update_state}, tasks::{ buttons::{is_channel_button_pressed, is_shift_button_pressed}, - clock::{ClockSubscriber, CLOCK_PUBSUB}, + clock::{ClockSubscriber, CLOCK_PUBSUB, TICK_COUNTER}, global_config::get_global_config, i2c::{I2cLeaderMessage, I2cLeaderSender, I2C_CONNECTED}, leds::{set_led_mode, LedMode, LedMsg}, @@ -84,9 +86,10 @@ impl Leds { } } +#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug)] pub struct InJack { - channel: usize, - range: Range, + pub channel: usize, + pub range: Range, } impl InJack { @@ -103,8 +106,9 @@ impl InJack { } } +#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug)] pub struct GateJack { - channel: usize, + pub channel: usize, } impl GateJack { @@ -121,9 +125,10 @@ impl GateJack { } } +#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug)] pub struct OutJack { - channel: usize, - range: Range, + pub channel: usize, + pub range: Range, } impl OutJack { @@ -299,25 +304,20 @@ impl Faders<1> { pub struct Clock { subscriber: ClockSubscriber, - tick_count: u16, } impl Clock { pub fn new() -> Self { let subscriber = CLOCK_PUBSUB.subscriber().unwrap(); - Self { - subscriber, - tick_count: 0, - } + Self { subscriber } } pub async fn wait_for_event(&mut self, division: ClockDivision) -> ClockEvent { loop { match self.subscriber.next_message_pure().await { ClockEvent::Tick => { - self.tick_count += 1; - if self.tick_count >= division as u16 { - self.tick_count = 0; + let ticks = TICK_COUNTER.load(Ordering::Relaxed); + if ticks.is_multiple_of(division as u64) { return ClockEvent::Tick; } } @@ -325,12 +325,21 @@ impl Clock { return ClockEvent::Stop; } clock_event @ ClockEvent::Start | clock_event @ ClockEvent::Reset => { - self.tick_count = 0; return clock_event; } } } } + + #[allow(dead_code)] + pub fn get_ticker(&self) -> fn() -> u64 { + ticks + } +} + +#[allow(dead_code)] +fn ticks() -> u64 { + TICK_COUNTER.load(Ordering::Relaxed) } pub enum SceneEvent { @@ -675,7 +684,15 @@ impl App { ) .await; - InJack::new(self.start_channel + chan, range) + let global_chan = self.start_channel + chan; + let jack = InJack::new(global_chan, range); + // Register new jack in global runtime state + update_state(|s| { + s.in_jacks[global_chan] = Some(jack); + true + }) + .await; + jack } pub async fn make_out_jack(&self, chan: usize, range: Range) -> OutJack { @@ -687,7 +704,15 @@ impl App { self.reconfigure_jack(chan, Mode::Mode5(ConfigMode5(dac_range)), None) .await; - OutJack::new(self.start_channel + chan, range) + let global_chan = self.start_channel + chan; + let jack = OutJack::new(global_chan, range); + // Register new jack in global runtime state + update_state(|s| { + s.out_jacks[global_chan] = Some(jack); + true + }) + .await; + jack } pub async fn make_gate_jack(&self, chan: usize, level: u16) -> GateJack { @@ -695,7 +720,59 @@ impl App { self.reconfigure_jack(chan, Mode::Mode3(ConfigMode3), Some(level)) .await; - GateJack::new(self.start_channel + chan) + let global_chan = self.start_channel + chan; + let jack = GateJack::new(global_chan); + // Register new jack in global runtime state + update_state(|s| { + s.gate_jacks[chan] = Some(jack); + true + }) + .await; + jack + } + + /// Obtain current output value from any CV jack global channel, 0-based (not an app-specific channel) + /// If output jack voltage range is 0-10V or -5 to +5V, return value is in range 0-4095 + /// If output jack voltage range in 0-5V, return value is in range 0-2047 + /// + /// If you point this at a gate out jack by mistake, it will return 0. + /// + pub fn get_out_global_jack_value(global_chan: usize) -> u16 { + let chan = global_chan.clamp(0, GLOBAL_CHANNELS - 1); + MAX_VALUES_DAC[chan].load(Ordering::Relaxed) + } + + /// Obtain current gate value from any Gate Jack global channel, 0-based (not an app-specific channel). + /// If gate is hi, will return true + /// If gate is lo, will return false + pub fn get_out_global_gate_jack_is_high(global_chan: usize) -> bool { + let chan = global_chan.clamp(0, GLOBAL_CHANNELS - 1); + let gate = MAX_TRIGGERS_GPO[chan].load(Ordering::Relaxed); + gate == 4 + } + + /// Gets a possible copy of the stored config of a given global CV output jack channel, if any + #[allow(unused)] + pub async fn get_out_jack_config(global_chan: usize) -> Option { + let chan = global_chan.clamp(0, GLOBAL_CHANNELS - 1); + let jacks = get_out_jacks().await; + jacks[chan] + } + + /// Gets a possible copy of the stored config of a given global Gate output jack channel, if any + #[allow(unused)] + pub async fn get_gate_jack_config(global_chan: usize) -> Option { + let chan = global_chan.clamp(0, GLOBAL_CHANNELS - 1); + let jacks = get_gate_jacks().await; + jacks[chan] + } + + /// Gets a possible copy of the stored config of a given global Gate input jack channel, if any + #[allow(unused)] + pub async fn get_in_jack_config(global_chan: usize) -> Option { + let chan = global_chan.clamp(0, GLOBAL_CHANNELS - 1); + let jacks = get_in_jacks().await; + jacks[chan] } pub async fn delay_millis(&self, millis: u64) { diff --git a/faderpunk/src/apps/clk_div.rs b/faderpunk/src/apps/clk_div.rs index 4b682bdd..5308b38e 100644 --- a/faderpunk/src/apps/clk_div.rs +++ b/faderpunk/src/apps/clk_div.rs @@ -146,6 +146,7 @@ pub async fn run( params.query(|p| (p.midi_out, p.midi_channel, p.note, p.gatel as u32, p.color)); let mut clock = app.use_clock(); + let ticks = clock.get_ticker(); let fader = app.use_faders(); let buttons = app.use_buttons(); let leds = app.use_leds(); @@ -162,8 +163,6 @@ pub async fn run( let resolution = [384, 192, 96, 48, 24, 16, 12, 8, 6, 4, 3, 2]; - let mut clkn: u32 = 0; - let (res, mute, min, max) = storage.query(|s| (s.fader_saved, s.mute_saved, s.min_div, s.max_div)); @@ -186,7 +185,11 @@ pub async fn run( loop { match clock.wait_for_event(ClockDivision::_1).await { ClockEvent::Reset => { - clkn = 0; + midi.send_note_off(note).await; + note_on = false; + jack.set_low().await; + } + ClockEvent::Stop => { midi.send_note_off(note).await; note_on = false; jack.set_low().await; @@ -194,11 +197,16 @@ pub async fn run( ClockEvent::Tick => { let muted = glob_muted.get(); let div = div_glob.get(); + let clkn = ticks() as u32; if clkn.is_multiple_of(div) && !muted { jack.set_high().await; if glob_latch_layer.get() == LatchLayer::Main { - leds.set(0, Led::Top, led_color, LED_BRIGHTNESS); + if matches!(div, 2 | 4 | 8 | 16) { + leds.set(0, Led::Bottom, Color::Orange, Brightness::High); + } else { + leds.set(0, Led::Bottom, Color::Blue, Brightness::High); + } } midi.send_note_on(note, 4095).await; note_on = true; @@ -235,8 +243,6 @@ pub async fn run( leds.set(0, Led::Bottom, Color::Red, LED_BRIGHTNESS); } } - - clkn += 1; } _ => {} } diff --git a/faderpunk/src/apps/cvcombine.rs b/faderpunk/src/apps/cvcombine.rs new file mode 100644 index 00000000..a0fb3b7a --- /dev/null +++ b/faderpunk/src/apps/cvcombine.rs @@ -0,0 +1,479 @@ +//! # CV Combine app +//! +//! Precision CV adder / combiner / quantizer of one or two other output variable CV jacks belonging to other apps +//! +//! This app is able to sum the output voltages of up to two other specified output jacks belonging to other apps in the same layout. +//! It simulates the behaviour of Eurorack precision adder / CV math modules. +//! +//! Created by Richard Smith (@phommed on Faderpunk Discord) in February 2026. +//! +use embassy_futures::{ + join::{join5}, select::{select, select3} +}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; +use heapless::Vec; +use libfp::{ + APP_MAX_PARAMS, AppIcon, Brightness, Color, Config, GLOBAL_CHANNELS, latch::LatchLayer, +Param, Range, Value, ext::FromValue}; + +use libm::roundf; +use serde::{Deserialize, Serialize}; + +use crate::app::{App, AppParams, AppStorage, Led, ManagedStorage, ParamStore, SceneEvent }; + +pub const CHANNELS: usize = 1; +pub const PARAMS: usize = 8; + +const _5V:i16 = 2047; +const _10V:i16 = 4095; +const V_PER_OCTAVE:f32 = 409.5; + +// App configuration visible to the configurator +pub static CONFIG: Config = Config::new( + "CV Combine", + "Adds two CV outputs from other channels, with octave offset and optional global quantization", + Color::Yellow, + AppIcon::KnobRound, +) +.add_param(Param::bool { name: "Enable Jack A" }) +.add_param(Param::i32 { name: "Jack A #", min: 1, max: GLOBAL_CHANNELS as i32 }) +.add_param(Param::bool { name: "Enable Jack B" }) +.add_param(Param::i32 { name: "Jack B #", min: 1, max: GLOBAL_CHANNELS as i32 }) +.add_param(Param::Color { + name: "Color", + variants: &[ + Color::Blue, + Color::Green, + Color::Rose, + Color::Orange, + Color::Cyan, + Color::Pink, + Color::Violet, + Color::Yellow, + ], +}) +.add_param(Param::Range { + name: "Range", + variants: &[Range::_0_10V, Range::_0_5V, Range::_Neg5_5V], +}).add_param(Param::bool { name: "Quantize output" }) +.add_param(Param::Enum { + name: "Combine Mode", + variants: &["A+B", "A-B", "Max", "Min", "Average"]}); + +pub struct Params { + // Will be added if = true + channel_a_enabled: bool, + // Output jack number 1 - GLOBAL_CHANNELS to be sampled + channel_a_jack_num: i32, + // Will be added if = true + channel_b_enabled: bool, + // Output jack number 1 - GLOBAL_CHANNELS to be sampled + channel_b_jack_num: i32, + // LED colour + color: Color, + // Output CV range + range: Range, + // Quantize output = true + quantize: bool, + // Output combination mode + combine_mode: usize, +} + +impl Default for Params { + fn default() -> Self { + Self { + channel_a_enabled: false, + channel_a_jack_num: 1, + channel_b_enabled: false, + channel_b_jack_num: 1, + color: Color::Yellow, + range: Range::_0_10V, + quantize: false, + combine_mode: 0, // A + B + } + } +} + +impl AppParams for Params { + fn from_values(values: &[Value]) -> Option { + if values.len() < PARAMS { + return None; + } + Some(Self { + channel_a_enabled: bool::from_value(values[0]), + channel_a_jack_num: i32::from_value(values[1]), + channel_b_enabled: bool::from_value(values[2]), + channel_b_jack_num: i32::from_value(values[3]), + color: Color::from_value(values[4]), + range: Range::from_value(values[5]), + quantize: bool::from_value(values[6]), + combine_mode: usize::from_value(values[7]), + }) + } + + fn to_values(&self) -> Vec { + let mut vec = Vec::new(); + vec.push(self.channel_a_enabled.into()).unwrap(); + vec.push(self.channel_a_jack_num.into()).unwrap(); + vec.push(self.channel_b_enabled.into()).unwrap(); + vec.push(self.channel_b_jack_num.into()).unwrap(); + vec.push(self.color.into()).unwrap(); + vec.push(self.range.into()).unwrap(); + vec.push(self.quantize.into()).unwrap(); + vec.push(self.combine_mode.into()).unwrap(); + vec + } +} + +#[derive(Serialize, Deserialize)] +pub struct Storage { + channel_a_mute_saved: bool, + channel_b_mute_saved: bool, + offset_enabled_saved:bool, + offset_voltage_saved: u16, + offset_divisor_saved: u16, +} + +impl Default for Storage { + fn default() -> Self { + Self { + channel_a_mute_saved: false, + channel_b_mute_saved: false, + offset_enabled_saved: true, + offset_voltage_saved: 0, + offset_divisor_saved: 0, + } + } +} + +impl AppStorage for Storage {} + +// Wrapper task - required for all apps +#[embassy_executor::task(pool_size = 16/CHANNELS)] +pub async fn wrapper(app: App, exit_signal: &'static Signal) { + let param_store = ParamStore::::new(app.app_id, app.layout_id); + let storage = ManagedStorage::::new(app.app_id, app.layout_id); + + param_store.load().await; + storage.load().await; + + let app_loop = async { + loop { + select3( + run(&app, ¶m_store, &storage), + param_store.param_handler(), + storage.saver_task(), + ) + .await; + } + }; + + select(app_loop, app.exit_handler(exit_signal)).await; +} + +// Main app logic +pub async fn run(app: &App, + params: &ParamStore, + storage: &ManagedStorage, +) { + + + // first_channel and second_channel params are converted to usize in range 0 - (GLOBAL_CHANNELS-1) + let (channel_a_enabled, channel_a_jack_num, channel_b_enabled, channel_b_jack_num, led_color, range, quantize, combine_mode) = params + .query(|p| { + ( + p.channel_a_enabled, + p.channel_a_jack_num, + p.channel_b_enabled, + p.channel_b_jack_num, + p.color, + p.range, + p.quantize, + p.combine_mode, + ) + }); + + let channel_a_safe = (channel_a_jack_num.clamp(1, GLOBAL_CHANNELS as i32) - 1) as usize; + let channel_b_safe = (channel_b_jack_num.clamp(1, GLOBAL_CHANNELS as i32) - 1) as usize; + + let output = app.make_out_jack(0, range).await; + let fader = app.use_faders(); + let buttons = app.use_buttons(); + let leds = app.use_leds(); + + let quantizer = app.use_quantizer(range); + let channel_a_mute_glob = app.make_global(storage.query(|s| s.channel_a_mute_saved)); + let channel_b_mute_glob = app.make_global(storage.query(|s| s.channel_b_mute_saved)); + let glob_latch_layer = app.make_global(LatchLayer::Main); + + // Set up initial state of LED button + if channel_a_mute_glob.get() { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + + let main_fut = async { + + // Get output jack config to find the configured output CV Range + // Assume app config changes will re-spawn this app and re-execute this code. + let a_jack_config = if channel_a_enabled { + App::::get_out_jack_config(channel_a_safe).await + } else { + None + }; + let b_jack_config = if channel_b_enabled { + App::::get_out_jack_config(channel_b_safe).await + } else { + None + }; + + loop { + app.delay_millis(1).await; + + let channel_a_active = channel_a_enabled && !channel_a_mute_glob.get(); + let channel_b_active = channel_b_enabled && !channel_b_mute_glob.get(); + + // Prevent feedback loops by disabling the possibility to sample from the same channel that the app is outputting on + let channel_a_use = channel_a_active && app.start_channel != channel_a_safe; + let channel_b_use = channel_b_active && app.start_channel != channel_b_safe; + + // Get sampled jack values and transform to voltage values according to their individual configured CV Range. + // If channel not active, treat as zero. If jack config not found (e.g. app unplugged), also treat as zero. + let a_in_v: i16 = if channel_a_use { + match a_jack_config { + Some(config) => { + let raw = App::::get_out_global_jack_value(channel_a_safe); + config.range.jack_value_to_voltage_value(raw) + }, + None => 0 + } + } else { + 0 + }; + let b_in_v:i16 = if channel_b_use { + match b_jack_config { + Some(config) => { + let raw = App::::get_out_global_jack_value(channel_b_safe); + config.range.jack_value_to_voltage_value(raw) + }, + None => 0 + } + } else { + 0 + }; + let a_plus_5v = a_in_v + _5V; + let b_plus_5v = b_in_v + _5V; + + let mut out_v:i16 = if combine_mode == 0 { + // Add + a_plus_5v + b_plus_5v - _10V + } else if combine_mode == 1 { + // Subtract + a_plus_5v - b_plus_5v - _10V + } else if combine_mode == 2 { + // Max + if a_plus_5v > b_plus_5v { a_in_v } else { b_in_v } + } else if combine_mode == 3 { + // Min + if channel_a_use && channel_b_use { + if a_plus_5v < b_plus_5v { a_in_v } else { b_in_v } + } else if channel_a_use && !channel_b_use { + a_in_v + } else if !channel_a_use && channel_b_use { + b_in_v + } else { + 0 + } + } else if combine_mode == 4 { + // Average + // If both channels active, output their average, else either a or b only + if channel_a_use && channel_b_use { + ((a_plus_5v + b_plus_5v) / 2) - _5V + } else if channel_a_use && !channel_b_use { + a_in_v + } else { + b_in_v + } + } else { + 0 + }; + + + // Optionally add additional octave or divided semitone offset + let (offset_enabled, offset_voltage_saved, offset_divisor_saved) = storage.query(|s| (s.offset_enabled_saved, s.offset_voltage_saved, s.offset_divisor_saved)); + let mut offset_v = 0; + let mut offset_divisor = 1; + if offset_enabled { + offset_divisor = if offset_divisor_saved > 0 { offset_divisor_saved } else { 1 }; + offset_v = calculate_offset_voltage(offset_voltage_saved, offset_divisor); + out_v += offset_v; + } + + // Update bottom LED to either show offset or divisor level + match glob_latch_layer.get() { + LatchLayer::Main => { + if offset_enabled && offset_v > 0 { + let pos: u8 = (offset_v / 8).clamp(0, 255) as u8; + leds.set(0, Led::Bottom, Color::Blue, Brightness::Custom(pos)); + } else if offset_enabled && offset_v < 0 { + let neg = ((offset_v.abs()) / 8).clamp(0, 255) as u8; + leds.set(0, Led::Bottom, Color::Rose, Brightness::Custom(neg)); + } else { + leds.unset(0, Led::Bottom); + } + } + LatchLayer::Alt => { + let divisor: u8 = (((11.0 / 4095.0) * offset_divisor as f32) as i16 + 1).clamp(1, 12) as u8; + leds.set(0, Led::Bottom, Color::Green, Brightness::Custom(21 * divisor)); // 255/12 is approx 21 + } + _ => {} + }; + + // Clamp to output voltage range, negative voltages clamped to 0V if output range does not support negative voltages + out_v = out_v.clamp(-_5V, _10V); + let out_safe = match range { + Range::_0_10V => out_v.clamp(0, _10V) as u16, + Range::_0_5V => out_v.clamp(0, _5V) as u16 * 2, + Range::_Neg5_5V => (out_v.clamp(-_5V, _5V) + _5V) as u16, + }; + + // Optionally quantize output CV to global quantizer scale + let out: u16 = if quantize { + let out_pitch = quantizer.get_quantized_note(out_safe).await; + out_pitch.as_counts(range) + } else { + out_safe + }; + + output.set_value(out); + leds.set(0, Led::Top, led_color, Brightness::Custom((out / 16) as u8)); + + + // Change state of button when shift is pressed or released to show correct active state of channels A & B + glob_latch_layer.set(LatchLayer::from(buttons.is_shift_pressed())); + if !buttons.is_shift_pressed() { + let muted = storage.query(|s| s.channel_a_mute_saved); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } else { + let muted = storage.query(|s| s.channel_b_mute_saved); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } + + } + }; + + // Returns offset voltage in range -2047 to + 2047 (ie. -5V to +5V) + fn calculate_offset_voltage(offset_voltage_saved: u16, offset_divisor: u16) -> i16 { + // Map divisor saved value to 1 - 12 + let divisor: i16 = (((11.0 / 4095.0) * offset_divisor as f32) as i16 + 1).clamp(1, 12); + let offset = (offset_voltage_saved as f32 / 409.5) - 5.0; // in range -5.0 to +5.0V (-2047 to + 2047) + if divisor == 1 { + (roundf(offset) * V_PER_OCTAVE) as i16 // whole volt (ie. octave) steps + } else { + // Continuous offset, quantized by divisor + roundf(offset * V_PER_OCTAVE) as i16 / divisor + } + } + + let faders_fut = async { + let mut latch = app.make_latch(fader.get_value()); + loop { + fader.wait_for_change().await; + + let latch_layer = glob_latch_layer.get(); + + let target_value = match latch_layer { + LatchLayer::Main => storage.query(|s| s.offset_voltage_saved), + LatchLayer::Alt => storage.query(|s| s.offset_divisor_saved), + LatchLayer::Third => 0, + }; + + if let Some(new_value) = latch.update(fader.get_value(), latch_layer, target_value) { + match latch_layer { + LatchLayer::Main => { + storage.modify_and_save(|s| s.offset_voltage_saved = new_value); + } + LatchLayer::Alt => { + storage.modify_and_save(|s| s.offset_divisor_saved = new_value); + } + LatchLayer::Third => () + } + } + }; + }; + + let btn_fut = async { + loop { + buttons.wait_for_down(0).await; + if !buttons.is_shift_pressed() { + // First channel mute + let muted = storage.modify_and_save(|s| { + s.channel_a_mute_saved = !s.channel_a_mute_saved; + s.channel_a_mute_saved + }); + channel_a_mute_glob.set(muted); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } else { + // Second channel mute + let muted = storage.modify_and_save(|s| { + s.channel_b_mute_saved = !s.channel_b_mute_saved; + s.channel_b_mute_saved + }); + channel_b_mute_glob.set(muted); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } + }; + }; + + let long_press_fut = async { + //long press + + loop { + let (_, is_shift_pressed) = buttons.wait_for_any_long_press().await; + + if !is_shift_pressed { + // Toggle offset on/off + storage.modify_and_save(|s| { + s.offset_enabled_saved = !s.offset_enabled_saved; + s.offset_enabled_saved + }); + } + } + }; + + let scene_handler = async { + loop { + match app.wait_for_scene_event().await { + SceneEvent::LoadScene(scene) => { + storage.load_from_scene(scene).await; + channel_a_mute_glob.set(storage.query(|s| s.channel_a_mute_saved)); + channel_b_mute_glob.set(storage.query(|s| s.channel_b_mute_saved)); + } + + SceneEvent::SaveScene(scene) => { + storage.save_to_scene(scene).await; + } + } + } + }; + + join5(main_fut, faders_fut, btn_fut, long_press_fut, scene_handler).await; + +} \ No newline at end of file diff --git a/faderpunk/src/apps/euclid.rs b/faderpunk/src/apps/euclid.rs index 37daf329..c127a99b 100644 --- a/faderpunk/src/apps/euclid.rs +++ b/faderpunk/src/apps/euclid.rs @@ -157,6 +157,7 @@ pub async fn run( storage: &ManagedStorage, ) { let mut clock = app.use_clock(); + let ticks = clock.get_ticker(); let die = app.use_die(); let faders = app.use_faders(); let buttons = app.use_buttons(); @@ -190,8 +191,6 @@ pub async fn run( let resolution = [384, 192, 96, 48, 24, 16, 12, 8, 6, 4, 3, 2]; - let mut clkn: u32 = 0; - let (fader_saved, shift_fader_saved, mute) = storage.query(|s| (s.fader_saved, s.shift_fader_saved, s.mute_saved)); @@ -212,17 +211,29 @@ pub async fn run( let fut1 = async { let mut note_on = false; let mut aux_on = false; + let mut tick_origin = ticks() as u32; loop { match clock.wait_for_event(ClockDivision::_1).await { ClockEvent::Reset => { - clkn = 0; + tick_origin = ticks() as u32; midi.send_note_off(note).await; midi.send_note_off(note2).await; note_on = false; + aux_on = false; jack[0].set_low().await; + jack[1].set_low().await; + } + ClockEvent::Stop => { + midi.send_note_off(note).await; + midi.send_note_off(note2).await; + note_on = false; + aux_on = false; + jack[0].set_low().await; + jack[1].set_low().await; } ClockEvent::Tick => { + let clkn = (ticks() as u32).wrapping_sub(tick_origin); let muted = glob_muted.get(); let div = div_glob.get(); @@ -255,7 +266,11 @@ pub async fn run( } if glob_latch_layer.get() == LatchLayer::Third { - leds.set(0, Led::Bottom, Color::Red, Brightness::Mid); + if matches!(div, 2 | 4 | 8 | 16) { + leds.set(0, Led::Bottom, Color::Orange, Brightness::High); + } else { + leds.set(0, Led::Bottom, Color::Blue, Brightness::High); + } } } @@ -304,7 +319,6 @@ pub async fn run( ), ); } - clkn += 1; } _ => {} } diff --git a/faderpunk/src/apps/gatecombine.rs b/faderpunk/src/apps/gatecombine.rs new file mode 100644 index 00000000..02e0d372 --- /dev/null +++ b/faderpunk/src/apps/gatecombine.rs @@ -0,0 +1,401 @@ +//! # Gate Combine app +//! +//! Combines two other output gates or triggers from other apps into a single combined gate signal, using binary logic. +//! The combined gate signal is then passed through a probabilistic gate that is controlled by the app's fader. +//! If the combined signal goes high, and the probablistic gate allows it through, the output gate will go high, +//! and stay high until the combined signal goes low, else the output will be low. +//! +//! Created by Richard Smith (@phommed on Faderpunk Discord) in February 2026. +//! +use embassy_futures::{ + join::{join5}, select::{select, select3} +}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; +use heapless::Vec; +use libfp::{ + APP_MAX_PARAMS, AppIcon, Brightness, Color, Config, GLOBAL_CHANNELS, + latch::LatchLayer, + Param, Value, ext::FromValue}; + +use serde::{Deserialize, Serialize}; + +use crate::app::{App, AppParams, AppStorage, Led, ManagedStorage, ParamStore, SceneEvent }; + +pub const CHANNELS: usize = 1; +pub const PARAMS: usize = 6; + +const LED_BRIGHTNESS: Brightness = Brightness::High; + +// Sampled input gate jack changed state must remain the same state for given number of milliseconds to change Gate Combine output +// Intention is to smooth out micro-timing differences between sampled output gates, or when chaining successive Gate Combine apps +const LATCHED_GATE_CHANGE_MILLIS: u32 = 3; + +// App configuration visible to the configurator +pub static CONFIG: Config = Config::new( + "Gate Combine", + "Adds two gate or trigger outputs from other channels together using binary logic", + Color::SkyBlue, + AppIcon::KnobRound, +) +.add_param(Param::bool { name: "Enable Channel A" }) +.add_param(Param::i32 { name: "Channel A Jack", min: 1, max: GLOBAL_CHANNELS as i32 }) +.add_param(Param::bool { name: "Enable Channel B" }) +.add_param(Param::i32 { name: "Channel B Jack", min: 1, max: GLOBAL_CHANNELS as i32 }) +.add_param(Param::Color { + name: "Color", + variants: &[ + Color::Blue, + Color::Green, + Color::Rose, + Color::Orange, + Color::Cyan, + Color::Pink, + Color::Violet, + Color::Yellow, + ], +}) +.add_param(Param::Enum { + name: "Combine Mode", + variants: &["OR", "AND", "XOR", "NOR", "NAND", "XNOR"]}); + +pub struct Params { + // Will be added if = true + channel_a_enabled: bool, + // Output jack number 1 - GLOBAL_CHANNELS to be sampled + channel_a_gate_jack_num: i32, + // Will be added if = true + channel_b_enabled: bool, + // Output jack number 1 - GLOBAL_CHANNELS to be sampled + channel_b_gate_jack_num: i32, + // LED colour + color: Color, + // Output combination mode + combine_mode: usize, +} + +impl Default for Params { + fn default() -> Self { + Self { + channel_a_enabled: false, + channel_a_gate_jack_num: 1, + channel_b_enabled: false, + channel_b_gate_jack_num: 1, + color: Color::Yellow, + combine_mode: 0, // OR + } + } +} + +impl AppParams for Params { + fn from_values(values: &[Value]) -> Option { + if values.len() < PARAMS { + return None; + } + Some(Self { + channel_a_enabled: bool::from_value(values[0]), + channel_a_gate_jack_num: i32::from_value(values[1]), + channel_b_enabled: bool::from_value(values[2]), + channel_b_gate_jack_num: i32::from_value(values[3]), + color: Color::from_value(values[4]), + combine_mode: usize::from_value(values[5]), + }) + } + + fn to_values(&self) -> Vec { + let mut vec = Vec::new(); + vec.push(self.channel_a_enabled.into()).unwrap(); + vec.push(self.channel_a_gate_jack_num.into()).unwrap(); + vec.push(self.channel_b_enabled.into()).unwrap(); + vec.push(self.channel_b_gate_jack_num.into()).unwrap(); + vec.push(self.color.into()).unwrap(); + vec.push(self.combine_mode.into()).unwrap(); + vec + } +} + +#[derive(Serialize, Deserialize)] +pub struct Storage { + channel_a_mute_saved: bool, + channel_b_mute_saved: bool, + prob_saved: u16, +} +impl Default for Storage { + fn default() -> Self { + Self { + channel_a_mute_saved: false, + channel_b_mute_saved: false, + prob_saved: 4096, + } + } +} + +impl AppStorage for Storage {} + +// Wrapper task - required for all apps +#[embassy_executor::task(pool_size = 16/CHANNELS)] +pub async fn wrapper(app: App, exit_signal: &'static Signal) { + let param_store = ParamStore::::new(app.app_id, app.layout_id); + let storage = ManagedStorage::::new(app.app_id, app.layout_id); + + param_store.load().await; + storage.load().await; + + let app_loop = async { + loop { + select3( + run(&app, ¶m_store, &storage), + param_store.param_handler(), + storage.saver_task(), + ) + .await; + } + }; + + select(app_loop, app.exit_handler(exit_signal)).await; +} + +// Main app logic +pub async fn run(app: &App, + params: &ParamStore, + storage: &ManagedStorage, +) { + + + // first_channel and second_channel params are converted to usize in range 0 - (GLOBAL_CHANNELS-1) + let (channel_a_enabled, channel_a_gate_jack_num, channel_b_enabled, channel_b_gate_jack_num, led_color, combine_mode) = params + .query(|p| { + ( + p.channel_a_enabled, + p.channel_a_gate_jack_num, + p.channel_b_enabled, + p.channel_b_gate_jack_num, + p.color, + p.combine_mode, + ) + }); + + let channel_a_safe = (channel_a_gate_jack_num.clamp(1, GLOBAL_CHANNELS as i32) - 1) as usize; + let channel_b_safe = (channel_b_gate_jack_num.clamp(1, GLOBAL_CHANNELS as i32) - 1) as usize; + + let output = app.make_gate_jack(0, 4095).await; + let fader = app.use_faders(); + let buttons = app.use_buttons(); + let leds = app.use_leds(); + let die = app.use_die(); + + let channel_a_mute_glob = app.make_global(storage.query(|s| s.channel_a_mute_saved)); + let channel_b_mute_glob = app.make_global(storage.query(|s| s.channel_b_mute_saved)); + let glob_latch_layer = app.make_global(LatchLayer::Main); + + // Set up initial state of LED button + if channel_a_mute_glob.get() { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + + let mut old_out_gate_was_high = false; + + let main_fut = async { + + let mut gate_a_unchanged_counter = 0u32; + let mut gate_b_unchanged_counter = 0u32; + let mut last_gate_a_is_high = false; + let mut last_gate_b_is_high = false; + let mut latched_gate_a_is_high = false; + let mut latched_gate_b_is_high = false; + + loop { + app.delay_millis(1).await; + + latched_gate_change(channel_a_safe, &mut gate_a_unchanged_counter, &mut last_gate_a_is_high, &mut latched_gate_a_is_high); + latched_gate_change(channel_b_safe, &mut gate_b_unchanged_counter, &mut last_gate_b_is_high, &mut latched_gate_b_is_high); + + let channel_a_active = channel_a_enabled && !channel_a_mute_glob.get(); + let channel_b_active = channel_b_enabled && !channel_b_mute_glob.get(); + let channel_a_use = channel_a_active && app.start_channel != channel_a_safe; + let channel_b_use = channel_b_active && app.start_channel != channel_b_safe; + let a_is_high = if channel_a_use { + latched_gate_a_is_high + } else { + false + }; + let b_is_high = if channel_b_use { + latched_gate_b_is_high + } else { + false + }; + let mut out_is_high:bool = if combine_mode == 0 { + // OR + a_is_high | b_is_high + } else if combine_mode == 1 { + // AND + a_is_high & b_is_high + } else if combine_mode == 2 { + // XOR + a_is_high ^ b_is_high + } else if combine_mode == 3 { + // NOR + !(a_is_high | b_is_high) + } else if combine_mode == 4 { + // NAND + !(a_is_high & b_is_high) + } else if combine_mode == 5 { + // XNOR + !(a_is_high ^ b_is_high) + } else { + false + }; + + // Apply probabilistic gate + + let prob = storage.query(|s| s.prob_saved); // Get gate probability from fader 0 - 4095 + if out_is_high && !old_out_gate_was_high { + // Combined output has just gone high. + + let rand_val = die.roll(); + // The higher the probability fader, more chance there is of gate pasing through + if rand_val >= prob { + // bad luck, gate must stay low this time + out_is_high = false; + } + } + + old_out_gate_was_high = out_is_high; + + if out_is_high { + output.set_high().await; + leds.set(0, Led::Top, led_color, LED_BRIGHTNESS); + } else { + output.set_low().await; + leds.unset(0, Led::Top); + } + + // Update bottom LED to show trigger probability when on main latch layer + match glob_latch_layer.get() { + LatchLayer::Main => { + let pos: u8 = (prob / 8).clamp(0, 255) as u8; + leds.set(0, Led::Bottom, Color::Blue, Brightness::Custom(pos)); + } + _ => { + leds.unset(0, Led::Bottom); + } + }; + + } + }; + + let fader_fut = async { + let mut latch = app.make_latch(fader.get_value()); + loop { + fader.wait_for_change_at(0).await; + + let latch_layer = glob_latch_layer.get(); + + let target_value = match latch_layer { + LatchLayer::Main => storage.query(|s| s.prob_saved), + LatchLayer::Alt => 0, + LatchLayer::Third => 0, + }; + + if let Some(new_value) = latch.update(fader.get_value(), latch_layer, target_value) { + match latch_layer { + LatchLayer::Main => { + storage.modify_and_save(|s| s.prob_saved = new_value); + } + LatchLayer::Alt => {} + LatchLayer::Third => {} + } + } + } + }; + + let btn_fut = async { + loop { + buttons.wait_for_down(0).await; + if !buttons.is_shift_pressed() { + // First channel mute + let muted = storage.modify_and_save(|s| { + s.channel_a_mute_saved = !s.channel_a_mute_saved; + s.channel_a_mute_saved + }); + channel_a_mute_glob.set(muted); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } else { + // Second channel mute + let muted = storage.modify_and_save(|s| { + s.channel_b_mute_saved = !s.channel_b_mute_saved; + s.channel_b_mute_saved + }); + channel_b_mute_glob.set(muted); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } + }; + }; + + let shift_fut = async { + loop { + app.delay_millis(1).await; + + glob_latch_layer.set(LatchLayer::from(buttons.is_shift_pressed())); + + // Change state of button when shift is pressed or released to show correct active state of first or second added channels + if !buttons.is_shift_pressed() { + let muted = storage.query(|s| s.channel_a_mute_saved); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } else { + let muted = storage.query(|s| s.channel_b_mute_saved); + if muted { + leds.unset(0, Led::Button); + } else { + leds.set(0, Led::Button, led_color, Brightness::Mid); + } + } + + }; + }; + + let scene_handler = async { + loop { + match app.wait_for_scene_event().await { + SceneEvent::LoadScene(scene) => { + storage.load_from_scene(scene).await; + channel_a_mute_glob.set(storage.query(|s| s.channel_a_mute_saved)); + channel_b_mute_glob.set(storage.query(|s| s.channel_b_mute_saved)); + } + + SceneEvent::SaveScene(scene) => { + storage.save_to_scene(scene).await; + } + } + } + }; + + join5(main_fut, fader_fut, btn_fut, shift_fut, scene_handler).await; + +} + +fn latched_gate_change(channel_safe: usize, gate_unchanged_counter: &mut u32, last_gate_is_high: &mut bool, latched_gate_is_high: &mut bool) { + let jack_is_now_high = App::::get_out_global_gate_jack_is_high(channel_safe); + if *last_gate_is_high == jack_is_now_high { + *gate_unchanged_counter += 1; + } else { + *last_gate_is_high = !*last_gate_is_high; + *gate_unchanged_counter = 0; + } + if *latched_gate_is_high != jack_is_now_high && *gate_unchanged_counter > LATCHED_GATE_CHANGE_MILLIS { + *latched_gate_is_high = jack_is_now_high; + } +} \ No newline at end of file diff --git a/faderpunk/src/apps/mod.rs b/faderpunk/src/apps/mod.rs index 29f9478a..2718416f 100644 --- a/faderpunk/src/apps/mod.rs +++ b/faderpunk/src/apps/mod.rs @@ -19,4 +19,6 @@ register_apps!( 18 => clk_div, 19 => panner, 22 => lfo_plus, + 44 => cvcombine, + 45 => gatecombine, ); diff --git a/faderpunk/src/apps/notefader.rs b/faderpunk/src/apps/notefader.rs index 93738350..88031852 100644 --- a/faderpunk/src/apps/notefader.rs +++ b/faderpunk/src/apps/notefader.rs @@ -165,7 +165,7 @@ pub async fn run( ( p.midi_out, p.midi_channel, - p.gatel, + p.gatel as u32, p.midi_note, p.span, p.outmode, @@ -174,6 +174,7 @@ pub async fn run( }); let mut clock = app.use_clock(); + let ticks = clock.get_ticker(); let quantizer = app.use_quantizer(range); let fader = app.use_faders(); @@ -191,8 +192,6 @@ pub async fn run( let resolution = [384, 192, 96, 48, 24, 16, 12, 8, 6, 4, 3, 2]; - let mut clkn = 0; - let (res, mute) = storage.query(|s| (s.fader_saved, s.mute_saved)); glob_muted.set(mute); @@ -230,16 +229,26 @@ pub async fn run( loop { match clock.wait_for_event(ClockDivision::_1).await { ClockEvent::Reset => { - clkn = 0; midi.send_note_off(note).await; note_on = false; + if outmode == 1 { + jack.set_value(0); + } + } + ClockEvent::Stop => { + midi.send_note_off(note).await; + note_on = false; + if outmode == 1 { + jack.set_value(0); + } } ClockEvent::Tick => { let muted = glob_muted.get(); let div = div_glob.get(); + let clkn = ticks() as u32; - if clkn % div == 0 && storage.query(|s| s.clocked) { + if clkn.is_multiple_of(div) && storage.query(|s| s.clocked) { if !muted { if note_on { midi.send_note_off(note).await; @@ -248,7 +257,11 @@ pub async fn run( note_on = true; } - leds.set(0, Led::Bottom, Color::Red, LED_BRIGHTNESS); + if matches!(div, 2 | 4 | 8 | 16) { + leds.set(0, Led::Bottom, Color::Orange, Brightness::High); + } else { + leds.set(0, Led::Bottom, Color::Blue, Brightness::High); + } } if clkn % div == (div * gatel / 100).clamp(1, div - 1) { @@ -263,7 +276,6 @@ pub async fn run( leds.set(0, Led::Bottom, led_color, Brightness::Off); } - clkn += 1; } _ => {} } diff --git a/faderpunk/src/apps/probatrigger.rs b/faderpunk/src/apps/probatrigger.rs index df04ab0a..60593ebb 100644 --- a/faderpunk/src/apps/probatrigger.rs +++ b/faderpunk/src/apps/probatrigger.rs @@ -140,11 +140,19 @@ pub async fn run( params: &ParamStore, storage: &ManagedStorage, ) { - let (midi_out, midi_chan, note, gatel, led_color) = - params.query(|p| (p.midi_out, p.midi_channel, p.midi_note, p.gatel, p.color)); + let (midi_out, midi_chan, note, gatel, led_color) = params.query(|p| { + ( + p.midi_out, + p.midi_channel, + p.midi_note, + p.gatel as u32, + p.color, + ) + }); let curve = Curve::Exponential; let mut clock = app.use_clock(); + let ticks = clock.get_ticker(); let die = app.use_die(); let fader = app.use_faders(); let buttons = app.use_buttons(); @@ -160,8 +168,6 @@ pub async fn run( let resolution = [384, 192, 96, 48, 24, 16, 12, 8, 6, 4, 3, 2]; - let mut clkn = 0; - let mut rndval = die.roll(); let (res, mute) = storage.query(|s| (s.fader_saved, s.mute_saved)); @@ -182,7 +188,11 @@ pub async fn run( loop { match clock.wait_for_event(ClockDivision::_1).await { ClockEvent::Reset => { - clkn = 0; + midi.send_note_off(note).await; + note_on = false; + jack.set_low().await; + } + ClockEvent::Stop => { midi.send_note_off(note).await; note_on = false; jack.set_low().await; @@ -191,8 +201,9 @@ pub async fn run( let muted = glob_muted.get(); let val = storage.query(|s| s.prob_saved); let div = div_glob.get(); + let clkn = ticks() as u32; - if clkn % div == 0 { + if clkn.is_multiple_of(div) { if curve.at(val) >= rndval && !muted { jack.set_high().await; leds.set(0, Led::Top, led_color, LED_BRIGHTNESS); @@ -201,7 +212,11 @@ pub async fn run( } if glob_latch_layer.get() == LatchLayer::Alt { - leds.set(0, Led::Bottom, Color::Red, LED_BRIGHTNESS); + if matches!(div, 2 | 4 | 8 | 16) { + leds.set(0, Led::Bottom, Color::Orange, Brightness::High); + } else { + leds.set(0, Led::Bottom, Color::Blue, Brightness::High); + } } else { leds.unset(0, Led::Bottom); } @@ -218,7 +233,6 @@ pub async fn run( leds.set(0, Led::Bottom, led_color, Brightness::Off); } - clkn += 1; } _ => {} } diff --git a/faderpunk/src/apps/rndcvcc.rs b/faderpunk/src/apps/rndcvcc.rs index 948e2178..a8b2d5a8 100644 --- a/faderpunk/src/apps/rndcvcc.rs +++ b/faderpunk/src/apps/rndcvcc.rs @@ -134,6 +134,7 @@ pub async fn run( params.query(|p| (p.bipolar, p.midi_out, p.midi_channel, p.midi_cc)); let mut clock = app.use_clock(); + let ticks = clock.get_ticker(); let rnd = app.use_die(); let fader = app.use_faders(); let buttons = app.use_buttons(); @@ -157,8 +158,6 @@ pub async fn run( let resolution = [384, 192, 96, 48, 24, 16, 12, 8, 6, 4, 3, 2]; - let mut clkn = 0; - let curve = Curve::Exponential; let fader_curve = Curve::Exponential; @@ -178,14 +177,13 @@ pub async fn run( let fut1 = async { loop { match clock.wait_for_event(ClockDivision::_1).await { - ClockEvent::Reset => { - clkn = 0; - } + ClockEvent::Reset => {} ClockEvent::Tick => { let muted = glob_muted.get(); - + let clkn = ticks() as u32; let div = div_glob.get(); - if clkn % div == 0 && !muted && storage.query(|s: &Storage| s.clocked) { + if clkn.is_multiple_of(div) && !muted && storage.query(|s: &Storage| s.clocked) + { val_glob.set(rnd.roll()); let color = if !glob_muted.get() { @@ -202,7 +200,10 @@ pub async fn run( leds.set(0, Led::Button, color, Brightness::Mid); } - if clkn % div == 0 && storage.query(|s: &Storage| s.clocked) && buttons.is_shift_pressed() { + if clkn.is_multiple_of(div) + && storage.query(|s: &Storage| s.clocked) + && buttons.is_shift_pressed() + { leds.set(0, Led::Bottom, Color::Red, Brightness::High); } if clkn % div == (div * 50 / 100).clamp(1, div - 1) @@ -210,7 +211,6 @@ pub async fn run( { leds.unset(0, Led::Bottom); } - clkn += 1; } _ => {} } diff --git a/faderpunk/src/apps/seq8.rs b/faderpunk/src/apps/seq8.rs index a63821b0..6bd7f655 100644 --- a/faderpunk/src/apps/seq8.rs +++ b/faderpunk/src/apps/seq8.rs @@ -156,6 +156,7 @@ pub async fn run( let buttons = app.use_buttons(); let faders = app.use_faders(); let mut clk = app.use_clock(); + let ticks = clk.get_ticker(); let led = app.use_leds(); let midi = [ @@ -165,8 +166,6 @@ pub async fn run( app.use_midi_output(midi_out, midi_chan4), ]; - let clockn_glob = app.make_global(0); - let cv_out = [ app.make_out_jack(0, Range::_0_10V).await, app.make_out_jack(2, Range::_0_10V).await, @@ -372,10 +371,9 @@ pub async fn run( let colors = [Color::Yellow, Color::Pink, Color::Cyan, Color::White]; app.delay_millis(16).await; let clockres = clockres_glob.get(); + let clockn = ticks() as usize; if buttons.is_shift_pressed() { - let clockn = clockn_glob.get(); - let seq_length = seq_length_glob.get(); let page = page_glob.get(); @@ -398,10 +396,16 @@ pub async fn run( if n >= seq_length[page / 2] { bright = Brightness::Off; } + + let led_color = if matches!(clockres[page / 2], 2 | 4 | 8 | 16) { + Color::Orange + } else { + Color::Blue + }; if n < 8 { - led.set(n as usize, Led::Top, Color::Red, bright) + led.set(n as usize, Led::Top, led_color, bright) } else { - led.set(n as usize - 8, Led::Bottom, Color::Red, bright) + led.set(n as usize - 8, Led::Bottom, led_color, bright) } } } @@ -415,7 +419,6 @@ pub async fn run( let seq_length = seq_length_glob.get(); let mut color = colors[0]; - let clockn = clockn_glob.get(); // this should go if page / 2 == 0 { color = colors[0]; @@ -494,17 +497,21 @@ pub async fn run( let clockres = clockres_glob.get(); let legato_seq = legatoseq_glob.get(); - let mut clockn = clockn_glob.get(); - match clk.wait_for_event(ClockDivision::_1).await { ClockEvent::Reset => { - clockn = 0; + for n in 0..4 { + midi[n].send_note_off(lastnote[n]).await; + gate_out[n].set_low().await; + } + } + ClockEvent::Stop => { for n in 0..4 { midi[n].send_note_off(lastnote[n]).await; gate_out[n].set_low().await; } } ClockEvent::Tick => { + let clockn = ticks() as usize; for n in 0..=3 { if clockn.is_multiple_of(clockres[n]) { let clkindex = @@ -520,8 +527,7 @@ pub async fn run( as u32) * 410 / 4095) as u16 - + (storage.query(|s| s.oct_fader[n]) / 1000) - * 410, + + (storage.query(|s| s.oct_fader[n]) / 1000) * 410, ) .await; lastnote[n] = out.as_midi(); @@ -543,12 +549,9 @@ pub async fn run( } } } - clockn += 1; } _ => {} } - - clockn_glob.set(clockn); } }; @@ -634,23 +637,20 @@ struct AltUpdateContext<'a> { resolution: &'a [usize; 8], } -fn apply_alt_update( - chan: usize, - seq_idx: usize, - value: u16, - ctx: &AltUpdateContext, -) { +fn apply_alt_update(chan: usize, seq_idx: usize, value: u16, ctx: &AltUpdateContext) { match chan { 0 => { // Sequence length - ctx.storage.modify_and_save(|s| s.length_fader[seq_idx] = value); + ctx.storage + .modify_and_save(|s| s.length_fader[seq_idx] = value); let mut arr = ctx.seq_length_glob.get(); arr[seq_idx] = (value / 256 + 1) as u8; ctx.seq_length_glob.set(arr); } 1 => { // Gate length - ctx.storage.modify_and_save(|s| s.gate_fader[seq_idx] = value); + ctx.storage + .modify_and_save(|s| s.gate_fader[seq_idx] = value); let clockres = ctx.clockres_glob.get(); let mut arr = ctx.gatelength_glob.get(); arr[seq_idx] = (clockres[seq_idx] * (value as usize) / 4096) as u8; @@ -659,15 +659,18 @@ fn apply_alt_update( } 2 => { // Octave - ctx.storage.modify_and_save(|s| s.oct_fader[seq_idx] = value); + ctx.storage + .modify_and_save(|s| s.oct_fader[seq_idx] = value); } 3 => { // Range - ctx.storage.modify_and_save(|s| s.range_fader[seq_idx] = value); + ctx.storage + .modify_and_save(|s| s.range_fader[seq_idx] = value); } 4 => { // Resolution - ctx.storage.modify_and_save(|s| s.res_fader[seq_idx] = value); + ctx.storage + .modify_and_save(|s| s.res_fader[seq_idx] = value); let res_index = (value / 512) as usize; let mut arr = ctx.clockres_glob.get(); arr[seq_idx] = ctx.resolution[res_index]; diff --git a/faderpunk/src/apps/turing.rs b/faderpunk/src/apps/turing.rs index cb89d24a..61bd714d 100644 --- a/faderpunk/src/apps/turing.rs +++ b/faderpunk/src/apps/turing.rs @@ -171,7 +171,7 @@ pub async fn run( p.color, p.midi_channel, p.midi_note, - p.gatel, + p.gatel as u32, p.range, ) }); @@ -180,6 +180,7 @@ pub async fn run( let fader = app.use_faders(); let leds = app.use_leds(); let mut clock = app.use_clock(); + let ticks = clock.get_ticker(); let die = app.use_die(); let quantizer = app.use_quantizer(range); @@ -207,7 +208,6 @@ pub async fn run( div_glob.set(resolution[res as usize / 512]); let fut1 = async { - let mut clkn: usize = 0; let mut att_reg: u16; loop { let div = div_glob.get(); @@ -215,13 +215,13 @@ pub async fn run( match clock.wait_for_event(ClockDivision::_1).await { ClockEvent::Reset => { - clkn = 0; if midi_mode == MidiMode::Note { midi.send_note_off(midi_note.get()).await; } register = storage.query(|s| s.register_saved); } ClockEvent::Tick => { + let clkn = ticks() as usize; if clkn.is_multiple_of(div) { if (clkn / div).is_multiple_of(length as usize) { let reg_old = storage.query(|s| s.register_saved); @@ -271,7 +271,11 @@ pub async fn run( } if buttons.is_button_pressed(0) && !buttons.is_shift_pressed() { - leds.set(0, Led::Bottom, Color::Red, Brightness::High); + if matches!(div, 2 | 4 | 8 | 16) { + leds.set(0, Led::Bottom, Color::Orange, Brightness::High); + } else { + leds.set(0, Led::Bottom, Color::Blue, Brightness::High); + } } } if clkn % div == (div * gatel as usize / 100).clamp(1, div - 1) { @@ -281,8 +285,6 @@ pub async fn run( midi.send_note_off(midi_note.get()).await; } } - - clkn += 1; } ClockEvent::Stop => { if midi_mode == MidiMode::Note { diff --git a/faderpunk/src/state.rs b/faderpunk/src/state.rs index b44ee078..bfad06ec 100644 --- a/faderpunk/src/state.rs +++ b/faderpunk/src/state.rs @@ -1,15 +1,25 @@ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; +use libfp::GLOBAL_CHANNELS; use serde::{Deserialize, Serialize}; -use crate::storage; +use crate::{ + app::{GateJack, InJack, OutJack}, + storage, +}; #[derive(Serialize, Deserialize, Clone, Copy, Default, Debug)] pub struct RuntimeState { pub clock_is_running: bool, + pub out_jacks: [Option; GLOBAL_CHANNELS], + pub in_jacks: [Option; GLOBAL_CHANNELS], + pub gate_jacks: [Option; GLOBAL_CHANNELS], } static STATE: Mutex = Mutex::new(RuntimeState { clock_is_running: true, + out_jacks: [None; GLOBAL_CHANNELS], + in_jacks: [None; GLOBAL_CHANNELS], + gate_jacks: [None; GLOBAL_CHANNELS], }); pub async fn init_state() { @@ -45,3 +55,18 @@ where pub async fn is_clock_running() -> bool { STATE.lock().await.clock_is_running } + +// Gets configured set of each CV out app jack +pub async fn get_out_jacks() -> [Option; GLOBAL_CHANNELS] { + STATE.lock().await.out_jacks +} + +/// Gets configured set of each input app jack +pub async fn get_in_jacks() -> [Option; GLOBAL_CHANNELS] { + STATE.lock().await.in_jacks +} + +/// Gets configured set of each gate out app jack +pub async fn get_gate_jacks() -> [Option; GLOBAL_CHANNELS] { + STATE.lock().await.gate_jacks +} diff --git a/faderpunk/src/tasks/clock.rs b/faderpunk/src/tasks/clock.rs index 2d1bbd41..e9be4ed4 100644 --- a/faderpunk/src/tasks/clock.rs +++ b/faderpunk/src/tasks/clock.rs @@ -12,9 +12,9 @@ use embassy_sync::{ channel::Channel, pubsub::{PubSubChannel, Subscriber}, }; -use embassy_time::{Instant, Timer}; +use embassy_time::{Duration, Instant, Timer}; use midly::live::SystemRealtime; -use portable_atomic::Ordering; +use portable_atomic::{AtomicU64, Ordering}; use libfp::{ utils::bpm_to_clock_duration, AuxJackMode, ClockSrc, GlobalConfig, MidiOut, MidiOutConfig, @@ -34,6 +34,10 @@ const CLOCK_PUBSUB_SIZE: usize = 16; const CLOCK_PUBSUB_SUBSCRIBERS: usize = 16; // 3 Ext clocks, internal clock, midi const CLOCK_PUBSUB_PUBLISHERS: usize = 5; +// Add a slight delay before the very first tick (to offset it to reset) +const TICK_RESET_DELAY: u8 = 2; + +pub static TICK_COUNTER: AtomicU64 = AtomicU64::new(0); type AuxInputs = ( Peri<'static, PIN_1>, @@ -240,6 +244,7 @@ async fn run_clock_gatekeeper() { if is_running || matches!(source, ClockSrc::Atom | ClockSrc::Meteor | ClockSrc::Cube) { + TICK_COUNTER.fetch_add(1, Ordering::Relaxed); clock_publisher.publish(ClockEvent::Tick).await; send_analog_ticks(&spawner, &config, &mut analog_tick_counters).await; midi_rt_event = Some(SystemRealtime::TimingClock); @@ -253,6 +258,7 @@ async fn run_clock_gatekeeper() { } // (Re-)start the clock. Full phase reset ClockInEvent::Start(_) => { + TICK_COUNTER.store(0, Ordering::Relaxed); is_running = true; clock_publisher.publish(ClockEvent::Reset).await; clock_publisher.publish(ClockEvent::Start).await; @@ -268,6 +274,7 @@ async fn run_clock_gatekeeper() { } // Reset the phase without affecting the run state ClockInEvent::Reset(_) => { + TICK_COUNTER.store(0, Ordering::Relaxed); clock_publisher.publish(ClockEvent::Reset).await; analog_tick_counters = [0; 3]; send_analog_reset(&spawner, &config).await; @@ -321,7 +328,7 @@ async fn run_clock_sources(aux_inputs: AuxInputs) { let config = config_receiver.get().await; let mut is_running = is_clock_running().await; let mut tick_duration = bpm_to_clock_duration(config.clock.internal_bpm, INTERNAL_PPQN); - let mut next_tick_at = Instant::now(); + let mut next_tick_at = Instant::now() + Duration::from_millis(TICK_RESET_DELAY as u64); if is_running { // If we're starting up and the clock should already be running, @@ -393,7 +400,8 @@ async fn run_clock_sources(aux_inputs: AuxInputs) { if next_is_running { // Schedule the first tick immediately. The main loop will // handle publishing it and scheduling the subsequent tick. - next_tick_at = Instant::now(); + next_tick_at = + Instant::now() + Duration::from_millis(TICK_RESET_DELAY as u64); clock_in_sender .send(ClockInEvent::Start(ClockSrc::Internal)) .await; diff --git a/faderpunk/src/tasks/max.rs b/faderpunk/src/tasks/max.rs index 44e6dc72..5b797b9e 100644 --- a/faderpunk/src/tasks/max.rs +++ b/faderpunk/src/tasks/max.rs @@ -242,11 +242,11 @@ async fn process_channel_values( Mode::Mode3(_) => match MAX_TRIGGERS_GPO[i].load(Ordering::Relaxed) { 1 => { max.gpo_set_low(port).await.unwrap(); - MAX_TRIGGERS_GPO[i].store(0, Ordering::Relaxed); + MAX_TRIGGERS_GPO[i].store(3, Ordering::Relaxed); } 2 => { max.gpo_set_high(port).await.unwrap(); - MAX_TRIGGERS_GPO[i].store(0, Ordering::Relaxed); + MAX_TRIGGERS_GPO[i].store(4, Ordering::Relaxed); } _ => {} }, diff --git a/faderpunk/src/tasks/midi.rs b/faderpunk/src/tasks/midi.rs index 16477d1d..e3bf5c04 100644 --- a/faderpunk/src/tasks/midi.rs +++ b/faderpunk/src/tasks/midi.rs @@ -38,6 +38,7 @@ midly::stack_buffer! { const MIDI_CHANNEL_SIZE: usize = 16; const MIDI_APP_QUEUE_SIZE: usize = 16; const MIDI_PUBSUB_SIZE: usize = 64; +const MIDI_BURST_PER_TICK: usize = 8; // Max apps const MIDI_PUBSUB_SUBS: usize = GLOBAL_CHANNELS; // Only one, from here @@ -221,15 +222,24 @@ pub async fn midi_distributor() { let _ = app_queues[start_channel].push_back(ev); } } - // The 1ms throttle timer has fired, send one message. + // The throttle timer has fired, send a small burst. Either::Second(_) => { - // Find the next app with a message in its queue (round-robin) - for i in 0..16 { - let app_idx = (last_app_id + 1 + i) % 16; - if let Some(ev) = app_queues[app_idx].pop_front() { - midi_out_sender.send(MidiOutEvent::Event(ev)).await; - last_app_id = app_idx; - break; // Stop after sending one message + for _ in 0..MIDI_BURST_PER_TICK { + let mut sent = false; + + // Find the next app with a message in its queue (round-robin) + for i in 0..16 { + let app_idx = (last_app_id + 1 + i) % 16; + if let Some(ev) = app_queues[app_idx].pop_front() { + midi_out_sender.send(MidiOutEvent::Event(ev)).await; + last_app_id = app_idx; + sent = true; + break; + } + } + + if !sent { + break; } } } diff --git a/libfp/src/lib.rs b/libfp/src/lib.rs index e0924ca7..950707c2 100644 --- a/libfp/src/lib.rs +++ b/libfp/src/lib.rs @@ -924,6 +924,21 @@ impl Range { pub fn is_bipolar(&self) -> bool { *self == Range::_Neg5_5V } + + /// Converts a normalised jack signal value (in range 0 - 4095) to signed voltage value + /// e.g. convert from an InJack get_value() method call, or from a DAC output value + /// If Range 0-10V, output in range 0 - 4095 + /// If Range 0-5V, output in range 0 - 2047 + /// If Range -5 - 5V, output in range -2048 - 2047 + /// Useful for simulating real-world voltage math (e.g. Precision Adders) + pub fn jack_value_to_voltage_value(&self, value: u16) -> i16 { + let value = value.clamp(0, 4095) as i16; + match self { + Range::_0_10V => value, + Range::_0_5V => value / 2, + Range::_Neg5_5V => value - 2048, + } + } } impl From for DACRANGE {