diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..cb9f84b34 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,6 @@ +reviews: + profile: assertive + auto_review: + enabled: true + base_branches: + - ".*" diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..6125a1028 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Public client configuration. Never put a Supabase service-role key here. +VITE_SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co +VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_YOUR_KEY + +# Leave blank until the hosted recording service is deployed and verified. +# Development defaults to localhost:8787; packaged builds require an explicit URL. +VITE_CLOUD_SHARE_ENDPOINT= diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 7dbd9d18b..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f54a72c5d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.cmd text eol=crlf + +*.dll binary +*.exe binary +*.gif binary +*.icns binary +*.ico binary +*.jpeg binary +*.jpg binary +*.mov binary +*.mp3 binary +*.mp4 binary +*.node binary +*.pdf binary +*.png binary +*.ttf binary +*.wasm binary +*.wav binary +*.webm binary +*.woff binary +*.woff2 binary +*.zip binary diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..d9910a8f0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: webadderall +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: webadderall +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..7cef0a218 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,93 @@ +name: Bug Report +description: Create a report to help us improve. If you are not sure what you are experiencing is a bug, report it anyway. We would rather you submit an unprofessional bug report than not report it at all. +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: checkboxes + attributes: + label: Search existing issues + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + + - type: textarea + id: bug-description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + placeholder: e.g., When I click submit, nothing happens... + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + placeholder: e.g., The form should submit and show a success message + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: false + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + placeholder: Drag and drop images here or paste them + validations: + required: false + + - type: dropdown + id: os-type + attributes: + label: OS + description: Operating system + options: + - Windows + - macOS + - Linux + - Other + validations: + required: false + + - type: input + id: os-version + attributes: + label: OS Version + description: Please specify your OS version + placeholder: e.g., Windows 11, macOS Sonoma, Ubuntu 22.04 + validations: + required: false + + - type: input + id: os-other + attributes: + label: Other OS + description: If you selected "Other" for OS, please specify your operating system + placeholder: e.g., FreeBSD, Solaris + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here. + placeholder: Links, references, or any additional information + validations: + required: false + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..4200c689e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,49 @@ +name: Feature Request +description: Suggest an idea for this project +title: "[Feature]: " +labels: ["enhancement", "feature-request"] +body: + - type: checkboxes + attributes: + label: Search existing issues + description: Please search to see if an issue already exists for this feature request. + options: + - label: I have searched the existing issues + required: true + + - type: textarea + id: problem-description + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of what the problem is. + placeholder: e.g., I'm always frustrated when I have to... + validations: + required: true + + - type: textarea + id: solution-description + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + placeholder: Describe the feature or change you're proposing + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + placeholder: Have you considered any workarounds or alternative approaches? + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + placeholder: Links, mockups, or any additional information + validations: + required: false + diff --git a/.github/instructions/general.instructions.md b/.github/instructions/general.instructions.md new file mode 100644 index 000000000..79eba3dfa --- /dev/null +++ b/.github/instructions/general.instructions.md @@ -0,0 +1 @@ +Review this PR. Do not leave line comments. Provide exactly one summary comment including: 1. A letter grade (A-F), 2. Major shortcomings blocking merge 3. Nice-to-have shortcomings 4. Merge readiness (Yes/No). \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..c45d35e06 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,44 @@ +# Pull Request Template + +## Description + + +## Motivation + + +## Type of Change +- [ ] New Feature +- [ ] Bug Fix +- [ ] Refactor / Code Cleanup +- [ ] Documentation Update +- [ ] Other (please specify) + +## Related Issue(s) + + +## Screenshots / Video + + +**Screenshot** (if applicable): + +```markdown +![Screenshot Description](path/to/screenshot.png) +``` + +**Video** (wherever possible): + +```html + +``` + +## Testing Guide + + +## Checklist +- [ ] I have performed a self-review of my code. +- [ ] I have added any necessary screenshots or videos. +- [ ] I have linked related issue(s) and updated the changelog if applicable. + +--- +*Thank you for contributing!* + diff --git a/.github/workflows/auto-translate-zh.yml b/.github/workflows/auto-translate-zh.yml new file mode 100644 index 000000000..62846e9d1 --- /dev/null +++ b/.github/workflows/auto-translate-zh.yml @@ -0,0 +1,38 @@ +name: Auto Translate to zh-CN + +on: + push: + branches: [main] + paths: + - "*.md" + +permissions: + contents: write + models: read + +jobs: + translate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/cache@v4 + with: + path: .genaiscript/cache/** + key: translation-cache-${{ github.run_id }} + restore-keys: translation-cache- + + - uses: pelikhan/action-continuous-translation@v0.2.11 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + source: en + lang: zh + files: "*.md;README.md;CONTRIBUTING.md;TRANSLATION_GUIDE.md;CLAUDE.md" + filename_template: "{{ base }}.zh-CN{{ ext }}" + + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "docs(zh-CN): auto-translate updates" + file_pattern: "*.zh-CN.md" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..78f1ddae1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,204 @@ + +name: Build Electron App + +on: + workflow_dispatch: + +permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + +jobs: + build-windows: + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build Windows app + shell: pwsh + run: | + npm run build:platform-native-helpers + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npx tsc + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npx vite build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npx electron-builder --win dir nsis --x64 --publish never + + - name: Smoke test packaged Windows paths + run: npm run smoke:packaged-binaries + + - name: Generate Windows artifact checksums + run: npm run checksums:release -- SHA256SUMS-windows.txt + + - name: Attest Windows artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-windows.txt + + - name: Upload Windows build + uses: actions/upload-artifact@v4 + with: + name: windows-x64-release + path: | + release/*.exe + release/*.blockmap + release/latest*.yml + release/SHA256SUMS*.txt + if-no-files-found: error + retention-days: 30 + + build-macos: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + arch_tag: darwin-x64 + runner: macos-15-intel + - arch: arm64 + arch_tag: darwin-arm64 + runner: macos-14 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build macOS app + run: | + npm run build:platform-native-helpers + npx tsc + npx vite build + npx electron-builder --mac dir dmg zip --${{ matrix.arch }} --publish never + + - name: Smoke test packaged macOS paths + env: + PACKAGED_SMOKE_ARCH_TAGS: ${{ matrix.arch_tag }} + run: npm run smoke:packaged-binaries + + - name: Generate macOS artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-${{ matrix.arch }}.txt + + - name: Attest macOS artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-${{ matrix.arch }}.txt + + - name: Upload macOS build + uses: actions/upload-artifact@v4 + with: + name: macos-${{ matrix.arch }}-release + path: | + release/**/*.dmg + release/**/*.zip + release/**/*.blockmap + release/latest-mac.yml + release/SHA256SUMS*.txt + if-no-files-found: error + retention-days: 30 + + build-linux: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install bundled FFmpeg binary + run: node node_modules/ffmpeg-static/install.js + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + + - name: Install Linux native build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libx11-dev libxt-dev libxtst-dev libxkbfile-dev libxi-dev libxrandr-dev libxinerama-dev + + - name: Remove stale uiohook build output + run: rm -rf node_modules/uiohook-napi/build + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build Linux app + run: | + npm run build:platform-native-helpers + npx tsc + npx vite build + npx electron-builder --linux dir AppImage --x64 --publish never + + - name: Smoke test packaged Linux paths + run: npm run smoke:packaged-binaries + + - name: Generate Linux artifact checksums + run: npm run checksums:release -- SHA256SUMS-linux.txt + + - name: Attest Linux artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-linux.txt + + - name: Upload Linux build + uses: actions/upload-artifact@v4 + with: + name: linux-x64-release + path: | + release/**/*.AppImage + release/**/*.blockmap + release/latest-linux.yml + release/SHA256SUMS*.txt + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/homebrew-tap.yml b/.github/workflows/homebrew-tap.yml new file mode 100644 index 000000000..307ab1194 --- /dev/null +++ b/.github/workflows/homebrew-tap.yml @@ -0,0 +1,176 @@ +name: Update Homebrew Tap + +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag to publish to Homebrew tap (e.g. v1.0.5)" + required: true + type: string + tap_repo: + description: "Tap repository in owner/repo format" + required: false + default: "webadderall/homebrew-tap" + type: string + +permissions: + contents: read + +jobs: + update-tap: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + TAP_REPO: ${{ github.event.inputs.tap_repo || vars.HOMEBREW_TAP_REPO || 'webadderall/homebrew-tap' }} + AUTO_MERGE_TAP_PR: ${{ vars.HOMEBREW_TAP_AUTO_MERGE || 'true' }} + steps: + - name: Validate token + run: | + if [ -z "${GH_TOKEN}" ]; then + echo "HOMEBREW_TAP_TOKEN secret is not set." + exit 1 + fi + + - name: Resolve release tag + id: tag + shell: bash + run: | + TAG="${{ github.event.inputs.tag }}" + + if [ -z "$TAG" ]; then + echo "Release tag is empty" + exit 1 + fi + + VERSION="${TAG#v}" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Download release artifacts + shell: bash + run: | + set -euo pipefail + mkdir -p /tmp/recordly-release + gh release download "${{ steps.tag.outputs.tag }}" \ + --repo "${{ github.repository }}" \ + --dir /tmp/recordly-release \ + --pattern "Recordly-arm64.dmg" \ + --pattern "Recordly-x64.dmg" + + - name: Compute checksums + id: sha + shell: bash + run: | + set -euo pipefail + ARM_FILE=$(find /tmp/recordly-release -maxdepth 1 -name 'Recordly-arm64.dmg' | head -n 1) + INTEL_FILE=$(find /tmp/recordly-release -maxdepth 1 -name 'Recordly-x64.dmg' | head -n 1) + + if [ -z "$ARM_FILE" ] || [ -z "$INTEL_FILE" ]; then + echo "Unable to locate macOS release artifacts for ${{ steps.tag.outputs.tag }}" + exit 1 + fi + + SHA_ARM=$(sha256sum "$ARM_FILE" | awk '{print $1}') + SHA_INTEL=$(sha256sum "$INTEL_FILE" | awk '{print $1}') + echo "arm=$SHA_ARM" >> "$GITHUB_OUTPUT" + echo "intel=$SHA_INTEL" >> "$GITHUB_OUTPUT" + echo "arm_file=$(basename "$ARM_FILE")" >> "$GITHUB_OUTPUT" + echo "intel_file=$(basename "$INTEL_FILE")" >> "$GITHUB_OUTPUT" + + - name: Clone tap repository + shell: bash + run: | + gh repo clone "$TAP_REPO" /tmp/homebrew-tap + git -C /tmp/homebrew-tap remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${TAP_REPO}.git" + + - name: Update cask file + id: update + shell: bash + run: | + set -euo pipefail + cd /tmp/homebrew-tap + mkdir -p Casks + + BRANCH="recordly-cask-${{ steps.tag.outputs.tag }}" + git checkout -B "$BRANCH" + + cat > Casks/recordly.rb <<'RUBY' + cask "recordly" do + version "__VERSION__" + + on_arm do + sha256 "__SHA_ARM__" + url "https://github.com/__REPO__/releases/download/__TAG__/__ARM_FILE__" + end + + on_intel do + sha256 "__SHA_INTEL__" + url "https://github.com/__REPO__/releases/download/__TAG__/__INTEL_FILE__" + end + + name "Recordly" + desc "Open-source screen recorder and editor" + homepage "https://www.recordly.dev" + + app "Recordly.app" + end + RUBY + + sed -i "s/__VERSION__/${{ steps.tag.outputs.version }}/g" Casks/recordly.rb + sed -i "s/__SHA_ARM__/${{ steps.sha.outputs.arm }}/g" Casks/recordly.rb + sed -i "s/__SHA_INTEL__/${{ steps.sha.outputs.intel }}/g" Casks/recordly.rb + sed -i "s#__REPO__#${{ github.repository }}#g" Casks/recordly.rb + sed -i "s#__TAG__#${{ steps.tag.outputs.tag }}#g" Casks/recordly.rb + sed -i "s#__ARM_FILE__#${{ steps.sha.outputs.arm_file }}#g" Casks/recordly.rb + sed -i "s#__INTEL_FILE__#${{ steps.sha.outputs.intel_file }}#g" Casks/recordly.rb + + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Casks/recordly.rb + git commit -m "chore(cask): update recordly to ${{ steps.tag.outputs.tag }}" + git push --force-with-lease origin "$BRANCH" + + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" + + - name: Open pull request in tap repo + if: steps.update.outputs.changed == 'true' + id: pr + shell: bash + run: | + cd /tmp/homebrew-tap + + PR_NUMBER=$(gh pr list --head "${{ steps.update.outputs.branch }}" --json number --jq '.[0].number // empty') + if [ -n "$PR_NUMBER" ]; then + echo "PR already exists for ${{ steps.update.outputs.branch }} (#$PR_NUMBER)" + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + exit 0 + fi + + PR_URL=$(gh pr create \ + --title "chore(cask): update recordly to ${{ steps.tag.outputs.tag }}" \ + --body "Automated cask update for ${{ steps.tag.outputs.tag }}.\n\n- arm64 sha256: \\`${{ steps.sha.outputs.arm }}\\`\n- x64 sha256: \\`${{ steps.sha.outputs.intel }}\\`" \ + --head "${{ steps.update.outputs.branch }}" \ + --base main) + + PR_NUMBER=$(echo "$PR_URL" | sed -E 's#.*/pull/([0-9]+).*#\1#') + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + + - name: Auto-merge tap PR + if: steps.update.outputs.changed == 'true' && env.AUTO_MERGE_TAP_PR == 'true' + shell: bash + run: | + cd /tmp/homebrew-tap + + PR_NUMBER="${{ steps.pr.outputs.number }}" + if [ -z "$PR_NUMBER" ]; then + echo "No pull request number available; skipping auto-merge." + exit 0 + fi + + gh pr merge "$PR_NUMBER" --squash --delete-branch --repo "$TAP_REPO" diff --git a/.github/workflows/macos-release-candidate.yml b/.github/workflows/macos-release-candidate.yml new file mode 100644 index 000000000..51af13cfe --- /dev/null +++ b/.github/workflows/macos-release-candidate.yml @@ -0,0 +1,297 @@ +name: macOS Release Candidate + +run-name: macOS RC validation for ${{ inputs.source_sha }} + +on: + workflow_dispatch: + inputs: + source_sha: + description: Exact 40-character SHA at the current head of main + required: true + type: string + +permissions: + contents: read + +concurrency: + group: macos-release-candidate-${{ inputs.source_sha }} + cancel-in-progress: false + +jobs: + authorize-source: + name: Authorize source commit + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + source_sha: ${{ steps.authorize.outputs.source_sha }} + steps: + - name: Checkout current main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: main + + - name: Require the canonical repository and current main + id: authorize + shell: bash + env: + REQUESTED_SHA: ${{ inputs.source_sha }} + run: | + set -euo pipefail + + if [[ "${GITHUB_REPOSITORY,,}" != "webadderallorg/recordly" ]]; then + echo "This workflow may run only in webadderallorg/Recordly." + exit 1 + fi + + if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then + echo "This workflow must itself be dispatched from main." + exit 1 + fi + + requested_sha="${REQUESTED_SHA,,}" + if [[ ! "$requested_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "source_sha must be an exact 40-character commit SHA." + exit 1 + fi + + main_sha="$(git rev-parse HEAD)" + if [[ "$requested_sha" != "$main_sha" ]]; then + echo "Refusing to sign a stale or non-main commit." + echo "Requested: $requested_sha" + echo "Current main: $main_sha" + exit 1 + fi + + echo "source_sha=$main_sha" >> "$GITHUB_OUTPUT" + echo "Authorized current main commit: $main_sha" + + build-and-verify: + name: Sign, notarize, and verify macOS ${{ matrix.arch }} + needs: authorize-source + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + arch_tag: darwin-x64 + runner: macos-15-intel + - arch: arm64 + arch_tag: darwin-arm64 + runner: macos-14 + env: + CI: true + steps: + - name: Checkout authorized source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ needs.authorize-source.outputs.source_sha }} + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: npm + cache-dependency-path: package-lock.json + node-version: '22' + + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.11' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install and verify bundled FFmpeg + shell: bash + run: | + set -euo pipefail + node scripts/install-ffmpeg-static.mjs + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) throw new Error('Bundled FFmpeg is missing'); console.log(ffmpeg);" + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Validate source entitlement plists + run: | + plutil -lint build/entitlements.mac.plist + plutil -lint build/entitlements.mac.inherit.plist + + - name: Build macOS application inputs + shell: bash + run: | + set -euo pipefail + npm run build:platform-native-helpers + npx tsc + npx vite build --config vite.config.ts + npm run normalize:electron-main-cjs + npm run smoke:electron-main-cjs + + - name: Validate Apple signing certificate + shell: bash + env: + APPLE_SIGNING_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_P12_BASE64 }} + APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + set -euo pipefail + umask 077 + + for name in APPLE_SIGNING_CERTIFICATE_P12_BASE64 APPLE_SIGNING_CERTIFICATE_PASSWORD APPLE_TEAM_ID; do + if [[ -z "${!name:-}" ]]; then + echo "Missing required macOS candidate secret: $name" + exit 1 + fi + done + + if [[ ! "$APPLE_TEAM_ID" =~ ^[A-Z0-9]{10}$ ]]; then + echo "APPLE_TEAM_ID must be exactly 10 uppercase letters or digits." + exit 1 + fi + + cert_path="$RUNNER_TEMP/recordly-developer-id.p12" + cert_pem_path="$RUNNER_TEMP/recordly-developer-id.pem" + printf '%s' "$APPLE_SIGNING_CERTIFICATE_P12_BASE64" | base64 --decode > "$cert_path" + if ! openssl pkcs12 \ + -in "$cert_path" \ + -clcerts \ + -nokeys \ + -passin env:APPLE_SIGNING_CERTIFICATE_PASSWORD \ + -out "$cert_pem_path"; then + rm -f "$cert_pem_path" + pkcs12_help="$(openssl pkcs12 -help 2>&1 || true)" + if [[ "$pkcs12_help" != *"-legacy"* ]]; then + echo "PKCS#12 extraction failed and this OpenSSL has no legacy-provider fallback." + exit 1 + fi + echo "Standard PKCS#12 extraction failed; retrying legacy Keychain compatibility." + openssl pkcs12 \ + -legacy \ + -in "$cert_path" \ + -clcerts \ + -nokeys \ + -passin env:APPLE_SIGNING_CERTIFICATE_PASSWORD \ + -out "$cert_pem_path" + fi + + openssl x509 -in "$cert_pem_path" -noout -checkend 86400 + cert_subject="$(openssl x509 -in "$cert_pem_path" -noout -subject -nameopt RFC2253 | sed 's/^subject=//')" + if [[ "$cert_subject" != *"CN=Developer ID Application:"* ]]; then + echo "The P12 leaf certificate is not a Developer ID Application certificate." + exit 1 + fi + if [[ ! "$cert_subject" =~ (^|,)OU=${APPLE_TEAM_ID}(,|$) ]]; then + echo "The P12 certificate team does not match APPLE_TEAM_ID." + exit 1 + fi + + echo "Developer ID certificate preflight passed and remains valid for at least 24 hours." + + - name: Authenticate Apple notarization credentials + timeout-minutes: 5 + shell: bash + env: + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + set -euo pipefail + + for name in APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do + if [[ -z "${!name:-}" ]]; then + echo "Missing required macOS candidate secret: $name" + exit 1 + fi + done + + xcrun notarytool history \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + >/dev/null + + echo "Apple notarization credential preflight passed." + + - name: Package, sign, and notarize macOS ${{ matrix.arch }} + shell: bash + env: + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + CSC_KEY_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} + CSC_LINK: ${{ runner.temp }}/recordly-developer-id.p12 + run: | + set -euo pipefail + npx electron-builder \ + --mac dir dmg zip \ + --${{ matrix.arch }} \ + --publish never \ + -c.mac.forceCodeSigning=true \ + -c.mac.notarize=true + + - name: Remove local signing material + if: always() + shell: bash + run: | + rm -f \ + "$RUNNER_TEMP/recordly-developer-id.p12" \ + "$RUNNER_TEMP/recordly-developer-id.pem" + + - name: Smoke test packaged binary paths + env: + PACKAGED_SMOKE_ARCH_TAGS: ${{ matrix.arch_tag }} + run: npm run smoke:packaged-binaries + + - name: Verify signed and notarized distribution artifacts + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: >- + npm run verify:macos-distribution -- + --release-dir release + --arch ${{ matrix.arch }} + --team-id "$APPLE_TEAM_ID" + --report "release/macos-distribution-report-${{ matrix.arch }}.json" + --summary "$GITHUB_STEP_SUMMARY" + + - name: Generate candidate checksums + run: npm run checksums:release -- SHA256SUMS-macos-${{ matrix.arch }}.txt + + - name: Upload temporary candidate evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: macos-${{ matrix.arch }}-release-candidate + path: | + release/*.dmg + release/*.zip + release/*.blockmap + release/latest-mac.yml + release/SHA256SUMS-macos-${{ matrix.arch }}.txt + release/macos-distribution-report-${{ matrix.arch }}.json + if-no-files-found: error + retention-days: 3 + + candidate-verdict: + name: macOS candidate verdict + if: always() + needs: + - authorize-source + - build-and-verify + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Require both architectures to pass + shell: bash + env: + AUTHORIZE_RESULT: ${{ needs.authorize-source.result }} + BUILD_RESULT: ${{ needs.build-and-verify.result }} + run: | + set -euo pipefail + if [[ "$AUTHORIZE_RESULT" != "success" || "$BUILD_RESULT" != "success" ]]; then + echo "macOS candidate rejected: authorize=$AUTHORIZE_RESULT build=$BUILD_RESULT" + exit 1 + fi + echo "Both signed and notarized macOS architectures passed the distribution gate." diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 000000000..bb3499870 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,71 @@ +name: Code Quality + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Repository quality + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + CI: true + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '22' + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + id: install + run: npm ci --ignore-scripts + + - name: Typecheck + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npx tsc --noEmit + + - name: Lint + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run lint + + - name: Check formatting (advisory) + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + continue-on-error: true + run: npm run format:check + + # Real recording-import tests require the binary skipped by --ignore-scripts. + - name: Install FFmpeg for integration tests + id: ffmpeg + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm rebuild ffmpeg-static + + - name: Test + if: ${{ !cancelled() && steps.install.outcome == 'success' && steps.ffmpeg.outcome == 'success' }} + run: npm test + + # Main currently has known locale-parity debt covered by PR #710. Keep the + # check visible without blocking unrelated PRs; make it required once that + # existing translation PR (or an equivalent fix) lands. + - name: Check translations (advisory) + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + continue-on-error: true + run: npm run i18n:check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..dea29ea6d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,746 @@ +name: Publish Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag_name: + description: Existing release tag to rebuild and publish + required: true + type: string + release_scope: + description: Platforms to publish + required: true + default: all + type: choice + options: + - all + - windows-linux + +permissions: + contents: write + actions: write + id-token: write + attestations: write + artifact-metadata: write + +concurrency: + group: release-${{ github.event.release.tag_name || github.event.inputs.tag_name || github.run_id }} + cancel-in-progress: false + +jobs: + prepare-release: + name: Prepare release + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.prepare.outputs.tag_name }} + package_version: ${{ steps.prepare.outputs.package_version }} + source_sha: ${{ steps.prepare.outputs.source_sha }} + prerelease: ${{ steps.prepare.outputs.prerelease }} + release_scope: ${{ steps.prepare.outputs.release_scope }} + steps: + - name: Checkout release source + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.tag_name }} + fetch-depth: 0 + + - name: Validate release tag + id: prepare + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_BODY: ${{ github.event.release.body }} + REQUESTED_SCOPE: ${{ github.event.inputs.release_scope }} + RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} + DISPATCH_TAG_NAME: ${{ github.event.inputs.tag_name }} + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "release" ]; then + TAG_NAME="$RELEASE_TAG_NAME" + IS_PRERELEASE="${{ github.event.release.prerelease }}" + RELEASE_SCOPE="all" + if [ "$IS_PRERELEASE" = "true" ] && grep -Fq '' <<< "$RELEASE_BODY"; then + RELEASE_SCOPE="windows-linux" + fi + else + TAG_NAME="$DISPATCH_TAG_NAME" + IS_PRERELEASE="$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json isPrerelease --jq '.isPrerelease' 2>/dev/null || echo false)" + RELEASE_SCOPE="${REQUESTED_SCOPE:-all}" + fi + + if [ -z "$TAG_NAME" ]; then + echo "Release tag is required." + exit 1 + fi + + TAG_VERSION="${TAG_NAME#v}" + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then + echo "Release tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)." + exit 1 + fi + + case "$RELEASE_SCOPE" in + all|windows-linux) ;; + *) + echo "Unsupported release scope: $RELEASE_SCOPE" + exit 1 + ;; + esac + + if [ "$IS_PRERELEASE" != "true" ] && [ "$RELEASE_SCOPE" != "all" ]; then + echo "Partial platform scope is allowed only for prereleases." + exit 1 + fi + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + gh release view "$TAG_NAME" --repo "${{ github.repository }}" >/dev/null 2>&1 || \ + gh release create "$TAG_NAME" --repo "${{ github.repository }}" --verify-tag --draft --title "$TAG_NAME" + fi + + echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" + echo "package_version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT" + echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + echo "prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" + echo "release_scope=$RELEASE_SCOPE" >> "$GITHUB_OUTPUT" + build-macos-x64: + name: Build macOS x64 + needs: prepare-release + if: needs.prepare-release.outputs.release_scope == 'all' + runs-on: macos-15-intel + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare-release.outputs.source_sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Validate macOS signing secrets + shell: bash + env: + APPLE_SIGNING_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_P12_BASE64 }} + APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + for name in APPLE_SIGNING_CERTIFICATE_P12_BASE64 APPLE_SIGNING_CERTIFICATE_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!name:-}" ]; then + echo "Missing required macOS release secret: $name" + exit 1 + fi + done + + - name: Prepare macOS signing certificate + shell: bash + env: + APPLE_SIGNING_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_P12_BASE64 }} + APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + CERT_PATH="$RUNNER_TEMP/apple-signing-cert.p12" + echo "$APPLE_SIGNING_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT_PATH" + { + echo "CSC_LINK=$CERT_PATH" + echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD" + } >> "$GITHUB_ENV" + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install bundled FFmpeg binary + run: node scripts/install-ffmpeg-static.mjs + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build native macOS helpers + run: npm run build:platform-native-helpers + + - name: Build macOS x64 + run: | + npx tsc + npx vite build + npx electron-builder --mac dir dmg zip --x64 --publish never + + - name: Smoke test packaged macOS x64 paths + env: + PACKAGED_SMOKE_ARCH_TAGS: darwin-x64 + run: npm run smoke:packaged-binaries + + - name: Generate macOS x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-x64.txt + + - name: Attest macOS x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-x64.txt + + - name: Upload macOS x64 artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-x64-release + path: | + release/*.dmg + release/*.zip + release/*.blockmap + release/latest-mac.yml + release/SHA256SUMS*.txt + if-no-files-found: error + + build-macos-arm64: + name: Build macOS arm64 + needs: prepare-release + if: needs.prepare-release.outputs.release_scope == 'all' + runs-on: macos-14 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare-release.outputs.source_sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Validate macOS signing secrets + shell: bash + env: + APPLE_SIGNING_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_P12_BASE64 }} + APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + for name in APPLE_SIGNING_CERTIFICATE_P12_BASE64 APPLE_SIGNING_CERTIFICATE_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!name:-}" ]; then + echo "Missing required macOS release secret: $name" + exit 1 + fi + done + + - name: Prepare macOS signing certificate + shell: bash + env: + APPLE_SIGNING_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_P12_BASE64 }} + APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + CERT_PATH="$RUNNER_TEMP/apple-signing-cert.p12" + echo "$APPLE_SIGNING_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT_PATH" + { + echo "CSC_LINK=$CERT_PATH" + echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD" + } >> "$GITHUB_ENV" + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install bundled FFmpeg binary + run: node scripts/install-ffmpeg-static.mjs + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build native macOS helpers + run: npm run build:platform-native-helpers + + - name: Build macOS arm64 + run: | + npx tsc + npx vite build + npx electron-builder --mac dir dmg zip --arm64 --publish never + + - name: Smoke test packaged macOS arm64 paths + env: + PACKAGED_SMOKE_ARCH_TAGS: darwin-arm64 + run: npm run smoke:packaged-binaries + + - name: Generate macOS arm64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-arm64.txt + + - name: Attest macOS arm64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-arm64.txt + + - name: Upload macOS arm64 artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-arm64-release + path: | + release/*.dmg + release/*.zip + release/*.blockmap + release/latest-mac.yml + release/SHA256SUMS*.txt + if-no-files-found: error + + merge-macos-update-metadata: + name: Merge macOS update metadata + needs: + - prepare-release + - build-macos-x64 + - build-macos-arm64 + if: needs.prepare-release.outputs.release_scope == 'all' + runs-on: ubuntu-latest + steps: + - name: Download macOS x64 artifacts + uses: actions/download-artifact@v4 + with: + name: macos-x64-release + path: release-assets/macos-x64 + + - name: Download macOS arm64 artifacts + uses: actions/download-artifact@v4 + with: + name: macos-arm64-release + path: release-assets/macos-arm64 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install PyYAML + run: python -m pip install pyyaml + + - name: Merge latest-mac.yml + shell: bash + run: | + set -euo pipefail + mkdir -p release-assets/merged + python <<'PY' + from pathlib import Path + import yaml + + x64_info = yaml.safe_load(Path('release-assets/macos-x64/latest-mac.yml').read_text()) + arm64_info = yaml.safe_load(Path('release-assets/macos-arm64/latest-mac.yml').read_text()) + + merged_files = [] + seen_urls = set() + for info in (x64_info, arm64_info): + for file_info in info.get('files', []): + url = file_info.get('url') + if url and url not in seen_urls: + seen_urls.add(url) + merged_files.append(file_info) + + if not merged_files: + raise SystemExit('No macOS update files found to merge.') + + preferred = next((item for item in merged_files if 'x64' in item.get('url', '')), merged_files[0]) + merged = dict(x64_info) + merged['files'] = merged_files + merged['path'] = preferred.get('url', merged.get('path')) + merged['sha512'] = preferred.get('sha512', merged.get('sha512')) + if not merged.get('releaseDate') and arm64_info.get('releaseDate'): + merged['releaseDate'] = arm64_info['releaseDate'] + + Path('release-assets/merged/latest-mac.yml').write_text( + yaml.safe_dump(merged, sort_keys=False) + ) + PY + + - name: Upload merged latest-mac.yml artifact + uses: actions/upload-artifact@v4 + with: + name: macos-merged-metadata + path: release-assets/merged/latest-mac.yml + if-no-files-found: error + + build-windows-x64: + name: Build Windows x64 + needs: prepare-release + runs-on: windows-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare-release.outputs.source_sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Configure Windows signing + shell: pwsh + env: + WINDOWS_SIGNING_CERTIFICATE_P12_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_P12_BASE64 }} + WINDOWS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} + run: | + if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERTIFICATE_P12_BASE64) -or [string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERTIFICATE_PASSWORD)) { + Write-Warning 'Windows signing secrets are missing. Building an unsigned installer.' + Add-Content -Path $env:GITHUB_ENV -Value 'CSC_IDENTITY_AUTO_DISCOVERY=false' + exit 0 + } + + $certPath = Join-Path $env:RUNNER_TEMP 'windows-signing-cert.p12' + [IO.File]::WriteAllBytes($certPath, [Convert]::FromBase64String($env:WINDOWS_SIGNING_CERTIFICATE_P12_BASE64)) + Add-Content -Path $env:GITHUB_ENV -Value "WIN_CSC_LINK=$certPath" + Add-Content -Path $env:GITHUB_ENV -Value "WIN_CSC_KEY_PASSWORD=$env:WINDOWS_SIGNING_CERTIFICATE_PASSWORD" + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install bundled FFmpeg binary + run: node scripts/install-ffmpeg-static.mjs + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + + - name: Remove stale uiohook build output + shell: pwsh + run: | + if (Test-Path node_modules/uiohook-napi/build) { + Remove-Item node_modules/uiohook-napi/build -Recurse -Force + } + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build Windows x64 + shell: pwsh + run: | + npm run build:platform-native-helpers + npx tsc + npx vite build + npx electron-builder --win dir nsis --x64 --publish never + + - name: Smoke test packaged Windows x64 paths + run: npm run smoke:packaged-binaries + + - name: Generate Windows x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-windows-x64.txt + + - name: Attest Windows x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-windows-x64.txt + + - name: Upload Windows x64 artifacts + uses: actions/upload-artifact@v4 + with: + name: windows-x64-release + path: | + release/*.exe + release/*.blockmap + release/latest*.yml + release/SHA256SUMS*.txt + if-no-files-found: error + + build-linux-x64: + name: Build Linux x64 + needs: prepare-release + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare-release.outputs.source_sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Linux native build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libx11-dev libxt-dev libxtst-dev libxkbfile-dev libxi-dev libxrandr-dev libxinerama-dev + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install bundled FFmpeg binary + run: node scripts/install-ffmpeg-static.mjs + + - name: Verify bundled FFmpeg binary + run: | + node -e "const fs=require('node:fs'); const ffmpeg=require('ffmpeg-static'); if (typeof ffmpeg !== 'string' || !fs.existsSync(ffmpeg)) { throw new Error('Bundled ffmpeg-static binary is missing: ' + ffmpeg); } console.log('Using bundled ffmpeg binary: ' + ffmpeg);" + + - name: Remove stale uiohook build output + run: rm -rf node_modules/uiohook-napi/build + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build Linux x64 + run: | + export CSC_IDENTITY_AUTO_DISCOVERY=false + npm run build:platform-native-helpers + npx tsc + npx vite build + npx electron-builder --linux dir AppImage --x64 --publish never + + - name: Smoke test packaged Linux x64 paths + run: npm run smoke:packaged-binaries + + - name: Generate Linux x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-linux-x64.txt + + - name: Attest Linux x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-linux-x64.txt + + - name: Upload Linux x64 artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-x64-release + path: | + release/*.AppImage + release/*.blockmap + release/latest-linux.yml + release/SHA256SUMS*.txt + if-no-files-found: error + + publish-release-assets: + name: Publish release assets + needs: + - prepare-release + - build-macos-x64 + - build-macos-arm64 + - merge-macos-update-metadata + - build-windows-x64 + - build-linux-x64 + if: | + always() && + needs.prepare-release.result == 'success' && + needs.build-windows-x64.result == 'success' && + needs.build-linux-x64.result == 'success' && + ( + needs.prepare-release.outputs.release_scope == 'windows-linux' || + ( + needs.build-macos-x64.result == 'success' && + needs.build-macos-arm64.result == 'success' && + needs.merge-macos-update-metadata.result == 'success' + ) + ) + runs-on: ubuntu-latest + steps: + - name: Download macOS x64 artifacts + if: needs.prepare-release.outputs.release_scope == 'all' + uses: actions/download-artifact@v4 + with: + name: macos-x64-release + path: release-assets/macos-x64 + + - name: Download macOS arm64 artifacts + if: needs.prepare-release.outputs.release_scope == 'all' + uses: actions/download-artifact@v4 + with: + name: macos-arm64-release + path: release-assets/macos-arm64 + + - name: Download merged macOS metadata + if: needs.prepare-release.outputs.release_scope == 'all' + uses: actions/download-artifact@v4 + with: + name: macos-merged-metadata + path: release-assets/macos-merged + + - name: Download Windows x64 artifacts + uses: actions/download-artifact@v4 + with: + name: windows-x64-release + path: release-assets/windows-x64 + + - name: Download Linux x64 artifacts + uses: actions/download-artifact@v4 + with: + name: linux-x64-release + path: release-assets/linux-x64 + + - name: Stage release assets + shell: bash + env: + RELEASE_SCOPE: ${{ needs.prepare-release.outputs.release_scope }} + IS_PRERELEASE: ${{ needs.prepare-release.outputs.prerelease }} + run: | + set -euo pipefail + shopt -s nullglob + + mkdir -p release-assets/upload + + if [ "$IS_PRERELEASE" = "true" ] && [ "$RELEASE_SCOPE" != "all" ]; then + echo "Prerelease beta updates must include all platforms so every opted-in client can resolve beta metadata." + exit 1 + fi + + required_metadata=( + release-assets/windows-x64/latest.yml + release-assets/linux-x64/latest-linux.yml + ) + + if [ "$RELEASE_SCOPE" = "all" ]; then + required_metadata+=(release-assets/macos-merged/latest-mac.yml) + fi + + for file in "${required_metadata[@]}"; do + if [ ! -f "$file" ]; then + echo "Required update metadata is missing: $file" + exit 1 + fi + done + + if [ "$IS_PRERELEASE" = "true" ]; then + cp release-assets/windows-x64/latest.yml release-assets/windows-x64/beta.yml + cp release-assets/linux-x64/latest-linux.yml release-assets/linux-x64/beta-linux.yml + update_metadata=( + release-assets/windows-x64/beta.yml + release-assets/linux-x64/beta-linux.yml + ) + + if [ "$RELEASE_SCOPE" = "all" ]; then + cp release-assets/macos-merged/latest-mac.yml release-assets/macos-merged/beta-mac.yml + update_metadata+=(release-assets/macos-merged/beta-mac.yml) + fi + else + update_metadata=( + release-assets/windows-x64/latest.yml + release-assets/linux-x64/latest-linux.yml + ) + + if [ "$RELEASE_SCOPE" = "all" ]; then + update_metadata+=(release-assets/macos-merged/latest-mac.yml) + fi + fi + + assets=( + release-assets/windows-x64/*.exe + release-assets/windows-x64/*.blockmap + release-assets/linux-x64/*.AppImage + release-assets/linux-x64/*.blockmap + ) + + if [ "$RELEASE_SCOPE" = "all" ]; then + assets+=( + release-assets/macos-x64/*.dmg + release-assets/macos-x64/*.zip + release-assets/macos-x64/*.blockmap + release-assets/macos-arm64/*.dmg + release-assets/macos-arm64/*.zip + release-assets/macos-arm64/*.blockmap + ) + fi + + assets+=("${update_metadata[@]}") + + if [ ${#assets[@]} -eq 0 ]; then + echo "No release assets found to checksum." + exit 1 + fi + + for file in "${assets[@]}"; do + target="release-assets/upload/$(basename "$file")" + if [ -e "$target" ]; then + echo "Duplicate release asset name: $(basename "$file")" + exit 1 + fi + cp "$file" "$target" + done + + - name: Generate release asset checksums + shell: bash + run: | + set -euo pipefail + cd release-assets/upload + sha256sum * | sort -k2,2 > SHA256SUMS.txt + sha256sum -c SHA256SUMS.txt + + - name: Attest release assets + uses: actions/attest@v4 + with: + subject-checksums: release-assets/upload/SHA256SUMS.txt + + - name: Upload release assets to GitHub + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + + assets=( + release-assets/upload/* + ) + + if [ ${#assets[@]} -eq 0 ]; then + echo "No release assets found to upload." + exit 1 + fi + + gh release upload "${{ needs.prepare-release.outputs.tag_name }}" \ + "${assets[@]}" \ + --repo "${{ github.repository }}" \ + --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + trigger-homebrew-update: + name: Trigger Homebrew tap update + needs: + - prepare-release + - publish-release-assets + runs-on: ubuntu-latest + if: needs.prepare-release.outputs.prerelease != 'true' && needs.prepare-release.outputs.release_scope == 'all' + steps: + - name: Dispatch Homebrew tap workflow + shell: bash + run: | + gh workflow run homebrew-tap.yml \ + --repo "${{ github.repository }}" \ + -f tag="${{ needs.prepare-release.outputs.tag_name }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/winget-releaser.yml b/.github/workflows/winget-releaser.yml new file mode 100644 index 000000000..741d48fd3 --- /dev/null +++ b/.github/workflows/winget-releaser.yml @@ -0,0 +1,26 @@ +name: Publish release to WinGet +on: + workflow_run: + workflows: ["Publish Release"] + types: [completed] + workflow_dispatch: +jobs: + publish: + runs-on: windows-latest + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' + steps: + - name: Get release tag + id: tag + shell: bash + run: | + TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name') + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: Webadderall.Recordly + installers-regex: 'windows\-x64\.exe$' + version: ${{ steps.tag.outputs.tag }} + release-tag: ${{ steps.tag.outputs.tag }} + token: ${{ secrets.WINGET_ACC_TOKEN }} diff --git a/.gitignore b/.gitignore index ea2ff9e84..de89bad27 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ lerna-debug.log* node_modules dist +dist-electron dist-ssr *.local @@ -23,5 +24,28 @@ dist-ssr *.sln *.sw? release/** +*.kiro/ +# npx electron-builder --mac --win +.tmp/ +.history/ +extension-sources/ +*.tsbuildinfo +vite.config.js +vite.config.d.ts -# npx electron-builder --mac --win \ No newline at end of file +# Native capture build artifacts +electron/native/wgc-capture/build/ +electron/native/cursor-monitor/build/ +electron/native/gpu-export-probe/build/ +electron/native/nvidia-cuda-compositor/build/ +electron/native/bin/*/whisper-* +electron/native/bin/*/whisper-runtime.json + +# Local debug helpers +tmp-*.ps1 +.tmp-*.ps1 +gpu-export-probe.mp4 + +# Browser test output +test-results/ +playwright-report/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..196186e59 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contribution Guidelines + +Thank you for considering contributing to this project! By contributing, you help make this project better for everyone. Please take a moment to review these guidelines to ensure a smooth contribution process. + +Areas where help is especially valuable: +- Export optimisations +- Native screen recording for Linux +- Wallpaper submissions +- Extensions (device frames, click effects, render hooks — see [EXTENSIONS.md](./EXTENSIONS.md)) + +## How to Contribute + +1. **Fork the Repository** + - Click the "Fork" button at the top right of this repository to create your own copy. + +2. **Clone Your Fork** + - Clone your forked repository to your local machine: + ```bash + git clone https://github.com/your-username/Recordly.git + ``` + +3. **Create a New Branch** + - Create a branch for your feature or bug fix: + ```bash + git checkout -b feature/your-feature-name + ``` + +4. **Make Changes** + - Make your changes. + +5. **Test Your Changes** + - Test your changes thoroughly to ensure they work as expected and do not break existing functionality. + +6. **Commit Your Changes** + - Commit your changes with a clear and concise commit message: + ```bash + git add . + git commit -m "Add a brief description of your changes" + ``` + +7. **Push Your Changes** + - Push your branch to your forked repository: + ```bash + git push origin feature/your-feature-name + ``` + +8. **Open a Pull Request** + - Go to the original repository and open a pull request from your branch. Provide a clear description of your changes and the problem they solve. + +## Reporting Issues + +If you encounter a bug or have a feature request, please open an issue in the [Issues](https://github.com/webadderallorg/Recordly/issues) section of this repository. Provide as much detail as possible to help us address the issue effectively. + +## Style Guide + +- Write clear, concise, and descriptive commit messages. +- Include comments where necessary to explain complex code. + +## License + +By contributing to this project, you agree that your contributions will be licensed under the [GNU Affero General Public License v3.0](./LICENSE.md). + +Thank you for your contributions! diff --git a/EXTENSIONS.md b/EXTENSIONS.md new file mode 100644 index 000000000..1104e9526 --- /dev/null +++ b/EXTENSIONS.md @@ -0,0 +1,310 @@ +# Recordly Extension API + +Go to https://www.marketplace.recordly.dev/extensions for full, regularly updated documentation + +Recordly extensions run in the editor renderer and use a permission-gated host API. They can draw into the render pipeline, react to playback and export events, register cursor effects, add settings panels, and contribute packaged assets such as frames, wallpapers, and cursor styles. + +## Quick Start + +For local user-installed extensions, use `Extensions -> Open Directory` in the app. Recordly stores them in the app `userData/extensions` directory. This repo also includes installable example bundles under `extension-examples/`. + +### Minimum Extension + +```text +my-extension/ + recordly-extension.json + index.js +``` + +```json +{ + "id": "com.example.my-extension", + "name": "My Extension", + "version": "1.0.0", + "description": "A short description", + "author": "Your Name", + "license": "MIT", + "main": "index.js", + "permissions": ["render"] +} +``` + +```js +export function activate(api) { + api.log("Hello from my extension"); +} + +export function deactivate() {} +``` + +### TypeScript + +Extensions can be authored in TypeScript. Import the `RecordlyExtensionAPI` type from `types.ts` in this repo for full IDE auto-complete and compile-time checks: + +```ts +import type { RecordlyExtensionAPI } from "./types"; + +export function activate(api: RecordlyExtensionAPI) { + api.registerRenderHook("final", (ctx) => { + ctx.ctx.fillStyle = "rgba(255,255,255,0.1)"; + ctx.ctx.fillRect(0, 0, ctx.width, 30); + }); +} + +export function deactivate() {} +``` + +Recordly loads the `main` entry from the manifest, which must be a `.js` file. If you author in TypeScript, compile or bundle to JavaScript before packaging. A `tsconfig.json` with `"module": "ESNext"` and `"target": "ESNext"` works well since extensions run in a Chromium renderer. + +### Manifest Screenshots + +Extensions can include `screenshots` in the manifest to show preview images in the marketplace: + +```json +{ + "id": "com.example.my-extension", + "screenshots": [ + "screenshots/preview-1.png", + "screenshots/preview-2.png" + ] +} +``` + +Paths are relative to the extension root. The marketplace displays screenshots in a carousel on both the in-app detail modal and the web detail page. + +## Manifest + +`recordly-extension.json` is validated both when the extension loads and when a zip is uploaded to the marketplace. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | `string` | Yes | Unique identifier, for example `yourname.cool-effect` | +| `name` | `string` | Yes | Human-readable name shown in the UI | +| `version` | `string` | Yes | Strict semver version, for example `1.0.0` | +| `description` | `string` | Yes | One-line summary | +| `author` | `string` | No | Author or organisation | +| `homepage` | `string` | No | HTTPS homepage or repository URL | +| `license` | `string` | No | SPDX license identifier | +| `engine` | `string` | No | Minimum supported Recordly version | +| `icon` | `string` | No | Relative path to a PNG icon | +| `screenshots` | `string[]` | No | Relative paths to preview images shown in the marketplace | +| `main` | `string` | Yes | Relative entry point JS file | +| `permissions` | `string[]` | Yes | Required capabilities | +| `contributes` | `object` | No | Metadata for packaged frames, cursor styles, sounds, wallpapers, and webcam frames | + +`contributes` is metadata only today. Recordly does not auto-register runtime behavior from the manifest. Use `activate()` to call APIs such as `registerFrame()`, `registerWallpaper()`, `registerCursorStyle()`, `registerSettingsPanel()`, and `playSound()`. + +## Permissions + +| Permission | Grants access to | +|-----------|------------------| +| `render` | Render hook registration | +| `cursor` | Cursor telemetry and cursor effect registration | +| `audio` | Bundled sound playback | +| `timeline` | Playback and timeline events | +| `ui` | Settings panels and device frame registration | +| `assets` | Bundled asset resolution plus wallpaper and cursor style registration | +| `export` | Export lifecycle events | + +## Render Pipeline + +Render hooks draw into `hookCtx.ctx`, a `CanvasRenderingContext2D`, at specific phases. + +| Phase | Preview | Export | Notes | +|-------|---------|--------|-------| +| `background` | Reserved | Reserved | Exists in the type surface but is not dispatched yet | +| `post-video` | Yes | Yes | Runs inside the scene transform | +| `post-zoom` | Yes | Yes | Runs inside the scene transform | +| `post-cursor` | Yes | Yes | Runs inside the scene transform | +| `post-webcam` | Yes | Yes | Runs after the built-in transform | +| `post-annotations` | Yes | Yes | Runs after the built-in transform | +| `final` | Yes | Yes | Last pass for HUD-style overlays | + +### Inside vs Outside the Scene Transform + +- `post-video`, `post-zoom`, and `post-cursor` already follow zoom and motion in preview and export. +- `post-webcam`, `post-annotations`, and `final` run after Recordly restores the canvas transform. Use `sceneTransform` manually if you want those overlays to move with the scene. + +### RenderHookContext + +```ts +{ + width: number; + height: number; + timeMs: number; + durationMs: number; + cursor: { cx: number; cy: number; interactionType?: string } | null; + smoothedCursor?: { + cx: number; + cy: number; + trail: Array<{ cx: number; cy: number }>; + } | null; + ctx: CanvasRenderingContext2D; + videoLayout?: { + maskRect: { x: number; y: number; width: number; height: number }; + borderRadius: number; + padding: number; + }; + zoom?: { scale: number; focusX: number; focusY: number; progress: number }; + sceneTransform?: { scale: number; x: number; y: number }; + shadow?: { enabled: boolean; intensity: number }; + getPixelColor(x: number, y: number): { r: number; g: number; b: number; a: number }; + getAverageSceneColor(): { r: number; g: number; b: number; a: number }; + getEdgeAverageColor(edgeWidth?: number): { r: number; g: number; b: number; a: number }; + getDominantColors(count?: number): Array<{ r: number; g: number; b: number; frequency: number }>; +} +``` + +Use `videoLayout.maskRect` and `videoLayout.borderRadius` for scene-relative sizing. That gives you true scene borders and correctly shaped rounded corners instead of canvas-wide overlays. + +## Cursor Effects + +Cursor effect callbacks run each frame after a click until they return `false`. + +```js +api.registerCursorEffect((ctx) => { + const progress = ctx.elapsedMs / 400; + if (progress >= 1) return false; + + const sceneWidth = ctx.videoLayout?.maskRect.width ?? ctx.width; + const x = ctx.cx * ctx.width; + const y = ctx.cy * ctx.height; + const radius = sceneWidth * 0.03 * progress; + + ctx.ctx.beginPath(); + ctx.ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.ctx.stroke(); + return true; +}); +``` + +`CursorEffectContext` now includes `videoLayout`, `zoom`, and `sceneTransform`, so effects can scale relative to the scene instead of the full canvas. + +## API Surface + +### Registration + +```js +api.registerRenderHook(phase, hook); +api.registerCursorEffect(effect); +api.registerFrame(frame); +api.registerWallpaper(wallpaper); +api.registerCursorStyle(cursorStyle); +api.registerSettingsPanel(panel); +api.on(event, handler); +``` + +Every registration returns a dispose function. + +### Settings + +```js +api.getSetting(settingId); +api.setSetting(settingId, value); +api.onSettingChange((settingId, value) => {}); +api.getAllSettings(); +``` + +Supported settings field types are `toggle`, `slider`, `select`, `color`, and `text`. + +### Assets and Audio + +```js +api.resolveAsset("images/overlay.png"); +api.playSound("sounds/click.mp3", { volume: 0.8 }); +api.log("hello", payload); +``` + +### Read-only Queries + +```js +api.getVideoInfo(); +api.getVideoLayout(); +api.getCursorAt(timeMs); +api.getSmoothedCursor(); +api.getZoomState(); +api.getShadowConfig(); +api.getKeystrokesInRange(startMs, endMs); +api.getAspectRatio(); +api.getActiveFrame(); +api.isExtensionActive(extensionId); +api.getPlaybackState(); +api.getCanvasDimensions(); +api.drawIcon(ctx, "Sparkle", 100, 100, 20, "#2563EB", "regular"); +``` + +### Drawing Icons + +Extensions can draw icons from Recordly's bundled Phosphor icon set directly on a canvas context: + +```js +api.drawIcon( + ctx, + "ArrowClockwise", // icon name from @phosphor-icons/react + 120, // x (center) + 80, // y (center) + 18, // size in px + "#ffffff", // color + "bold", // optional weight: thin | light | regular | bold | fill +); +``` + +This is useful for lightweight overlays and avoids bundling your own icon assets. + +## Settings Panels + +```js +api.registerSettingsPanel({ + id: "my-settings", + label: "My Extension", + icon: "sparkles", + parentSection: "cursor", + fields: [ + { id: "enabled", label: "Enable", type: "toggle", defaultValue: true }, + { id: "size", label: "Size", type: "slider", defaultValue: 1, min: 0.1, max: 3, step: 0.1 }, + { + id: "style", + label: "Style", + type: "select", + defaultValue: "ripple", + options: [{ label: "Ripple", value: "ripple" }, { label: "Pulse", value: "pulse" }], + }, + { id: "color", label: "Color", type: "color", defaultValue: "#2563EB" }, + ], +}); +``` + +Use `parentSection` to nest your panel inside an existing area such as `cursor` or `scene`. + +## Events + +| Event | Permission | Description | +|-------|------------|-------------| +| `playback:timeupdate` | `timeline` | Fires each playback tick | +| `playback:play` | `timeline` | Playback started | +| `playback:pause` | `timeline` | Playback paused | +| `cursor:click` | `cursor` | Cursor click detected | +| `cursor:move` | `cursor` | Cursor move detected | +| `timeline:region-added` | `timeline` | Region added | +| `timeline:region-removed` | `timeline` | Region removed | +| `export:start` | `export` | Export started | +| `export:frame` | `export` | A frame was rendered during export | +| `export:complete` | `export` | Export finished | + +## Frames, Wallpapers, and Cursor Styles + +- `registerFrame()` is the runtime API for device frames. Use a `draw(ctx, width, height)` function when possible for resolution-independent output. +- `registerWallpaper()` contributes scene backgrounds. +- `registerCursorStyle()` contributes cursor image packs. +- All three rely on packaged files relative to the extension root. + +## Lifecycle + +1. Discovery: Recordly scans built-in extensions and the user extensions directory. +2. Activation: `activate(api)` runs and you register hooks, effects, panels, and assets. +3. Runtime: Registered callbacks execute in preview and export according to their phase. +4. Deactivation: `deactivate()` runs and all registrations are automatically disposed. + +## Examples + +- `extension-examples/webadderall.more-wallpapers` shows a user-installable wallpaper bundle that registers 180 packaged wallpapers through `registerWallpaper()`. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 45139414b..000000000 --- a/LICENSE +++ /dev/null @@ -1,8 +0,0 @@ -OpenScreen Free Use License -Copyright (c) 2025 Siddharth Vaddem - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..e7ad2a920 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,252 @@ +## GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +### QUICK SUMMARY: +- You ARE allowed to **use, distribute, and modify this code for all intents and purposes.** +- You ARE allowed to self-host this for your own personal use. +- RECIPROCAL LICENSING: **If you modify this code or use it + to power a website or service (SaaS)**, you MUST make your **entire + source code** (including all edits) p**ublicly available** under this + same AGPLv3 license. +- You CANNOT use the "Recordly" name or branding for your own project. +- If you use Recordly's code or create code derived from Recordly you must attribute Recordly in the user-facing UI and the repo. + +Copyright (C) 2026 webadderall + +Recordly is an independent application that started as a fork of the OpenScreen project, authored by Siddharth Vaddem (2025). + +--- +PART 1: THE AGPLv3 LICENSE +--- +(The following is the full text of the AGPLv3. This license governs the +entirety of this project from 2026 onwards.) + +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright © 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS +0. Definitions. +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and giving a relevant date. +b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". +c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. +d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. +b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. +c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. +d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. +e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or +b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or +c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or +d) Limiting the use for publicity purposes of names of licensors or authors of the material; or +e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or +f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +======================================================================= +PART 2: ORIGINAL MIT ATTRIBUTION (Pursuant to MIT License requirements) +======================================================================= +The original code upon which this project is based is used under the +following terms: + +Copyright (c) 2025 Siddharth Vaddem + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 39872a264..c13d0fcd2 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,409 @@ - - - -

- OpenScreen Logo -

- - -#

OpenScreen

- -

Beautiful screen recordings with zoom animation.
Simple, open-source alternative to Screen.studio.

- - -If you don't want to pay $29/month for Screen Studio but want a much simpler version that does what most people seem to need, making beautiful product demos and walkthroughs, here's a free-to-use app for you. OpenScreen does not offer all Screen Studio features, but covers the basics well! - - -OpenScreen is 100% free for personal and commercial use. Use it, modify it, distribute it. (Just be cool 😁 and give a shoutout if you feel like it !) - - - -**⚠️ DISCLAIMER: This is very much in beta and might be buggy here and there (but hope you have a good experience!).** - -

-

- OpenScreen App Preview - OpenScreen App Preview 2 - OpenScreen App Preview 3 -

-

- -## Core Features -- Record your whole screen or specific apps -- Add manual zooms (customizable depth levels) -- Customize the duration and position of zooms however you please -- Crop video recordings to hide parts -- Choose between wallpapers, solid colors, or gradients for your background -- Motion blur and exponential easing for smoother pan and zoom effects - -**Note:** -- OpenScreen is designed to support both Windows and macOS. We did not use any native APIs specific to one OS, so it should work on both. However, it hasn't been fully tested on Windows yet, if you run into issues, please let me know! -- After you install the app, you'll need to grant it accessibility and screen recording permissions for it to work properly. - -**Contribution:** -Planning to add a contribution guide soon so people can contribute to this repo. Stay tuned! - -## Getting Started -Download the latest installer for your platform from the [GitHub Releases](https://github.com/siddharthvaddem/openscreen/releases) page. - -## Built with -- Electron -- React -- TypeScript -- Vite -- PixiJS -- dnd-timeline - ---- - - -_I'm new to open source, idk what I'm doing lol. If something is wrong please raise an issue 🙏_ - -## License -MIT License: You can use, modify, and distribute this software for any purpose, including commercial use, without restriction. See the LICENSE file for details. +Language: EN | [简中](README.zh-CN.md) + +

+ Recordly Logo +

+ +

+ macOS Windows Linux + AGPL 3.0 license +

+ +### Create polished demo videos in minutes +[Recordly](https://www.recordly.dev) is your **open-source screen recorder** and editor for **walkthroughs, demos, product videos**, and more. +**Accepting PRs.** + +MP4 to GIF export (4) + + +--- +### Backed by the community +CodeRabbit Typemark + +--- + +## What is Recordly? + +Recordly is a desktop app for recording and editing screen captures with motion-driven presentation tools built in. Instead of sending raw footage to a motion designer just to add zooms, cursor polish, or a styled background, Recordly handles that workflow in one place for free. + +Recordly runs on: + +- **macOS** 14.0+ +- **Windows** 10 Build 19041+ +- **Linux** on modern distros + +Platform notes: + +- **macOS** uses native ScreenCaptureKit-based capture helpers. +- **Windows** uses a native Windows Graphics Capture (WGC) helper on supported builds, with native WASAPI audio support. +- **Linux** records through Electron capture APIs. X11 uses a screen/window source list; Wayland opens the system picker when recording starts. Cursor hiding is not supported on Linux today. + +--- + +# Core Features + +## Auto-zooms, cursor polish, and styled frames +Recordly can automatically emphasize activity with zoom suggestions, smooth cursor movement, add motion effects, and place the final composition inside a styled frame with wallpapers, colors, gradients, blur, padding, and shadows. + +

+ Recordly cursor and zoom demo video +

+ +## Dynamic webcam bubble overlays +Add webcam footage as an overlay bubble, position it with presets or custom coordinates, mirror it, control shadow and roundness, and optionally make it react to zoom so it stays visually balanced during motion. + +

+ Recordly webcam overlay demo video +

+ +## Timeline editing built for demos +Use drag-and-drop timeline tools for zooms, trims, speed regions, annotations, extra audio regions, and crop-aware edits. Save and reopen work as `.recordly` project files. + +

+ timeline editor +

+ +## Extensions & Marketplace + +Recordly has a community-driven extension system. Anyone can build and publish extensions that add new capabilities to Recordly — cursor click sounds, device frames, browser mockups, wallpapers, render hooks, settings panels, and more. + +Browse and install community extensions from the [Recordly Marketplace](https://marketplace.recordly.dev/extensions). + +--- + +## All Features + +### Recording + +- Record an entire display or a single app window +- Jump directly from recording into the editor +- Capture microphone audio and system audio +- Use native capture backends where supported +- Resume editing from saved `.recordly` project files +- Open existing recordings or existing project files from the app + +### Timeline and Editing + +- Drag-and-drop timeline editing +- Trim unwanted sections +- Add manual zoom regions +- Use automatic zoom suggestions based on cursor activity +- Add speed-up and slow-down regions +- Add text, image, and figure annotations +- Add extra audio regions on the timeline +- Crop the recorded frame +- Save and reopen projects with editor state preserved + +### Cursor Controls + +- Show or hide the rendered cursor overlay +- Cursor size adjustment +- Cursor smoothing +- Cursor motion blur +- Cursor click bounce +- Cursor sway +- Cursor loop mode for cleaner looping exports +- macOS-style cursor assets for the rendered overlay + +### Webcam Overlay + +- Enable or disable webcam overlay footage +- Upload, replace, or remove webcam footage +- Mirror webcam footage +- Size control +- Preset positions and custom X/Y placement +- Margin control +- Roundness control +- Shadow control +- Optional zoom-reactive webcam scaling + +### Frame Styling and Backgrounds + +- Built-in wallpapers +- Runtime wallpaper discovery from the wallpapers directory +- Custom uploaded backgrounds +- Solid color backgrounds +- Gradient backgrounds +- Frame padding +- Rounded corners +- Background blur +- Drop shadows +- Aspect ratio presets for the final frame + +### Export + +- MP4 export +- GIF export +- Export quality selection +- GIF frame-rate selection +- GIF loop toggle +- GIF size presets +- Aspect ratio and output dimension controls +- Reveal exported files in the system file manager + +### Workflow and Usability + +- Customizable keyboard shortcuts +- In-app shortcut reference +- Feedback and issue links from the editor +- Project persistence for editor preferences +- Faster preview recovery after export +--- + +# Screenshots + +

+ Recordly recording interface screenshot +

+ +

+ Recordly editor screenshot +

+ +

+ Recordly timeline screenshot +

+ +--- + +# Installation + +## Download a build + +Prebuilt releases are available at: + +https://github.com/webadderallorg/Recordly/releases + +--- + +## Arch Linux / Manjaro (yay) + +Install from the AUR ([recordly-bin](https://aur.archlinux.org/packages/recordly-bin)): + +```bash +yay -S recordly-bin +``` + +PKGBUILD, desktop entry, release sync, and optional **local-from-source** packaging live in **[recordly-aur](https://github.com/firtoz/recordly-aur)** so this repository stays free of Arch release chores. For maintainer contact and how the package is updated, see that repo or the AUR package page. + +--- + +## Build from source + +### Prerequisites + +**macOS:** Xcode Command Line Tools (`xcode-select --install`). + +**Linux (Ubuntu/Debian):** + +```bash +sudo apt install build-essential cmake libx11-dev libxtst-dev libxrandr-dev libxt-dev +``` + +**Windows:** Visual Studio 2022 (or Build Tools) with the C++ workload and CMake. + +### Steps + +```bash +git clone https://github.com/webadderallorg/Recordly.git recordly +cd recordly +npm install +npm run dev +``` + +For packaged builds: + +```bash +npm run build +``` + +Target-specific build commands are also available: + +- `npm run build:mac` +- `npm run build:win` +- `npm run build:linux` + +--- + +## macOS: "App cannot be opened" + +Locally built apps may be quarantined by macOS. + +Remove the quarantine flag with: + +```bash +xattr -rd com.apple.quarantine /Applications/Recordly.app +``` + +--- + +# System Requirements + +| Platform | Minimum version | Notes | +|---|---|---| +| **macOS** | macOS 14.0 (Sonoma) | Required for ScreenCaptureKit audio and microphone capture. | +| **Windows** | Windows 10 20H1 (Build 19041, May 2020) | Required for the native Windows Graphics Capture (WGC) helper and best cursor-hiding behavior. | +| **Linux** | Any modern distro | Recording works through Electron capture. System audio generally requires PipeWire. | + +> [!IMPORTANT] +> On Windows builds older than 19041, recording can still work through fallback capture, but the real OS cursor may remain visible in recordings. + +--- + +# Usage + +## Record + +1. Launch Recordly. +2. Select a screen or window. +3. Choose microphone and system-audio options. +4. Start recording. +5. Stop recording to open the editor. + +## Edit + +Inside the editor you can: + +- add trims, zooms, speed regions, and annotations +- tune cursor behavior and preview volume +- style the frame with wallpapers, colors, gradients, blur, padding, and corners +- add or adjust webcam overlay footage +- add extra audio regions +- crop the frame and choose an aspect ratio + +Save your work anytime as a `.recordly` project. + +## Export + +Export options include: + +- **MP4** for standard video output +- **GIF** for lightweight sharing and loops + +You can adjust format-specific settings such as quality, GIF frame rate, GIF looping, and output size before export. + +--- + +# Limitations + +### Cursor capture + +Recordly renders a polished cursor overlay on top of the recording. Platform cursor-hiding behavior still depends on OS support. + +**macOS** +- ScreenCaptureKit can exclude the real cursor cleanly. + +**Windows** +- Best results require Windows 10 Build 19041+ and the native capture helper. +- Older builds fall back to Electron capture, so the real cursor may remain visible. + +**Linux** +- Electron desktop capture does not currently support cursor hiding. +- If you also enable the rendered cursor overlay, exports may show both the real cursor and the styled cursor. + +### System audio + +System audio support varies by platform. + +**Windows** +- Native WASAPI support + +**Linux** +- Usually requires PipeWire + +**macOS** +- Requires macOS 14.0+ and the ScreenCaptureKit-based workflow + +--- + +# How It Works + +Recordly combines a platform-specific capture layer with a renderer-driven editor and export pipeline. + +**Capture** +- Electron coordinates recording and application flow +- macOS uses native ScreenCaptureKit helpers +- Windows uses a native Windows Graphics Capture (WGC) helper and native audio helpers where available + +**Editing** +- Timeline regions define zooms, trims, speed changes, audio overlays, and annotations +- Cursor and webcam styling are applied in the editor state + +**Rendering** +- Scene composition is handled by **PixiJS** + +**Export** +- The same scene logic used in preview is rendered into exported MP4 or GIF output + +**Projects** +- `.recordly` files store the source media path plus editor state so work can be reopened later + +--- + +# Contribution + +Contributions are welcome. + +Areas where help is especially useful: + +- Linux capture and cursor behavior +- Export performance and stability +- UI and UX refinement +- Localisation work +- Additional editor tools and workflow polish + +Please keep pull requests focused, test recording/edit/export flows, and avoid unrelated refactors. + +See `CONTRIBUTING.md` for guidelines. + +--- + +# Community + +Bug reports and feature requests: + +https://github.com/webadderallorg/Recordly/issues + +Pull requests are welcome. + +--- + +# Hall of Supporters + +[![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/webadderall) + +- Tom Egan @tomegan on X +- Robin Ebers @robinebers on X +- Tadees +- buildwithfur +- piccinato +- Tobias +- Anonymous Supporter +- Tandava Appadoo +- Digitalfastmind +- Roberto Marcelino +- Tony +- Rajan RK +- Francesco +- Erwan +- Anonymous supporter + +--- + +# License + +Recordly is licensed under the **AGPL 3.0**. + +--- + +# Credits + +## Acknowledgements + +Recordly originally started as a fork of [OpenScreen](https://github.com/siddharthvaddem/openscreen). Over 80% of code has diverged since. +Many features of OpenScreen such as its zoom animations are directly ported from early versions of Recordly. + +Created by +[@webadderall](https://x.com/webadderall) + +--- diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 000000000..b1ca9f556 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,400 @@ +语言: [EN](README.md) | 简中 + +

+ Recordly logo +

+ +

+ macOS Windows Linux + AGPL 3.0 license +

+ +### 无需额外剪辑,也能做出精致的屏幕录制。 +[Recordly](https://www.recordly.dev) 是一款**开源屏幕录制器**和编辑器,适合制作**操作讲解、演示、产品视频**等内容。 +**欢迎提交 PR。** [赞助](https://ko-fi.com/webadderall/goal?g=0) + +https://github.com/user-attachments/assets/9b66c71d-ac97-49ff-a0c9-63ac26edf2e4 + +--- + +## Recordly 是什么? + +Recordly 是一款桌面应用,用于录制并编辑屏幕内容,内置面向演示视频的动态呈现工具。你不需要再把原始素材交给动效设计师去补缩放、光标润色或样式化背景,Recordly 可以在一个地方免费完成整套流程。 + +Recordly 支持: + +- **macOS** 14.0+ +- **Windows** 10 Build 19041+ +- **Linux** 现代发行版 + +平台说明: + +- **macOS** 使用基于 ScreenCaptureKit 的原生捕获辅助程序。 +- **Windows** 在支持的系统版本上使用原生 Windows Graphics Capture(WGC)辅助程序,并支持原生 WASAPI 音频。 +- **Linux** 通过 Electron 捕获 API 录制。目前 Linux 还不支持隐藏真实光标。 + +--- + +# 核心功能 + +## 自动缩放、光标润色与样式化画面 +Recordly 可以根据操作自动强调重点区域,平滑光标运动,添加动态效果,并将最终画面放进带有壁纸、纯色、渐变、模糊、留白和阴影的样式化边框中。 + +

+ Recordly cursor and zoom demo video +

+ +## 动态摄像头气泡叠加 +你可以把摄像头素材作为气泡叠加层加入画面,使用预设位置或自定义坐标摆放,支持镜像、阴影和圆角调节,也可以让它跟随缩放变化,保证动态镜头里整体视觉更协调。 + +

+ Recordly webcam overlay demo video +

+ +## 为演示设计的时间线编辑 +使用拖拽式时间线工具处理缩放、裁剪、变速区域、注释、额外音频区域以及裁切感知编辑,并将工作保存为 `.recordly` 项目文件,之后随时回来继续编辑。 + +

+ timeline editor +

+ +## 扩展与市场 + +Recordly 拥有一个社区驱动的扩展系统。任何人都可以构建和发布扩展来为 Recordly 添加新功能,例如光标点击音效、设备边框、浏览器模拟外壳、壁纸、渲染钩子、设置面板等等。 + +浏览并安装社区扩展:[Recordly 扩展市场](https://marketplace.recordly.dev/extensions)。 + +--- + +## 全部功能 + +### 录制 + +- 录制整个显示器或单个应用窗口 +- 录制完成后直接进入编辑器 +- 录制麦克风音频和系统音频 +- 在支持的平台上使用原生捕获后端 +- 从保存的 `.recordly` 项目文件继续编辑 +- 可在应用中打开已有录像或已有项目文件 + +### 时间线与编辑 + +- 拖拽式时间线编辑 +- 裁掉不需要的片段 +- 添加手动缩放区域 +- 根据光标活动生成自动缩放建议 +- 添加加速和减速区域 +- 添加文本、图片和图形注释 +- 在时间线上添加额外音频区域 +- 裁切录制画面 +- 保存并重新打开项目,保留编辑状态 + +### 光标控制 + +- 显示或隐藏渲染后的光标叠加层 +- 调整光标大小 +- 光标平滑 +- 光标运动模糊 +- 点击弹跳效果 +- 光标摆动效果 +- 光标循环模式,方便导出更自然的循环片段 +- 使用 macOS 风格的渲染光标素材 + +### 摄像头叠加 + +- 启用或禁用摄像头叠加素材 +- 上传、替换或移除摄像头素材 +- 镜像摄像头画面 +- 调整尺寸 +- 使用预设位置或自定义 X/Y 坐标 +- 调整边距 +- 调整圆角程度 +- 调整阴影强度 +- 可选的缩放联动摄像头缩放效果 + +### 画面样式与背景 + +- 内置壁纸 +- 运行时自动发现 wallpapers 目录中的壁纸 +- 上传自定义背景图片 +- 纯色背景 +- 渐变背景 +- 画面留白 +- 圆角 +- 背景模糊 +- 投影阴影 +- 最终画面的宽高比预设 + +### 导出 + +- MP4 导出 +- GIF 导出 +- 导出质量选择 +- GIF 帧率选择 +- GIF 循环开关 +- GIF 尺寸预设 +- 宽高比和输出尺寸控制 +- 在系统文件管理器中定位导出文件 + +### 工作流与易用性 + +- 可自定义键盘快捷键 +- 应用内快捷键说明 +- 在编辑器中直接打开反馈和问题链接 +- 编辑器偏好设置持久化 +- 导出后更快恢复预览 + +--- + +# 截图 + +

+ Recordly recording interface screenshot +

+ +

+ Recordly editor screenshot +

+ +

+ Recordly timeline screenshot +

+ +--- + +# 安装 + +## 下载构建版本 + +预构建发布版本请见: + +https://github.com/webadderallorg/Recordly/releases + +--- + +## Arch Linux / Manjaro(yay) + +可通过 AUR 安装([recordly-bin](https://aur.archlinux.org/packages/recordly-bin)): + +```bash +yay -S recordly-bin +``` + +PKGBUILD、桌面入口、发布同步,以及可选的**本地源码打包**都维护在 **[recordly-aur](https://github.com/firtoz/recordly-aur)** 中,因此这个仓库本身不需要承担 Arch 发布维护工作。关于维护者联系方式和软件包更新方式,请查看该仓库或 AUR 软件包页面。 + +--- + +## 从源码构建 + +### 前置依赖 + +**macOS:** 安装 Xcode Command Line Tools(`xcode-select --install`)。 + +**Linux(Ubuntu / Debian):** + +```bash +sudo apt install build-essential cmake libx11-dev libxtst-dev libxrandr-dev libxt-dev +``` + +**Windows:** 安装 Visual Studio 2022(或 Build Tools),并勾选 C++ 工作负载和 CMake。 + +### 步骤 + +```bash +git clone https://github.com/webadderallorg/Recordly.git recordly +cd recordly +npm install +npm run dev +``` + +如果需要打包构建: + +```bash +npm run build +``` + +也可以使用平台专用构建命令: + +- `npm run build:mac` +- `npm run build:win` +- `npm run build:linux` + +--- + +## macOS:“App cannot be opened” + +本地构建的应用可能会被 macOS 隔离。 + +可以用以下命令移除隔离标记: + +```bash +xattr -rd com.apple.quarantine /Applications/Recordly.app +``` + +--- + +# 系统要求 + +| 平台 | 最低版本 | 说明 | +|---|---|---| +| **macOS** | macOS 14.0 (Sonoma) | 使用 ScreenCaptureKit 捕获音频和麦克风所必需。 | +| **Windows** | Windows 10 20H1(Build 19041,2020 年 5 月) | 原生 Windows Graphics Capture(WGC)辅助程序及最佳光标隐藏行为所必需。 | +| **Linux** | 任意现代发行版 | 通过 Electron 捕获录制。系统音频通常需要 PipeWire。 | + +> [!IMPORTANT] +> 在 Windows 19041 之前的版本上,录制仍可能通过回退捕获方式工作,但真实系统光标可能仍会出现在视频中。 + +--- + +# 使用方法 + +## 录制 + +1. 启动 Recordly。 +2. 选择屏幕或窗口。 +3. 选择麦克风和系统音频选项。 +4. 开始录制。 +5. 停止录制后进入编辑器。 + +## 编辑 + +在编辑器中,你可以: + +- 添加裁剪、缩放、变速区域和注释 +- 调整光标行为和预览音量 +- 使用壁纸、纯色、渐变、模糊、留白和圆角来美化画面 +- 添加或调整摄像头叠加素材 +- 添加额外音频区域 +- 裁切画面并选择宽高比 + +你可以随时将工作保存为 `.recordly` 项目。 + +## 导出 + +支持以下导出格式: + +- **MP4**,适合常规视频输出 +- **GIF**,适合轻量分享和循环片段 + +你可以在导出前调整格式相关设置,例如质量、GIF 帧率、GIF 循环方式和输出尺寸。 + +--- + +# 限制 + +### 光标捕获 + +Recordly 会在录制画面上渲染一个经过美化的光标叠加层,但真实系统光标是否能被隐藏仍取决于平台能力。 + +**macOS** +- ScreenCaptureKit 可以较干净地排除真实光标。 + +**Windows** +- 最佳效果需要 Windows 10 Build 19041+ 和原生捕获辅助程序。 +- 较旧版本会回退到 Electron 捕获,因此真实光标可能仍会显示。 + +**Linux** +- Electron 桌面捕获目前不支持隐藏真实光标。 +- 如果同时启用渲染光标叠加,导出中可能会同时看到真实光标和样式化光标。 + +### 系统音频 + +系统音频支持因平台而异。 + +**Windows** +- 原生 WASAPI 支持 + +**Linux** +- 通常需要 PipeWire + +**macOS** +- 需要 macOS 14.0+ 和基于 ScreenCaptureKit 的工作流 + +--- + +# 工作原理 + +Recordly 将平台相关的捕获层与基于渲染器的编辑、导出流程结合在一起。 + +**捕获** +- Electron 负责录制流程和应用级控制 +- macOS 使用原生 ScreenCaptureKit 辅助程序 +- Windows 在可用时使用原生 Windows Graphics Capture(WGC)辅助程序和原生音频辅助程序 + +**编辑** +- 时间线区域定义缩放、裁剪、变速、音频叠加和注释 +- 光标和摄像头样式都保存在编辑器状态中 + +**渲染** +- 场景合成由 **PixiJS** 负责 + +**导出** +- 预览使用的同一套场景逻辑会被用于导出 MP4 或 GIF + +**项目** +- `.recordly` 文件会保存源媒体路径和编辑器状态,方便后续继续编辑 + +--- + +# 贡献 + +欢迎贡献。 + +特别需要帮助的方向包括: + +- Linux 录制与光标行为改进 +- 导出性能与稳定性优化 +- UI 和 UX 打磨 +- 本地化工作 +- 更多编辑工具与工作流优化 + +请尽量让 Pull Request 保持聚焦,测试录制、编辑、导出流程,并避免无关重构。 + +请参阅 `CONTRIBUTING.md` 了解具体指南。 + +--- + +# 社区 + +问题反馈和功能建议: + +https://github.com/webadderallorg/Recordly/issues + +欢迎提交 Pull Request。 + +--- + +# 支持者名单 + +[![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/webadderall) + +- Tadees +- buildwithfur +- Tobias +- Anonymous Supporter +- Tandava Appadoo +- Digitalfastmind +- Roberto Marcelino +- Rajan RK +- Francesco +- Erwan +- Anonymous supporter + +--- + +# 许可证 + +Recordly 基于 **AGPL 3.0** 发布。 + +--- + +# 致谢 + +## 鸣谢 + +Recordly 最初是从 [OpenScreen](https://github.com/siddharthvaddem/openscreen) 分叉而来,之后已逐步演变为一个不同的项目。 + +创建者 +[@webadderall](https://x.com/webadderall) + +--- diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 000000000..4ec8e22ad --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,103 @@ +# Releasing Recordly + +This repository now uses `electron-builder` + `electron-updater` for macOS, Windows, and Linux auto-updates. + +For this Electron app, that is the right path instead of wiring Sparkle.framework directly. On macOS, `electron-updater` handles the release metadata and update flow that Sparkle would otherwise cover in a native app, while still sharing the same GitHub Releases pipeline used by Windows and Linux. + +## What the release workflow does + +When you publish a GitHub release tagged like `v1.2.3`, `.github/workflows/release.yml` will: + +- validate that `package.json` is also `1.2.3` +- build signed macOS x64 and arm64 artifacts +- notarize the macOS builds +- merge the dual-architecture `latest-mac.yml` metadata into one release asset +- build and sign the Windows NSIS installer +- build the Linux AppImage +- publish installer artifacts and auto-update metadata files to the GitHub release +- dispatch the Homebrew tap workflow after the release assets are available + +The packaged app then checks GitHub Releases for: + +- `latest-mac.yml` +- `latest.yml` +- `latest-linux.yml` + +## Required GitHub secrets + +### macOS signing and notarization + +Set these repository secrets: + +- `APPLE_SIGNING_CERTIFICATE_P12_BASE64` +- `APPLE_SIGNING_CERTIFICATE_PASSWORD` +- `APPLE_ID` +- `APPLE_APP_SPECIFIC_PASSWORD` +- `APPLE_TEAM_ID` + +`APPLE_SIGNING_CERTIFICATE_P12_BASE64` must be a base64-encoded `.p12` export of a `Developer ID Application` certificate. + +If the certificate you currently have is only `Apple Development`, that is not enough for public notarized releases and auto-update distribution. You need `Developer ID Application`. + +To export and encode the certificate: + +```bash +security export -k ~/Library/Keychains/login.keychain-db -t identities -f pkcs12 -P "YOUR_P12_PASSWORD" -o recordly-mac-signing.p12 +base64 < recordly-mac-signing.p12 | pbcopy +``` + +Paste the copied base64 into `APPLE_SIGNING_CERTIFICATE_P12_BASE64` and the export password into `APPLE_SIGNING_CERTIFICATE_PASSWORD`. + +### Windows signing + +Set these repository secrets: + +- `WINDOWS_SIGNING_CERTIFICATE_P12_BASE64` +- `WINDOWS_SIGNING_CERTIFICATE_PASSWORD` + +These should point to an Authenticode code-signing certificate exported as `.p12` and then base64-encoded. + +### Homebrew tap automation + +Set this repository secret if you want the cask PR to open automatically: + +- `HOMEBREW_TAP_TOKEN` + +Optional repository variables: + +- `HOMEBREW_TAP_REPO` +- `HOMEBREW_TAP_AUTO_MERGE` + +## Release flow + +1. Bump `package.json` to the version you want to ship. +2. Commit and push that version. +3. Create a Git tag in the form `vX.Y.Z`. +4. Create and publish a GitHub release for that tag. Prefer the helper so custom notes are prepended while GitHub still generates the contributor section: + +```bash +npm run release:create -- --tag v1.2.3 --title "v1.2.3" --notes-file ./release-notes.md +``` + +For prereleases: + +```bash +npm run release:create -- --tag v1.2.0-beta.2 --title "v1.2.0 beta-2" --prerelease --notes-file ./release-notes.md +``` + +This uses `gh release create --generate-notes`, which keeps GitHub's generated change summary and contributor list instead of replacing it with a fully manual release body. + +5. The `Publish Release` workflow builds, signs, notarizes, uploads, and publishes update metadata. + +That is the normal path if you want “click new release and let CI do the rest.” + +## Rebuilding an existing release + +If you need to rerun publishing for an existing tag, use the manual dispatch for `.github/workflows/release.yml` and provide the existing tag. + +## Notes + +- macOS auto-updates require the `zip` target in addition to `dmg`, because `latest-mac.yml` is generated from the zipped build. +- macOS arm64 and x64 builds both publish updater zips, and the release workflow merges them into one `latest-mac.yml` so `electron-updater` can choose the correct architecture automatically. +- The release workflow publishes versioned artifact names so the generated update metadata matches the uploaded files. +- `build.yml` is intentionally forced to `--publish never` so ad hoc CI builds do not accidentally upload to a draft release. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..6d1558837 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,49 @@ +# Third-party notices + +## Voom + +Recordly Share includes code adapted from the Voom project: + +- Project: [Voom](https://github.com/aritropaul/voom) +- Adapted component: `services/recordly-share/worker` +- License: MIT +- Copyright © 2026 Aritro Paul +- Upstream reference: `c1c0350ed610b0c9bc4a307fcc6a01aab0f926f9` + +Recordly changes include product branding, desktop publishing integration, +authenticated commenting, local-development configuration, and viewer styling. + +The Voom software is provided under the following license: + +> MIT License +> +> Copyright (c) 2026 Aritro Paul +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. + +## Solar Icons + +Solar Icon Set by 480 Design, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). +Source: https://github.com/480-Design/Solar-Icon-Set. Icons are rendered using the MIT-licensed `@solar-icons/react` package. The muted microphone adds a diagonal stroke. + +## DM Sans + +Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts). + +DM Sans is licensed under the SIL Open Font License, Version 1.1. The unmodified regular and italic variable fonts are sourced from [Google Fonts](https://github.com/google/fonts/tree/main/ofl/dmsans). The full license is included in [src/assets/fonts/dm-sans/OFL.txt](src/assets/fonts/dm-sans/OFL.txt). diff --git a/TRANSLATION_GUIDE.md b/TRANSLATION_GUIDE.md new file mode 100644 index 000000000..b0b599d72 --- /dev/null +++ b/TRANSLATION_GUIDE.md @@ -0,0 +1,65 @@ +# Translation Guide + +This project uses a namespace-based i18n setup so contributors can localize safely without changing app logic. + +## Locale Files + +All locale files live under: + +- `src/i18n/locales/en/` +- `src/i18n/locales/es/` +- `src/i18n/locales/zh-CN/` + +Each locale has the same namespace files: + +- `common.json` +- `launch.json` +- `editor.json` +- `timeline.json` +- `settings.json` +- `dialogs.json` +- `shortcuts.json` + +English (`en`) is the source of truth for key structure. + +## Key Rules + +- Keep the same key paths across locales. +- Do not rename existing keys unless coordinated with code changes. +- Add new keys to `en` first, then mirror into all other locales. +- Prefer descriptive, stable keys. Example: `app.editorTitle`. +- Interpolation is supported with `{{name}}` style placeholders. + +## How Translation Is Read + +- Keys with a namespace prefix like `settings.export.title` use that namespace. +- Keys without a namespace default to `common`. +- Missing translations fall back to English, then to the provided fallback string, then to the key. + +## Validate Locale Structure + +Run: + +```bash +npm run i18n:check +``` + +This checks for: + +- Missing namespace files +- Missing keys compared to `en` +- Extra keys not present in `en` + +## Contributor Workflow + +1. Pull latest `main`. +2. Update `en/.json` with new keys if needed. +3. Add matching keys to other locale files. +4. Run `npm run i18n:check`. +5. Run app locally (`npm run dev`) and spot-check UI text. +6. Open PR with a short summary of changed namespaces. + +## Scope Notes + +Current framework is app-wide and ready for full localization rollout. +Not every UI string is migrated yet. Migration should be done incrementally by namespace to keep PRs reviewable and low-risk. diff --git a/announcements.json b/announcements.json new file mode 100644 index 000000000..f7e39713e --- /dev/null +++ b/announcements.json @@ -0,0 +1,6 @@ +{ + "settings": { + "aspectRatio": "4:3" + }, + "announcements": [] +} diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..5ae2a194e --- /dev/null +++ b/biome.json @@ -0,0 +1,194 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!!**/dist", + "!!**/dist-electron", + "!!**/dist-ssr", + "!!**/release", + "!!**/.tmp", + "!!**/electron/native/**/build" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "formatWithErrors": true, + "lineEnding": "lf", + "lineWidth": 100, + "attributePosition": "auto", + "indentWidth": 4 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noAdjacentSpacesInRegex": "error", + "noBannedTypes": "error", + "noExtraBooleanCast": "error", + "noUselessCatch": "error", + "noUselessEscapeInRegex": "error", + "noUselessThisAlias": "error", + "noUselessTypeConstraint": "error" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInnerDeclarations": "error", + "noInvalidConstructorSuper": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedLabels": "error", + "noUnusedVariables": "error", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "useIsNan": "error", + "useValidForDirection": "error", + "useValidTypeof": "error", + "useYield": "error" + }, + "style": { + "noNamespace": "error", + "useArrayLiterals": "error", + "useAsConstAssertion": "error", + "useComponentExportOnlyModules": "off" + }, + "suspicious": { + "noAssignInExpressions": "error", + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCompareNegZero": "error", + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateElseIf": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "error", + "noExplicitAny": "error", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noIrregularWhitespace": "error", + "noMisleadingCharacterClass": "error", + "noMisleadingInstantiator": "error", + "noNonNullAssertedOptionalChain": "error", + "noPrototypeBuiltins": "error", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noSparseArray": "error", + "noTsIgnore": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "noUselessRegexBackrefs": "error", + "noWith": "error", + "useGetterReturn": "error" + } + } + }, + "javascript": { "formatter": { "quoteStyle": "double" } }, + "css": { "parser": { "cssModules": true, "tailwindDirectives": true } }, + "overrides": [ + { + "includes": ["services/supabase/functions/**/*.ts"], + "javascript": { "globals": ["Deno"] } + }, + { + "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], + "linter": { + "rules": { + "complexity": { "noArguments": "error" }, + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidBuiltinInstantiation": "off", + "noInvalidConstructorSuper": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, + "style": { "useConst": "error" }, + "suspicious": { + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "noVar": "error", + "useGetterReturn": "off" + } + } + } + }, + { + "includes": ["electron/electron-env.d.ts"], + "linter": { + "rules": { + "style": { + "noNamespace": "off" + }, + "suspicious": { + "noExplicitAny": "off" + } + } + } + }, + { + "includes": ["**/*.test.ts", "**/*.test.tsx"], + "linter": { + "rules": { + "correctness": { + "noEmptyPattern": "off" + }, + "suspicious": { + "noEmptyBlockStatements": "off" + } + } + } + }, + { + "includes": [ + "src/components/video-editor/audio.test.ts", + "src/lib/exporter/frameRenderer.test.ts", + "src/lib/exporter/localMediaSource.test.ts", + "src/lib/exporter/modernFrameRenderer.test.ts" + ], + "linter": { + "rules": { + "correctness": { + "noUnusedVariables": "off" + }, + "suspicious": { + "noExplicitAny": "off" + } + } + } + } + ], + "assist": { + "enabled": true, + "actions": { "source": { "organizeImports": "on" } } + } +} diff --git a/branding/source-assets/1024-mac.png b/branding/source-assets/1024-mac.png new file mode 100644 index 000000000..7602ca8eb Binary files /dev/null and b/branding/source-assets/1024-mac.png differ diff --git a/branding/source-assets/128-mac.png b/branding/source-assets/128-mac.png new file mode 100644 index 000000000..94ad18d1b Binary files /dev/null and b/branding/source-assets/128-mac.png differ diff --git a/branding/source-assets/16-mac.png b/branding/source-assets/16-mac.png new file mode 100644 index 000000000..90118bdf0 Binary files /dev/null and b/branding/source-assets/16-mac.png differ diff --git a/branding/source-assets/256-mac.png b/branding/source-assets/256-mac.png new file mode 100644 index 000000000..fac38ff6e Binary files /dev/null and b/branding/source-assets/256-mac.png differ diff --git a/branding/source-assets/32-mac.png b/branding/source-assets/32-mac.png new file mode 100644 index 000000000..2e21b5e13 Binary files /dev/null and b/branding/source-assets/32-mac.png differ diff --git a/branding/source-assets/512-mac.png b/branding/source-assets/512-mac.png new file mode 100644 index 000000000..3a0647e1a Binary files /dev/null and b/branding/source-assets/512-mac.png differ diff --git a/branding/source-assets/64-mac.png b/branding/source-assets/64-mac.png new file mode 100644 index 000000000..15ec012b2 Binary files /dev/null and b/branding/source-assets/64-mac.png differ diff --git a/branding/source-assets/Recordly.svg b/branding/source-assets/Recordly.svg new file mode 100644 index 000000000..a936f4979 --- /dev/null +++ b/branding/source-assets/Recordly.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/branding/source-assets/recordlygeneric.svg b/branding/source-assets/recordlygeneric.svg new file mode 100644 index 000000000..57910c64a --- /dev/null +++ b/branding/source-assets/recordlygeneric.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/branding/source-assets/recordlymac.png b/branding/source-assets/recordlymac.png new file mode 100644 index 000000000..6e4baf246 Binary files /dev/null and b/branding/source-assets/recordlymac.png differ diff --git a/build/entitlements.mac.inherit.plist b/build/entitlements.mac.inherit.plist new file mode 100644 index 000000000..a0414705f --- /dev/null +++ b/build/entitlements.mac.inherit.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.camera + + com.apple.security.device.audio-input + + com.apple.security.inherit + + + \ No newline at end of file diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist new file mode 100644 index 000000000..98652b7fb --- /dev/null +++ b/build/entitlements.mac.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.camera + + com.apple.security.device.audio-input + + + \ No newline at end of file diff --git a/components.json b/components.json deleted file mode 100644 index b4f439552..000000000 --- a/components.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.cjs", - "css": "src/index.css", - "baseColor": "stone", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "registries": {} -} diff --git a/dist-electron/main.js b/dist-electron/main.js deleted file mode 100644 index 0634f7dc0..000000000 --- a/dist-electron/main.js +++ /dev/null @@ -1,342 +0,0 @@ -import { BrowserWindow as E, screen as O, ipcMain as c, desktopCapturer as W, shell as V, app as d, nativeImage as L, Tray as U, Menu as A } from "electron"; -import { fileURLToPath as S } from "node:url"; -import t from "node:path"; -import p from "node:fs/promises"; -import { uIOhook as w } from "uiohook-napi"; -const P = t.dirname(S(import.meta.url)), C = t.join(P, ".."), y = process.env.VITE_DEV_SERVER_URL, x = t.join(C, "dist"); -function N() { - const e = new E({ - width: 250, - height: 80, - minWidth: 250, - maxWidth: 250, - minHeight: 80, - maxHeight: 80, - frame: !1, - transparent: !0, - resizable: !1, - alwaysOnTop: !0, - skipTaskbar: !0, - hasShadow: !1, - webPreferences: { - preload: t.join(P, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - backgroundThrottling: !1 - } - }); - return e.webContents.on("did-finish-load", () => { - e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }), y ? e.loadURL(y + "?windowType=hud-overlay") : e.loadFile(t.join(x, "index.html"), { - query: { windowType: "hud-overlay" } - }), e; -} -function H() { - const e = new E({ - width: 1200, - height: 800, - minWidth: 800, - minHeight: 600, - frame: !0, - transparent: !1, - resizable: !0, - alwaysOnTop: !1, - skipTaskbar: !1, - title: "", - webPreferences: { - preload: t.join(P, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - webSecurity: !1 - } - }); - return e.maximize(), e.webContents.on("did-finish-load", () => { - e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }), y ? e.loadURL(y + "?windowType=editor") : e.loadFile(t.join(x, "index.html"), { - query: { windowType: "editor" } - }), e; -} -function z() { - const { width: e, height: n } = O.getPrimaryDisplay().workAreaSize, i = new E({ - width: 620, - height: 420, - minHeight: 350, - maxHeight: 500, - x: Math.round((e - 620) / 2), - y: Math.round((n - 420) / 2), - frame: !1, - resizable: !1, - alwaysOnTop: !0, - transparent: !0, - backgroundColor: "#00000000", - webPreferences: { - preload: t.join(P, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0 - } - }); - return y ? i.loadURL(y + "?windowType=source-selector") : i.loadFile(t.join(x, "index.html"), { - query: { windowType: "source-selector" } - }), i; -} -let u = !1, b = !1, m = 0, f = []; -function q() { - if (u) - return { success: !1, message: "Already tracking" }; - if (u = !0, m = performance.now(), f = [], b) - return { success: !0, message: "Mouse tracking resumed", startTime: m }; - $(); - try { - return w.start(), b = !0, { success: !0, message: "Mouse tracking started", startTime: m }; - } catch (e) { - return console.error("Failed to start mouse tracking:", e), u = !1, { success: !1, message: "Failed to start hook", error: e }; - } -} -function B() { - if (!u) - return { success: !1, message: "Not currently tracking" }; - u = !1; - const e = performance.now() - m; - return { - success: !0, - message: "Mouse tracking stopped", - data: { - startTime: m, - events: f, - duration: e - } - }; -} -function $() { - w.on("mousemove", (e) => { - if (u) { - const i = { - type: "move", - timestamp: performance.now() - m, - x: e.x, - y: e.y - }; - f.push(i); - } - }), w.on("mousedown", (e) => { - if (u) { - const i = { - type: "down", - timestamp: performance.now() - m, - x: e.x, - y: e.y, - button: e.button, - clicks: e.clicks - }; - f.push(i); - } - }), w.on("mouseup", (e) => { - if (u) { - const i = { - type: "up", - timestamp: performance.now() - m, - x: e.x, - y: e.y, - button: e.button - }; - f.push(i); - } - }), w.on("click", (e) => { - if (u) { - const i = { - type: "click", - timestamp: performance.now() - m, - x: e.x, - y: e.y, - button: e.button, - clicks: e.clicks - }; - f.push(i); - } - }); -} -function G() { - return [...f]; -} -function I() { - if (b) - try { - w.stop(), b = !1, u = !1, f = []; - } catch (e) { - console.error("Error cleaning up mouse tracking:", e); - } -} -let _ = null; -function J(e, n, i, v, T) { - c.handle("get-sources", async (o, a) => (await W.getSources(a)).map((r) => ({ - id: r.id, - name: r.name, - display_id: r.display_id, - thumbnail: r.thumbnail ? r.thumbnail.toDataURL() : null, - appIcon: r.appIcon ? r.appIcon.toDataURL() : null - }))), c.handle("select-source", (o, a) => { - _ = a; - const s = v(); - return s && s.close(), _; - }), c.handle("get-selected-source", () => _), c.handle("open-source-selector", () => { - const o = v(); - if (o) { - o.focus(); - return; - } - n(); - }), c.handle("switch-to-editor", () => { - const o = i(); - o && o.close(), e(); - }), c.handle("start-mouse-tracking", () => q()), c.handle("stop-mouse-tracking", () => B()), c.handle("store-recorded-video", async (o, a, s) => { - try { - const r = t.join(h, s); - return await p.writeFile(r, Buffer.from(a)), { - success: !0, - path: r, - message: "Video stored successfully" - }; - } catch (r) { - return console.error("Failed to store video:", r), { - success: !1, - message: "Failed to store video", - error: String(r) - }; - } - }), c.handle("store-mouse-tracking-data", async (o, a) => { - try { - const s = G(); - if (s.length === 0) - return { success: !1, message: "No tracking data to save" }; - const r = t.join(h, a); - return await p.writeFile(r, JSON.stringify(s, null, 2), "utf-8"), { - success: !0, - path: r, - eventCount: s.length, - message: "Mouse tracking data stored successfully" - }; - } catch (s) { - return console.error("Failed to store mouse tracking data:", s), { - success: !1, - message: "Failed to store mouse tracking data", - error: String(s) - }; - } - }), c.handle("get-recorded-video-path", async () => { - try { - const a = (await p.readdir(h)).filter((R) => R.endsWith(".webm")); - if (a.length === 0) - return { success: !1, message: "No recorded video found" }; - const s = a.sort().reverse()[0]; - return { success: !0, path: t.join(h, s) }; - } catch (o) { - return console.error("Failed to get video path:", o), { success: !1, message: "Failed to get video path", error: String(o) }; - } - }), c.handle("set-recording-state", (o, a) => { - T && T(a, (_ || { name: "Screen" }).name); - }), c.handle("open-external-url", async (o, a) => { - try { - return await V.openExternal(a), { success: !0 }; - } catch (s) { - return console.error("Failed to open URL:", s), { success: !1, error: String(s) }; - } - }), c.handle("get-asset-base-path", () => { - try { - return d.isPackaged ? t.join(process.resourcesPath, "assets") : t.join(d.getAppPath(), "public", "assets"); - } catch (o) { - return console.error("Failed to resolve asset base path:", o), null; - } - }), c.handle("save-exported-video", async (o, a, s) => { - try { - const r = d.getPath("downloads"), R = t.join(r, s); - return await p.writeFile(R, Buffer.from(a)), { - success: !0, - path: R, - message: "Video exported successfully" - }; - } catch (r) { - return console.error("Failed to save exported video:", r), { - success: !1, - message: "Failed to save exported video", - error: String(r) - }; - } - }); -} -const K = t.dirname(S(import.meta.url)), h = t.join(d.getPath("userData"), "recordings"); -async function Q() { - try { - const e = await p.readdir(h), n = Date.now(), i = 1 * 24 * 60 * 60 * 1e3; - for (const v of e) { - const T = t.join(h, v), o = await p.stat(T); - n - o.mtimeMs > i && (await p.unlink(T), console.log(`Deleted old recording: ${v}`)); - } - } catch (e) { - console.error("Failed to cleanup old recordings:", e); - } -} -async function X() { - try { - await p.mkdir(h, { recursive: !0 }), console.log("Recordings directory ready:", h); - } catch (e) { - console.error("Failed to create recordings directory:", e); - } -} -process.env.APP_ROOT = t.join(K, ".."); -const Y = process.env.VITE_DEV_SERVER_URL, ie = t.join(process.env.APP_ROOT, "dist-electron"), j = t.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = Y ? t.join(process.env.APP_ROOT, "public") : j; -let l = null, k = null, g = null, D = ""; -function F() { - l = N(); -} -function Z() { - const e = t.join(process.env.VITE_PUBLIC || j, "rec-button.png"); - let n = L.createFromPath(e); - n = n.resize({ width: 24, height: 24, quality: "best" }), g = new U(n), M(); -} -function M() { - if (!g) return; - const e = [ - { - label: "Stop Recording", - click: () => { - l && !l.isDestroyed() && l.webContents.send("stop-recording-from-tray"); - } - } - ], n = A.buildFromTemplate(e); - g.setContextMenu(n), g.setToolTip(`Recording: ${D}`); -} -function ee() { - l && (l.close(), l = null), l = H(); -} -function te() { - return k = z(), k.on("closed", () => { - k = null; - }), k; -} -d.on("window-all-closed", () => { - process.platform !== "darwin" && (I(), d.quit(), l = null); -}); -d.on("activate", () => { - E.getAllWindows().length === 0 && F(); -}); -d.on("before-quit", async (e) => { - e.preventDefault(), I(), await Q(), d.exit(0); -}); -d.whenReady().then(async () => { - await X(), J( - ee, - te, - () => l, - () => k, - (e, n) => { - D = n, e ? (g || Z(), M(), l && l.minimize()) : (g && (g.destroy(), g = null), l && l.restore()); - } - ), F(); -}); -export { - ie as MAIN_DIST, - h as RECORDINGS_DIR, - j as RENDERER_DIST, - Y as VITE_DEV_SERVER_URL -}; diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs deleted file mode 100644 index 92d0779b1..000000000 --- a/dist-electron/preload.mjs +++ /dev/null @@ -1 +0,0 @@ -"use strict";const e=require("electron");e.contextBridge.exposeInMainWorld("electronAPI",{getAssetBasePath:async()=>await e.ipcRenderer.invoke("get-asset-base-path"),getSources:async r=>await e.ipcRenderer.invoke("get-sources",r),switchToEditor:()=>e.ipcRenderer.invoke("switch-to-editor"),openSourceSelector:()=>e.ipcRenderer.invoke("open-source-selector"),selectSource:r=>e.ipcRenderer.invoke("select-source",r),getSelectedSource:()=>e.ipcRenderer.invoke("get-selected-source"),startMouseTracking:()=>e.ipcRenderer.invoke("start-mouse-tracking"),stopMouseTracking:()=>e.ipcRenderer.invoke("stop-mouse-tracking"),storeRecordedVideo:(r,t)=>e.ipcRenderer.invoke("store-recorded-video",r,t),storeMouseTrackingData:r=>e.ipcRenderer.invoke("store-mouse-tracking-data",r),getRecordedVideoPath:()=>e.ipcRenderer.invoke("get-recorded-video-path"),setRecordingState:r=>e.ipcRenderer.invoke("set-recording-state",r),onStopRecordingFromTray:r=>{const t=()=>r();return e.ipcRenderer.on("stop-recording-from-tray",t),()=>e.ipcRenderer.removeListener("stop-recording-from-tray",t)},openExternalUrl:r=>e.ipcRenderer.invoke("open-external-url",r),saveExportedVideo:(r,t)=>e.ipcRenderer.invoke("save-exported-video",r,t)}); diff --git a/docs/HEROUI_MIGRATION.md b/docs/HEROUI_MIGRATION.md new file mode 100644 index 000000000..22b182b96 --- /dev/null +++ b/docs/HEROUI_MIGRATION.md @@ -0,0 +1,92 @@ +# HeroUI UI branch + +This migration is isolated on `codex/heroui-ui`, based on `9693ad7` from +`codex/clip-controls`. It lives in the sibling `recordly-heroui` Git worktree. +The original `recordly` checkout and its uncommitted files were left intact. +Nothing has been merged or pushed. + +## Try it + +From the original checkout: + +```sh +cd ../recordly-heroui +npm run dev +``` + +A fresh checkout needs `npm install` first, including Recordly's normal native +helper installation. The dependency lockfile belongs to this branch. + +To go back, close the development app and run `npm run dev` from the original +`recordly` directory. No reset, stash, or file restoration is needed. You can +keep both checkouts while comparing them. To undo just the UI refinement pass, +use `git revert `. To undo the entire migration after merging, +revert the branch commits newest-first, including the original `93be193` migration. + +## UI approach + +The controls use HeroUI React 3.2.6 and its default light/dark theme, following +[the official component demos](https://heroui.com/en/docs/react/components). +This includes buttons, fields, switches, sliders, tabs, tag groups, toggle groups, radios, +selects, modals, popovers, menus, tooltips, color pickers, progress indicators, +skeletons, and toasts. React 19 and Tailwind 4 satisfy HeroUI v3 requirements. +The former Radix, Sonner, and third-party color picker dependencies are removed. + +The editor uses a floating inspector card, an open canvas and timeline, aligned +toolbars, consistent spacing, and restrained selection colors. Advanced controls +live behind a per-section switch; changing views preserves project values. +Background types and other exclusive choices use TagGroup. The header follows +native fullscreen state and keeps project titles centered at narrow widths. Floating layers keep one +surface instead of nesting cards and shadows. Timeline blocks retain the original +Recordly palette in both themes. Inspector controls use compact 12–13px text and +32–36px controls, with 14px section titles. Image and video wallpaper grids share +a plus tile for importing and a small remove control on custom tiles, revealed +on hover or keyboard focus. The background gallery slides left/right with tab order. + +The clip lane samples real source frames with a bounded cache and one background +video decoder. Sampling follows clip source offsets, speed, and the visible timeline +range; it never seeks the playback element. Zooms use a compact lane with labels +that adapt to block width. The ruler chooses tick density from available width, +and the red playhead has a white centre line. Webcam roundness defaults to 100%; +existing saved values remain intact. New recordings inherit the saved webcam +appearance, and 100% is the maximum squircle radius rather than a circular mask. +Three compact timeline tracks fit before +vertical scrolling. Popovers support outside-click and Escape dismissal and +share one padding layer. Export states retain generous content padding. Timeline +selection ignores pointer jitter up to 4px, including clicks on trim handles, +so selecting a clip cannot introduce a leading gap. Project and preset names truncate inside their rows. +The recorder keeps its compact desktop layout. + +The adapters in `src/components/ui` translate existing Recordly state/callback +props to HeroUI APIs. The timeline geometry, crop handles, video canvas, caption +canvas editor, waveform, and native file inputs remain application-specific; +HeroUI does not replace those editing engines. Recording/export/project logic +is retained, with nullable ref types updated for React 19. + +## Verification + +```sh +npm run typecheck +npm test +npx vite build +npx playwright install chromium +npm run test:ui +``` + +The production build includes both the renderer and Electron main/preload +bundles. It is not an installer packaging run. + +Browser tests use an explicit mocked Electron bridge and a generated six-second +video fixture; they never start a real screen recording. A second changing-frame +fixture verifies that timeline thumbnails decode distinct source frames. They cover control +callbacks and keyboard behavior, modal focus, export settings, presets, cropping, +annotation formatting/undo, project menus, recorder popovers, countdown and update +windows, theme switching, Advanced state, color editing, wallpaper uploads and +keyboard/hover removal, and header/playback +alignment from 800–1440px with and without macOS window controls. Screenshots and failure +traces go to the ignored `test-results/` directory. + +`npm run dev:ui` starts only Vite for browser inspection; the component fixture +is at `/tests/ui/controls.html`. The real editor requires Electron or the test +bridge. Native recording, device permissions, and end-to-end media export still +need a desktop smoke test on the target platform. diff --git a/docs/announcements.md b/docs/announcements.md new file mode 100644 index 000000000..51637ff42 --- /dev/null +++ b/docs/announcements.md @@ -0,0 +1,96 @@ +# In-app announcements + +Recordly can show dismissible announcements in the editor as a popup, carousel slide, lightweight live notification, or header banner. Popups can contain images or video; notifications and banners are text-only with optional buttons. + +## Remote announcements + +Edit [`announcements.json`](../announcements.json) on the `main` branch to publish an announcement without releasing a new app version. Released clients check the raw GitHub file at most once every six hours per running app instance. Fetch failures are silent and never block startup. + +```json +{ + "settings": { + "aspectRatio": "4:3" + }, + "announcements": [ + { + "id": "recordly-1.4-release", + "title": "A faster Recordly is here", + "body": "Exports are faster and cursor motion is smoother. Thanks for using Recordly!", + "presentation": "popup", + "audience": "editor", + "priority": 10, + "mediaMode": "cover", + "displayDurationSeconds": 15, + "maxImpressions": 3, + "controls": { + "close": true, + "dismiss": false, + "action": true, + "navigation": false, + "indicators": true + }, + "startsAt": "2026-09-01T00:00:00Z", + "endsAt": "2026-10-01T00:00:00Z", + "minVersion": "1.4.0", + "media": { + "type": "image", + "url": "https://example.com/recordly-1.4-banner.jpg", + "alt": "Recordly 1.4 feature preview" + }, + "action": { + "label": "See what changed", + "url": "https://github.com/webadderallorg/Recordly/releases" + } + }, + { + "id": "recordly-maintenance-notice", + "title": "Quick service notice", + "body": "Cloud sharing will undergo brief maintenance tonight.", + "presentation": "notification", + "audience": "editor", + "displayDurationSeconds": 10, + "maxImpressions": 2, + "startsAt": "2026-09-05T00:00:00Z", + "endsAt": "2026-09-06T00:00:00Z" + }, + { + "id": "recordly-editor-banner", + "title": "Try the new editor", + "body": "The redesigned timeline is now available.", + "presentation": "banner", + "audience": "editor", + "maxImpressions": 3, + "action": { + "label": "Open settings", + "section": "settings" + } + } + ] +} +``` + +Use a new stable `id` whenever an announcement should appear again. Once a user dismisses an ID, it remains dismissed. Remote items with the same ID override bundled items. + +Supported fields: + +- `settings.aspectRatio` sets one shared `width:height` ratio for the entire popup carousel, such as `16:9`, `4:3`, or `1:1`. All slides keep that same size. If omitted, the bundled default is used. +- `id`, `title`, and `body` are required. +- `presentation` is `popup`, `notification`, or `banner` and defaults to `popup`. Notifications appear as text-only non-modal toasts, while banners appear only in the editor directly beneath its header. Media and `mediaMode` are ignored for notifications and banners. At most five notifications are shown from one feed load; banners are shown one at a time by priority. +- `audience` is `all` or `editor` and defaults to `all`. +- `priority` controls carousel order; larger numbers appear first. +- `mediaMode` is `banner` or `cover`. `banner` is the default current layout; `cover` fills the popup with the media and overlays the text. +- `startsAt` and `endsAt` are optional ISO timestamps. +- `displayDurationSeconds` accepts 3–300 seconds. It auto-advances popup slides; for notifications it controls how long the toast remains visible and defaults to 10 seconds. +- `maxImpressions` optionally limits an announcement to 1–100 app sessions. Each announcement counts at most once per session; an explicit dismissal always hides it permanently. +- `controls` can independently show or hide `close`, `dismiss`, `action`, `navigation`, and `indicators`. Every control defaults to `true`. Notifications and banners use only `close` and `action`; popup carousels use the other controls. Escape and clicking outside a popup remain available even when visible close controls are hidden. +- `minVersion` and `maxVersion` are optional inclusive app-version bounds. +- `media.type` is `image` or `video` for popups. Media URLs must be HTTPS or root-relative bundled assets. Videos can also specify `posterUrl`. +- `action` has a label and exactly one destination: an HTTPS `url` opened in the system browser, or an editor `section` opened inside the app. Supported sections are `scene`, `cursor`, `webcam`, `captions`, `settings`, and `extensions`. An action containing both destinations, neither destination, or an unknown section is ignored. + +Set `RECORDLY_ANNOUNCEMENTS_URL` before launching the app to use a different HTTPS feed. Set it to `off` to disable remote announcements. + +## Announcements bundled with an update + +Add typed entries to `src/content/announcements.ts`. Bundled items use the same schema and are available offline. This is useful when a message should ship atomically with a new release. + +Remote content is treated as data only: HTML is not rendered, URLs are restricted, feeds are size-limited and time-limited, and malformed items are ignored. diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 000000000..a4b1462f4 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,60 @@ +# Recordly authentication setup + +Recordly requires a non-anonymous Supabase Auth session before onboarding can advance to permissions or recording can start. The sign-in dialog uses email magic links and whichever of Google, Microsoft, or GitHub OAuth are enabled in the configured project; there is no guest or local demo login. Sessions persist across app restarts. The desktop application uses PKCE and returns from the system browser through a loopback callback in development and `recordly://auth/callback` in production. The dialog reads `/auth/v1/settings` and shows only enabled OAuth providers. SAML is intentionally hidden until configured. + +## Account and permission settings + +**Settings → Account** is available in both the editor and Projects dashboard. Display-name changes update Supabase user metadata and refresh the account avatar everywhere. Email changes use Supabase's email confirmation flow and the same desktop PKCE callback as sign-in; the existing email stays active until confirmation completes. Pending email changes are shown after reopening Settings. Connected sign-in methods reflect the user's actual account identities. Sign out clears the local session, revokes refresh sessions on all devices, and brings back mandatory sign-in; local projects remain on disk. + +**Settings → Permissions** shows live macOS Screen Recording and Accessibility statuses, refreshes when the app regains focus, and links to the onboarding permission setup. Microphone and camera permission requests remain tied to enabling those devices for recording. On other platforms, capture selection remains part of starting a recording. + +## 1. Create the project + +1. Create a Supabase project. +2. In **Project settings → API**, copy the project URL and publishable key. +3. Copy `.env.example` to `.env.local` and fill in both values. Never use the service-role key in the desktop application. +4. In **Authentication → URL configuration**, add `recordly://auth/callback` and `http://127.0.0.1:43821/auth/callback` to the redirect allow list. + +Enable email authentication and allow new sign-ups for magic links. Request the email from Recordly, then open it on the same computer with Recordly running. Configure production SMTP as described in `services/supabase/EMAIL_SETUP.md`. + +## 2. Google + +1. Create a Web OAuth client in Google Auth Platform. +2. Add Supabase's callback URL, `https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback`, as an authorized redirect URI. +3. Enable Google under **Supabase → Authentication → Sign In / Providers** and paste the Google client ID and secret. +4. Keep the requested scopes to `openid`, email, and profile unless Recordly genuinely needs more. + +## 3. Microsoft and GitHub + +Enable Azure and GitHub under **Authentication → Sign In / Providers**, supply their client credentials, and use `https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback` as the provider callback. Microsoft sign-in requests the `email` scope. + +## 4. SAML SSO (currently hidden) + +SAML is configured per customer workspace. Supabase's SAML support requires Pro or above. + +1. Enable SAML in the Supabase Auth provider settings. +2. Obtain the customer's IdP metadata URL or metadata XML file. +3. Register the connection and its email domain with the Supabase CLI, for example: + + ```sh + supabase sso add --type saml --project-ref YOUR_PROJECT_REF \ + --metadata-url 'https://customer.example/idp/metadata' \ + --domains customer.example + ``` + +The auth helper supports extracting a work email domain for SAML; this option is currently hidden in the modal. + +## 5. Verify locally + +Restart `npm run dev` after creating `.env.local`. Open an editor and verify: + +1. Without a session, **Welcome to Recordly** opens and cannot be skipped or dismissed. +2. Request an email magic link. Sending the email alone must not grant access. Opening the link exchanges its PKCE code for a persistent Supabase session and advances to **Permissions**. +3. OAuth opens the system browser and returns to Recordly through the same callback. +4. On Permissions, **Start recording** opens the recording controls after required system permissions are granted. +5. Restart the app to verify session restoration. Sign out from the account dialog and verify that mandatory sign-in returns. +6. **Show onboarding** opens Permissions directly for signed-in users. The recording HUD also checks for a Supabase session before preparing a recording. + +Automated UI tests use an isolated Supabase URL and mocked Auth HTTP responses; no production auth bypass is enabled in development or tests. + +Add the same `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` values to the share Worker's secrets or variables. Set `OWNER_USER_ID` to the owner’s Supabase user ID. The Worker validates the access token with Supabase and requires that owner identity before accepting API requests. `API_SECRET` is server-side only and remains available for library administration and explicitly enabled local integration tests; it is never entered into or exposed by the desktop app. diff --git a/docs/cloud-sharing.md b/docs/cloud-sharing.md new file mode 100644 index 000000000..f8e5fc238 --- /dev/null +++ b/docs/cloud-sharing.md @@ -0,0 +1,30 @@ +# Recordly cloud sharing + +Recordly's **Export** menu offers downloading the current edit or **Create share link**. The Account button at the bottom of the left toolbar opens sign-in. Sharing renders an MP4 to a temporary file, uploads it to Recordly Share, finalizes its metadata, and removes the temporary file when the dialog closes. + +The desktop app defaults to the local development endpoint: + +```text +http://localhost:8787/api/upload +``` + +The endpoint is intentionally not user-configurable. All builds currently use the local service above. Production service integration is planned but is not selected by any build. Publishing requires the user's Recordly access token. No share API secret is exposed in the app. + +## Publishing protocol + +1. `POST /api/upload` creates a recording and share code. +2. Small files use `PUT /api/upload-data/:shareCode`; files over 50 MB use multipart uploads in retryable 25 MB chunks. +3. `POST /api/metadata/:shareCode` publishes the title and creator notes. +4. The returned `/s/:shareCode` URL opens the complete viewing and feedback page. + +The service provides R2 video storage, D1 metadata, range streaming, a responsive viewing page, timestamped comments, reactions, captions, password protection, expiry, and a private video library. See [the service README](../services/recordly-share/worker/README.md) for local and deployment instructions. + +## Comments + +Anyone with a valid link can watch a public recording and leave timestamped feedback using a display name. No account or sign-in is required. The existing per-IP rate limit and 2,000-character comment limit still apply. + +## Third-party licensing + +The hosting service is based on MIT-licensed open-source software. Required attribution and the complete license text are preserved in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) and [the vendored license](../services/recordly-share/LICENSE). + +The self-hosted worker is a single-owner library. Set `OWNER_USER_ID` to the owner’s Supabase user ID. Other accounts in the same Supabase project cannot administer the library. Missing owner configuration disables Supabase bearer access. diff --git a/docs/media/CursorLoop.gif b/docs/media/CursorLoop.gif new file mode 100644 index 000000000..e4844abb8 Binary files /dev/null and b/docs/media/CursorLoop.gif differ diff --git a/docs/media/CursorSwayDemo.gif b/docs/media/CursorSwayDemo.gif new file mode 100644 index 000000000..352e82e08 Binary files /dev/null and b/docs/media/CursorSwayDemo.gif differ diff --git a/docs/media/demo.gif b/docs/media/demo.gif new file mode 100644 index 000000000..d307f34b1 Binary files /dev/null and b/docs/media/demo.gif differ diff --git a/public/wallpapers/wallpaper8.jpg b/docs/media/feature1.gif similarity index 52% rename from public/wallpapers/wallpaper8.jpg rename to docs/media/feature1.gif index 9cf1193f6..3ff0ae37b 100644 Binary files a/public/wallpapers/wallpaper8.jpg and b/docs/media/feature1.gif differ diff --git a/docs/media/feature2.gif b/docs/media/feature2.gif new file mode 100644 index 000000000..2b4ebb2ad Binary files /dev/null and b/docs/media/feature2.gif differ diff --git a/docs/media/feature3.png b/docs/media/feature3.png new file mode 100644 index 000000000..b8330b154 Binary files /dev/null and b/docs/media/feature3.png differ diff --git a/docs/pr/project-browser/idle-hud.png b/docs/pr/project-browser/idle-hud.png new file mode 100644 index 000000000..ffc398104 Binary files /dev/null and b/docs/pr/project-browser/idle-hud.png differ diff --git a/docs/pr/project-browser/login-expanded.png b/docs/pr/project-browser/login-expanded.png new file mode 100644 index 000000000..1c6657e13 Binary files /dev/null and b/docs/pr/project-browser/login-expanded.png differ diff --git a/docs/pr/project-browser/login.png b/docs/pr/project-browser/login.png new file mode 100644 index 000000000..84139cb49 Binary files /dev/null and b/docs/pr/project-browser/login.png differ diff --git a/docs/pr/project-browser/projects-dark.png b/docs/pr/project-browser/projects-dark.png new file mode 100644 index 000000000..3e20a451e Binary files /dev/null and b/docs/pr/project-browser/projects-dark.png differ diff --git a/docs/pr/project-browser/projects.png b/docs/pr/project-browser/projects.png new file mode 100644 index 000000000..a2025cba6 Binary files /dev/null and b/docs/pr/project-browser/projects.png differ diff --git a/docs/pr/project-browser/raw.png b/docs/pr/project-browser/raw.png new file mode 100644 index 000000000..4655ee2de Binary files /dev/null and b/docs/pr/project-browser/raw.png differ diff --git a/docs/pr/project-browser/recording-hud.png b/docs/pr/project-browser/recording-hud.png new file mode 100644 index 000000000..b236e2f8d Binary files /dev/null and b/docs/pr/project-browser/recording-hud.png differ diff --git a/docs/pr/project-browser/settings-motion.png b/docs/pr/project-browser/settings-motion.png new file mode 100644 index 000000000..5c2cf8ad3 Binary files /dev/null and b/docs/pr/project-browser/settings-motion.png differ diff --git a/docs/pr/project-browser/settings.png b/docs/pr/project-browser/settings.png new file mode 100644 index 000000000..5cdae534f Binary files /dev/null and b/docs/pr/project-browser/settings.png differ diff --git a/docs/pr/project-browser/shortcuts.png b/docs/pr/project-browser/shortcuts.png new file mode 100644 index 000000000..61982c2d8 Binary files /dev/null and b/docs/pr/project-browser/shortcuts.png differ diff --git a/docs/pr/user-system/account-settings.png b/docs/pr/user-system/account-settings.png new file mode 100644 index 000000000..8967d05ab Binary files /dev/null and b/docs/pr/user-system/account-settings.png differ diff --git a/docs/pr/user-system/capture-picker.png b/docs/pr/user-system/capture-picker.png new file mode 100644 index 000000000..727cf21aa Binary files /dev/null and b/docs/pr/user-system/capture-picker.png differ diff --git a/docs/pr/user-system/home-loading.png b/docs/pr/user-system/home-loading.png new file mode 100644 index 000000000..6c0679d76 Binary files /dev/null and b/docs/pr/user-system/home-loading.png differ diff --git a/docs/pr/user-system/permissions.png b/docs/pr/user-system/permissions.png new file mode 100644 index 000000000..8a765b8cc Binary files /dev/null and b/docs/pr/user-system/permissions.png differ diff --git a/docs/pr/user-system/sign-in.png b/docs/pr/user-system/sign-in.png new file mode 100644 index 000000000..eabddb6b8 Binary files /dev/null and b/docs/pr/user-system/sign-in.png differ diff --git a/docs/timeline-sequence.md b/docs/timeline-sequence.md new file mode 100644 index 000000000..dc0fe667b --- /dev/null +++ b/docs/timeline-sequence.md @@ -0,0 +1,25 @@ +# Clip sequence timing + +Primary footage is an ordered, contiguous sequence. A visual clip separator has no duration. Both sides of a cut at 2.8 seconds represent 2.8 seconds, even if the second clip starts at a different point in the source recording. + +- `ClipRegion.startMs/endMs`: edited timeline coordinates. +- `sourceStartMs`: the retained recording in-point. Packing or reordering never derives this from the new timeline position. +- `speed`: source milliseconds consumed per timeline millisecond. +- `clipSequence.ts`: packs trims/deletes/speed changes, handles explicit insertion indices, and maps connected effects and audio anchors. +- `timeline/core/clipPresentation.ts`: maps media time into inset clip rectangles and back. Scrubbing a separator returns its cut timestamp. The playhead does not wait or animate through a separator. + +Clip resizing is constrained by available source footage and minimum clip duration, rather than collision with the next clip. Dragging a clip chooses an insertion index rather than looking for empty space. Source-based captions are reprojected from their original cues. Imported audio retains its duration as its anchor moves. Annotation selection is independent of whether the annotation is visible at the current playhead time. + +Legacy projects with primary-track gaps are normalized before establishing the loaded undo baseline. Editing and export use the same contiguous clip positions. + +## Verification + +135 focused unit tests passed for sequence packing/rippling, persistence, drag and resize resolution, visual seam mapping, playback, annotation visibility and deletion shortcuts. All 13 renderer regressions passed for block deletion, dragged annotation selection, background transitions, gap-free trims, clip reordering, and connected annotation/audio undo. These browser checks use fixture media and a mocked Electron bridge. + +## Boundary snapping and preview motion + +Timeline effects use the same media-to-display mapping as clips and the playhead. Dragging or resizing a zoom, annotation, caption, or audio block snaps within 8 screen pixels of other block boundaries, including clip cuts, at every timeline zoom level. Dragging preserves the block duration. Moving beyond that distance releases the snap. Zoom blocks can span visual separators continuously. + +Hover previews and caption placement use the same inverse mapping. Keyframe positions and playhead snapping use media time consistently. Source-video seeks at contiguous cuts preserve camera springs; explicit timeline seeks still snap to the requested frame. + +Caption visibility was checked in the real renderer with fixture footage at 1×, 2× and 4×, including source-frame matching, cue visibility boundaries and moving playback. The caption test captures the preview at 2×. This verifies rendering and timing, not speech-recognition accuracy or a packaged export. diff --git a/docs/ui-redundancy-audit.md b/docs/ui-redundancy-audit.md new file mode 100644 index 000000000..2cd0fcfd3 --- /dev/null +++ b/docs/ui-redundancy-audit.md @@ -0,0 +1,65 @@ +# HeroUI editor redundancy audit + +Reviewed 19 September 2026 in the `recordly-heroui` redesign. Findings use the current renderer and source, including the existing uncommitted redesign work. + +## Cleaned up + +| Area | Problem | Change | +| --- | --- | --- | +| Zoom inspector | Two Delete Zoom buttons for the same selected block: one in the scrolling content, another in the fixed footer. | Kept the fixed footer action. | +| Extensions navigation | A permanent tool tab leads only to “Extensions are no longer available.” | Removed the navigation entry; retained the existing compatibility component. | +| Account navigation | A permanent button only shows “Account coming soon.” | Removed the placeholder button. | +| Clip and Caption inspectors | The panel title is repeated immediately as a section heading without introducing a subsection. | Kept the panel title and removed the repeated heading. | +| Basic Zoom inspector | Animation heading, Reset button and directions to Settings appear without an animation control. The actual control is Advanced-only. | Grouped these with Classic Animation under Advanced. | + +## Further consolidation opportunities + +These are recommendations, not additional changes in this patch. + +1. **Crop has two separate editing surfaces.** The toolbar opens the visual Crop Video dialog; Scene → Advanced offers four inset sliders for the same crop state. Put precise inset inputs and reset inside the crop dialog so there is one place to edit cropping. Preserve numeric precision when consolidating. +2. **Zoom controls are scattered across two inspectors.** The block inspector contains Mode, Amount and Classic Animation; Settings contains Connect neighboring zooms, automatic suggestions and Motion Presets. Group motion settings in a clearly labelled project-wide subsection alongside Zoom controls, instead of a message directing users elsewhere. Some motion presets also affect the cursor, so their scope must stay explicit. +3. **Adding content is split between Add Layer and adjacent buttons.** Add Layer offers annotation/audio, while Add Zoom is a separate icon. A single Add menu could enumerate all supported insertions; keep Split separate as an editing operation. Hover-to-add is useful direct manipulation and need not be removed. +4. **Support commands occupy permanent editing-toolbar space.** Discord and Feedback sit beside project opening and Undo/Redo. A single Help menu could contain both plus shortcut help. This is lower-priority toolbar clutter, not two identical functions. +5. **Audio repeats its inspector title.** An “Audio” heading inside the Audio inspector mainly provides a home for Reset. Move Reset to the inspector header when standardizing section actions. +6. **Saved Presets and Motion Presets need clearer scope.** The header stores whole-editor presets; Settings applies motion presets. These are different functions, but the repeated naming obscures that distinction. Label the former “Style presets” and the latter “Motion” or explain their scope. + +Removed the unused compatibility timeline toolbar and its design-catalog fixture, plus the unmounted TimelineAxis and ClipMarkerOverlay components. Removed ignored SliderControl props and their call-site values. + +## Interaction defects fixed + +- Connected `handleClipDelete` to `TimelineEditor.onClipDelete`. Previously Backspace reached a no-op because the callback was absent. +- Routed Delete, Backspace and the configured delete shortcut consistently for every selected block type before the timeline-focus check. Inspecting a block no longer makes it undeletable. Form controls, editable text, composition events and overlay controls retain their own keyboard behavior. +- Removed the clip-only Backspace special case, which bypassed shared selection priority. +- Preserved a selected caption fragment when the playhead leaves its time span. Deletion still maps the fragment to its source caption cue. +- Removed unnecessary requirements for optional selection callbacks from clip, annotation and audio deletion. +- Cleared stale keyframe selection when selecting a timeline block. +- Cleared block selection when navigating to a global sidebar tool. A selected annotation previously overrode every tool's inspector, making Cursor, Scene, Webcam and Settings appear to show the same content. + +## Validation + +Passed: TypeScript checking, 72 focused unit tests, 14 browser tests, targeted lint, and diff whitespace checks. + +Regression coverage includes keyboard deletion for each block type, inspector deletion, clip undo, caption deletion after seeking, text-entry protection, and switching away from an annotation inspector. Unit coverage includes both delete keys, configured shortcuts, selection priority, editable/overlay guards and composition/modifier handling. Browser tests use the real renderer with a mocked Electron bridge and fixture media; packaged Electron is not exercised here. + +## Videos library and HUD follow-up + +- HUD source/device rows and source/region triggers use ghost buttons. Project cards fit the 300px HUD popup, scroll vertically with thin scrollbars, and no longer truncate the library at 24 entries. +- Caption generation skips session re-registration when the source is unchanged, preserving active companion-audio approvals. Local media URLs resolve the current server port; preview retries a failed local load once through the existing approved-media API. +- Videos uses a secondary header button at the far left and replaces the existing left inspector with lightweight file cards (no embedded video players). It lists files in the configured recordings directory. Removal stages the selected recordings and their capture companions using filesystem moves. Undo restores the latest removal without copying media or overwriting newer files; the next removal or app exit sends staged files to system Trash. This replaces the earlier hide-only behavior. Automatic recording age/count pruning is removed. +- Timeline drops insert at a clip boundary. Since preview/export share a single source, imports prepare an immutable combined source in `.recordly-media`. New footage fits the current source canvas/frame rate; subsequent imports copy existing prepared video, with lossless system/mic companions and shifted cursor telemetry. Preparation takes time for large recordings. Clip source bounds prevent trimming into the next recording. Timeline history can undo/redo insertions because existing source offsets are retained. +- The existing FFmpeg metadata parser is now shared by import and export, rather than duplicating it or depending on the incompatible FFprobe binary found in this development install. + +Validation: real FFmpeg integration covers differing dimensions, audio routing, repeated inserts, and original preservation. Browser tests cover library removal/undo, drag insertion/undo/redo, caption generation without session reset, preview recovery, and 30 projects in the HUD popup. Caption visibility at 1x/2x/4x and gap snapping remain covered. + + +## Clips, overlays, and cloud integration + +- The Videos inspector uses shared search, checkbox, menu, and button controls. File rows support selection, double-click/+ insertion, and drag-to-timeline; bulk Trash is in the actions menu. No library video elements are loaded. +- Webcam imports normalize and concatenate a separate webcam track, account for capture start offsets, and preserve source-time visibility ranges. Preview and both renderer paths hide the bubble in screen-only spans. Saved projects preserve these ranges; style presets omit them and preserve the current source's ranges when applied. +- Imported click, mouse-up, right-click, and cursor-shape samples retain event types, shifted timestamps, and letterbox-adjusted coordinates. +- Annotation drag bounds now use the complete canvas. Position percentages may extend outside the recording rectangle and survive save/reload; export uses the same coordinates. Webcam and caption overlays already use their own canvas layout. +- Zoom blocks show the icon with the full label when space allows; narrow blocks hide the icon and mode while retaining the numeric multiplier. +- Cloud integration was ported from the cloud worktree, including its current service/viewer edits, desktop authentication, multipart uploads, and preparation of the current edit for sharing. Account lives at the bottom of the tool rail; Create share link lives in Export. Nothing was deployed or uploaded. Live sign-in requires the public Supabase environment settings described in authentication.md. + + +Thumbnail and sign-in refinement: Videos uses lazily requested 160×90 JPEG stills, cached by source path, modification time, and size. It displays unmodified filenames, including extensions, with ellipsis and the full hover title. Sign-in uses HeroUI Modal, Form, TextField, Input, Button, and Alert components with shared styling. diff --git a/electron-builder.json5 b/electron-builder.json5 index 327778bfe..4a6b6aed1 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -1,52 +1,130 @@ -// @see - https://www.electron.build/configuration/configuration -{ - "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", - "appId": "com.siddharthvaddem.openscreen", - "asar": true, - "productName": "Openscreen", - "directories": { - "output": "release/${version}" - }, - "files": [ - "dist", - "dist-electron" - ], - "extraResources": [ - { - "from": "public/wallpapers", - "to": "assets/wallpapers" - } - ], - "mac": { - "target": [ - "dmg" - ], - "icon": "icons/icons/mac/icon.icns", - "artifactName": "${productName}-Mac-${version}-Installer.${ext}" - }, - "win": { - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } - ], - "icon": "icons/icons/win/icon.ico", - "artifactName": "${productName}-Windows-${version}-Setup.${ext}", - "requestedExecutionLevel": "asInvoker" - }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true, - "deleteAppDataOnUninstall": false - }, - "linux": { - "target": [ - "AppImage" - ], - "artifactName": "${productName}-Linux-${version}.${ext}" - } -} +// @see - https://www.electron.build/configuration/configuration +{ + "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + "appId": "dev.recordly.app", + "electronUpdaterCompatibility": ">=2.16", + "asar": true, + "asarUnpack": [ + "node_modules/ffmpeg-static/**", + "node_modules/ffprobe-static/**", + "node_modules/uiohook-napi/**", + "electron/native/bin/**" + ], + "productName": "Recordly", + "protocols": [ + { + "name": "Recordly authentication", + "schemes": ["recordly"] + } + ], + "npmRebuild": true, + "buildDependenciesFromSource": true, + "compression": "normal", + "directories": { + "output": "release" + }, + "files": [ + "dist", + "!dist/wallpapers/**", + "dist-electron", + "electron/native/bin", + "!electron/native/bin/**/whisper-bench*", + "!electron/native/bin/**/whisper-quantize*", + "!electron/native/bin/**/whisper-server*", + "!electron/native/bin/**/whisper-vad-speech-segments*", + "!node_modules/ffprobe-static/bin/darwin/**", + "!node_modules/ffprobe-static/bin/linux/**", + "!node_modules/ffprobe-static/bin/win32/ia32/**", + "!electron/native/**/build/**", + "!*.png", + "!preview*.png", + "!*.md", + "!README.md", + "!CONTRIBUTING.md", + "!LICENSE" + ], + "extraResources": [ + { + "from": "public/wallpapers", + "to": "assets/wallpapers" + } + ], + "publish": [ + { + "provider": "github", + "owner": "webadderall", + "repo": "Recordly", + "tagNamePrefix": "v", + "publishAutoUpdate": true + } + ], + + "mac": { + "hardenedRuntime": true, + "entitlements": "build/entitlements.mac.plist", + "entitlementsInherit": "build/entitlements.mac.inherit.plist", + "target": [ + { + "target": "dmg", + "arch": ["x64", "arm64"] + }, + { + "target": "zip", + "arch": ["x64", "arm64"] + } + ], + "icon": "icons/icons/mac/icon.icns", + "artifactName": "${productName}-${arch}.${ext}", + "extendInfo": { + "NSAudioCaptureUsageDescription": "Recordly needs audio capture permission to record system audio.", + "NSCameraUsageDescription": "Recordly needs camera access to record webcam video.", + "NSMicrophoneUsageDescription": "Recordly needs microphone access to record voice audio.", + "NSCameraUseContinuityCameraDeviceType": true, + "com.apple.security.device.audio-input": true, + "CFBundleDocumentTypes": [ + { + "CFBundleTypeName": "Recordly Project", + "CFBundleTypeRole": "Editor", + "CFBundleTypeExtensions": ["recordly"], + "CFBundleTypeIconFile": "icon.icns", + "LSHandlerRank": "Owner", + "LSItemContentTypes": ["dev.recordly.app.project"] + } + ], + "UTExportedTypeDeclarations": [ + { + "UTTypeIdentifier": "dev.recordly.app.project", + "UTTypeDescription": "Recordly Project", + "UTTypeConformsTo": ["public.json"], + "UTTypeTagSpecification": { + "public.filename-extension": ["recordly"], + "public.mime-type": "application/x-recordly-project" + } + } + ] + } + }, + "linux": { + "target": [ + "AppImage" + ], + "icon": "icons/icons/png", + "artifactName": "${productName}-linux-x64.${ext}", + "category": "AudioVideo" + }, + "win": { + "target": [ + "nsis" + ], + "icon": "icons/icons/win/icon.ico", + "executableName": "Recordly", + "artifactName": "${productName}-windows-${arch}.${ext}" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "Recordly" + } +} diff --git a/electron/appPaths.ts b/electron/appPaths.ts new file mode 100644 index 000000000..68d6cfc39 --- /dev/null +++ b/electron/appPaths.ts @@ -0,0 +1,11 @@ +import path from "node:path"; +import { app } from "electron"; + +if (process.env["VITE_DEV_SERVER_URL"]) { + const devUserDataPath = path.join(app.getPath("appData"), "Recordly-dev"); + app.setPath("userData", devUserDataPath); + app.setPath("sessionData", path.join(devUserDataPath, "session")); +} + +export const USER_DATA_PATH = app.getPath("userData"); +export const RECORDINGS_DIR = path.join(USER_DATA_PATH, "recordings"); diff --git a/electron/appSettingsStore.ts b/electron/appSettingsStore.ts new file mode 100644 index 000000000..c777b1004 --- /dev/null +++ b/electron/appSettingsStore.ts @@ -0,0 +1,36 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { APP_SETTINGS_FILE } from "./ipc/constants"; +import { parseJsonWithByteOrderMark } from "./ipc/utils"; + +export function readAppSettingsStore(): Record { + try { + const content = readFileSync(APP_SETTINGS_FILE, "utf-8"); + const parsed = parseJsonWithByteOrderMark(content); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return {}; + } + + return parsed as Record; + } catch { + return {}; + } +} + +export function writeAppSettingsStore(store: Record) { + writeFileSync(APP_SETTINGS_FILE, JSON.stringify(store, null, 2), "utf-8"); +} + +export function hasAppSetting(store: Record, key: string): boolean { + return Reflect.getOwnPropertyDescriptor(store, key) !== undefined; +} + +export function readAppSetting(key: string): unknown { + const store = readAppSettingsStore(); + return hasAppSetting(store, key) ? store[key] : null; +} + +export function writeAppSetting(key: string, value: unknown) { + const store = readAppSettingsStore(); + store[key] = value; + writeAppSettingsStore(store); +} diff --git a/electron/authCallback.ts b/electron/authCallback.ts new file mode 100644 index 000000000..cd4ef1cfc --- /dev/null +++ b/electron/authCallback.ts @@ -0,0 +1,120 @@ +import { createServer, type Server } from "node:http"; +import { app, BrowserWindow, ipcMain } from "electron"; + +const DEV_CALLBACK_HOST = "127.0.0.1:43821"; +const DEV_CALLBACK_ORIGIN = `http://${DEV_CALLBACK_HOST}`; +const CALLBACK_PATH = "/callback"; +const LOOPBACK_CALLBACK_PATH = "/auth/callback"; +const MAX_CALLBACK_URL_LENGTH = 8192; +const CALLBACK_PARAMETERS = new Set(["code", "error", "error_code", "error_description"]); + +type AuthCallbackOptions = { + isDev: boolean; + focusApp: () => void; +}; + +function callbackHeaders(contentType: string) { + return { + "Cache-Control": "no-store", + "Content-Security-Policy": + "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'", + "Content-Type": contentType, + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + }; +} + +export function createAuthCallbackController({ isDev, focusApp }: AuthCallbackOptions) { + const protocol = isDev ? "recordly-dev" : "recordly"; + let pendingUrl: string | null = null; + let server: Server | null = null; + + function parseCallback(rawUrl: string): URL | null { + if (rawUrl.length > MAX_CALLBACK_URL_LENGTH) return null; + try { + const url = new URL(rawUrl); + if (url.protocol !== `${protocol}:` || url.hostname !== "auth") return null; + if (url.pathname !== CALLBACK_PATH || url.username || url.password) return null; + return url; + } catch { + return null; + } + } + + function find(args: readonly string[]) { + return args.find((arg) => parseCallback(arg) !== null) ?? null; + } + + function dispatch(rawUrl: string) { + const url = parseCallback(rawUrl); + if (!url) return false; + pendingUrl = url.href; + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed()) window.webContents.send("auth:callback", url.href); + } + if (app.isReady()) focusApp(); + return true; + } + + function startDevServer() { + if (!isDev || server) return; + server = createServer((request, response) => { + if ( + request.method !== "GET" || + request.headers.host !== DEV_CALLBACK_HOST || + (request.url?.length ?? 0) > MAX_CALLBACK_URL_LENGTH + ) { + response.writeHead(404, callbackHeaders("text/plain; charset=utf-8")); + response.end("Not found"); + return; + } + + const requestUrl = new URL(request.url ?? "/", DEV_CALLBACK_ORIGIN); + if (requestUrl.pathname !== LOOPBACK_CALLBACK_PATH) { + response.writeHead(404, callbackHeaders("text/plain; charset=utf-8")); + response.end("Not found"); + return; + } + + const appUrl = new URL(`${protocol}://auth${CALLBACK_PATH}`); + for (const key of CALLBACK_PARAMETERS) { + for (const value of requestUrl.searchParams.getAll(key)) { + appUrl.searchParams.append(key, value); + } + } + if (!appUrl.searchParams.has("code") && !appUrl.searchParams.has("error")) { + response.writeHead(400, callbackHeaders("text/plain; charset=utf-8")); + response.end("Invalid authentication callback"); + return; + } + + dispatch(appUrl.href); + response.writeHead(200, callbackHeaders("text/html; charset=utf-8")); + response.end( + 'Return to Recordly

Return to Recordly

Your sign-in link has been received. Return to the app to finish signing in.

', + ); + }); + server.on("error", (error) => { + console.error("[auth] Could not start local callback server", error); + server = null; + }); + server.listen(43821, "127.0.0.1"); + } + + function close() { + server?.close(); + server = null; + } + + app.on("open-url", (event, url) => { + event.preventDefault(); + dispatch(url); + }); + + ipcMain.handle("auth:get-pending-callback", () => pendingUrl); + ipcMain.handle("auth:ack-callback", (_, url: string) => { + if (pendingUrl === url) pendingUrl = null; + }); + + return { close, dispatch, find, protocol, startDevServer }; +} diff --git a/electron/countdownController.test.ts b/electron/countdownController.test.ts new file mode 100644 index 000000000..eca328a56 --- /dev/null +++ b/electron/countdownController.test.ts @@ -0,0 +1,86 @@ +import { EventEmitter } from "node:events"; +import type { BrowserWindow } from "electron"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { createCountdownController } from "./countdownController"; +function fixture(loading = false) { + const contents = Object.assign(new EventEmitter(), { + isLoadingMainFrame: () => loading, + isDestroyed: () => false, + send: vi.fn(), + }); + const win = Object.assign(new EventEmitter(), { + webContents: contents, + isDestroyed: () => false, + close: vi.fn(), + }); + const state = vi.fn(); + const controller = createCountdownController(() => win as unknown as BrowserWindow, state); + return { controller, win, contents, state }; +} +beforeEach(() => vi.useFakeTimers()); +afterEach(() => vi.useRealTimers()); +it("cancels an active countdown immediately and allows another start", async () => { + const { controller, state } = fixture(); + const pending = controller.start(3); + controller.cancel(); + await expect(pending).resolves.toEqual({ success: false, cancelled: true }); + expect(state).toHaveBeenLastCalledWith(null); + expect(vi.getTimerCount()).toBe(0); + const next = controller.start(1); + await vi.advanceTimersByTimeAsync(1000); + await expect(next).resolves.toEqual({ success: true }); +}); +it("settles cancellation before the countdown renderer finishes loading", async () => { + const { controller, contents } = fixture(true); + const pending = controller.start(3); + controller.cancel(); + await expect(pending).resolves.toEqual({ success: false, cancelled: true }); + contents.emit("did-finish-load"); + expect(contents.send).not.toHaveBeenCalled(); + expect(vi.getTimerCount()).toBe(0); +}); +it.each([ + "closed", + "render-process-gone", +])("settles when the window exits through %s", async (event) => { + const { controller, win, contents } = fixture(true); + const pending = controller.start(3); + (event === "closed" ? win : contents).emit(event); + await expect(pending).resolves.toEqual({ success: false, cancelled: true }); +}); +it("settles loading failures and releases the countdown lock", async () => { + const { controller, contents } = fixture(true); + const pending = controller.start(3); + contents.emit("did-fail-load"); + await expect(pending).resolves.toMatchObject({ success: false, error: expect.any(String) }); + const next = controller.start(1); + contents.emit("did-finish-load"); + await vi.advanceTimersByTimeAsync(1000); + await expect(next).resolves.toEqual({ success: true }); +}); +it("rejects concurrent starts and emits ticks until normal completion", async () => { + const { controller, contents } = fixture(); + const pending = controller.start(2); + await expect(controller.start(3)).resolves.toMatchObject({ + success: false, + error: expect.any(String), + }); + await vi.advanceTimersByTimeAsync(2000); + await expect(pending).resolves.toEqual({ success: true }); + expect(contents.send.mock.calls).toEqual([ + ["countdown-tick", 2], + ["countdown-tick", 1], + ]); + expect(vi.getTimerCount()).toBe(0); +}); +it("resets state if creating the countdown window fails", async () => { + const state = vi.fn(); + const controller = createCountdownController(() => { + throw Error("window unavailable"); + }, state); + await expect(controller.start(3)).resolves.toMatchObject({ + success: false, + error: expect.any(String), + }); + expect(state).toHaveBeenLastCalledWith(null); +}); diff --git a/electron/countdownController.ts b/electron/countdownController.ts new file mode 100644 index 000000000..3a3ae3098 --- /dev/null +++ b/electron/countdownController.ts @@ -0,0 +1,83 @@ +import type { BrowserWindow } from "electron"; + +type Result = { success: boolean; cancelled?: boolean; error?: string }; +/** Every countdown exit settles the IPC request, including cancellation during window load. */ +export function createCountdownController( + createWindow: () => BrowserWindow, + onRemaining: (seconds: number | null) => void, +) { + let finishActive: ((result: Result) => void) | null = null; + return { + start(seconds: number): Promise { + if (finishActive) + return Promise.resolve({ success: false, error: "Countdown already in progress" }); + if (!Number.isFinite(seconds) || seconds < 0) + return Promise.resolve({ success: false, error: "Invalid countdown delay" }); + return new Promise((resolve) => { + let win: BrowserWindow | undefined; + let timer: ReturnType | undefined; + let settled = false; + let started = false; + let remaining = Math.ceil(seconds); + const finish = (result: Result) => { + if (settled) return; + settled = true; + if (timer) clearInterval(timer); + finishActive = null; + onRemaining(null); + if (win) { + win.removeListener("closed", cancel); + win.webContents.removeListener("did-finish-load", begin); + win.webContents.removeListener("did-fail-load", fail); + win.webContents.removeListener("render-process-gone", cancel); + } + resolve(result); + if (win && !win.isDestroyed()) win.close(); + }; + const cancel = () => finish({ success: false, cancelled: true }); + const fail = () => + finish({ success: false, error: "Countdown window failed to load" }); + const tick = () => { + if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { + cancel(); + return; + } + onRemaining(remaining); + win.webContents.send("countdown-tick", remaining); + }; + const begin = () => { + if (settled || started) return; + started = true; + if (remaining === 0) { + finish({ success: true }); + return; + } + tick(); + if (settled) return; + timer = setInterval(() => { + remaining--; + if (remaining <= 0) finish({ success: true }); + else tick(); + }, 1000); + }; + finishActive = finish; + onRemaining(remaining); + try { + win = createWindow(); + win.once("closed", cancel); + win.webContents.once("did-fail-load", fail); + win.webContents.once("render-process-gone", cancel); + if (win.webContents.isLoadingMainFrame()) + win.webContents.once("did-finish-load", begin); + else begin(); + } catch (error) { + finish({ success: false, error: String(error) }); + } + }); + }, + cancel() { + finishActive?.({ success: false, cancelled: true }); + return { success: true }; + }, + }; +} diff --git a/electron/cursorHider.ts b/electron/cursorHider.ts new file mode 100644 index 000000000..266d111a9 --- /dev/null +++ b/electron/cursorHider.ts @@ -0,0 +1,144 @@ +import { spawnSync } from "node:child_process"; + +const PY_HIDE_WIN = ` +import ctypes, sys + +class POINT(ctypes.Structure): + _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)] + +class CURSORINFO(ctypes.Structure): + _fields_ = [ + ("cbSize", ctypes.c_uint), + ("flags", ctypes.c_uint), + ("hCursor", ctypes.c_void_p), + ("ptScreenPos", POINT), + ] + +user32 = ctypes.windll.user32 +CURSOR_SHOWING = 0x00000001 + +for _ in range(32): + info = CURSORINFO() + info.cbSize = ctypes.sizeof(CURSORINFO) + if user32.GetCursorInfo(ctypes.byref(info)) and not (info.flags & CURSOR_SHOWING): + sys.exit(0) + user32.ShowCursor(False) + +sys.exit(0) +`.trim(); + +const PY_SHOW_WIN = ` +import ctypes, sys + +class POINT(ctypes.Structure): + _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)] + +class CURSORINFO(ctypes.Structure): + _fields_ = [ + ("cbSize", ctypes.c_uint), + ("flags", ctypes.c_uint), + ("hCursor", ctypes.c_void_p), + ("ptScreenPos", POINT), + ] + +user32 = ctypes.windll.user32 +CURSOR_SHOWING = 0x00000001 + +for _ in range(32): + info = CURSORINFO() + info.cbSize = ctypes.sizeof(CURSORINFO) + if user32.GetCursorInfo(ctypes.byref(info)) and (info.flags & CURSOR_SHOWING): + sys.exit(0) + user32.ShowCursor(True) + +sys.exit(0) +`.trim(); + +function getPowerShellCommand(show: boolean) { + const desiredFlag = show ? 1 : 0; + const showLiteral = show ? "$true" : "$false"; + + return [ + '$signature = @"', + "using System;", + "using System.Runtime.InteropServices;", + "public struct POINT { public int X; public int Y; }", + "public struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public POINT ptScreenPos; }", + "public static class CursorNative {", + ' [DllImport("user32.dll")] public static extern int ShowCursor(bool show);', + ' [DllImport("user32.dll")] public static extern bool GetCursorInfo(ref CURSORINFO info);', + "}", + '"@;', + "Add-Type -TypeDefinition $signature -Language CSharp -ErrorAction SilentlyContinue | Out-Null;", + "$info = New-Object CURSORINFO;", + "$info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf([type]CURSORINFO);", + "for ($i = 0; $i -lt 32; $i++) {", + " if ([CursorNative]::GetCursorInfo([ref]$info) -and (($info.flags -band 1) -eq " + + desiredFlag + + ")) { exit 0 }", + " [CursorNative]::ShowCursor(" + showLiteral + ") | Out-Null;", + "}", + "exit 0", + ].join(" "); +} + +function runPythonSnippet(code: string) { + for (const executable of ["python", "python3", "py"]) { + const result = spawnSync(executable, ["-c", code], { timeout: 5000 }); + if (!result.error && result.status === 0) { + return true; + } + } + + return false; +} + +function runPowerShellSnippet(command: string) { + const result = spawnSync( + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", command], + { timeout: 8000 }, + ); + + return !result.error && result.status === 0; +} + +let cursorHidden = false; + +export function hideCursor() { + if (process.platform !== "win32" || cursorHidden) { + return false; + } + + try { + const didHide = + runPythonSnippet(PY_HIDE_WIN) || runPowerShellSnippet(getPowerShellCommand(false)); + + if (didHide) { + cursorHidden = true; + } + + return didHide; + } catch (error) { + console.error("[cursorHider] Failed to hide Windows cursor:", error); + return false; + } +} + +export function showCursor() { + if (process.platform !== "win32" || !cursorHidden) { + return false; + } + + try { + const didShow = + runPythonSnippet(PY_SHOW_WIN) || runPowerShellSnippet(getPowerShellCommand(true)); + if (didShow) { + cursorHidden = false; + } + return didShow; + } catch (error) { + console.error("[cursorHider] Failed to show Windows cursor:", error); + return false; + } +} diff --git a/electron/editorStartupView.test.ts b/electron/editorStartupView.test.ts new file mode 100644 index 000000000..db1aa14e4 --- /dev/null +++ b/electron/editorStartupView.test.ts @@ -0,0 +1,17 @@ +import { expect, it } from "vitest"; +import { resolveEditorStartupView } from "./editorStartupView"; + +it("shows Home for an empty app session", () => { + expect(resolveEditorStartupView({})).toBe("home"); +}); +for (const source of [ + { projectPath: "/projects/demo.recordly" }, + { videoPath: "/recordings/demo.mp4" }, + { recordingVideoPath: "/recordings/fresh.mp4" }, + { devInput: "/fixtures/demo.mp4" }, + { smokeExport: true }, +]) { + it(`uses the editor skeleton for ${Object.keys(source)[0]}`, () => { + expect(resolveEditorStartupView(source)).toBe("editor"); + }); +} diff --git a/electron/editorStartupView.ts b/electron/editorStartupView.ts new file mode 100644 index 000000000..18636f588 --- /dev/null +++ b/electron/editorStartupView.ts @@ -0,0 +1,18 @@ +/** Resolve the destination before renderer code or project hydration runs. */ +export function resolveEditorStartupView({ + projectPath, + videoPath, + recordingVideoPath, + devInput, + smokeExport, +}: { + projectPath?: string | null; + videoPath?: string | null; + recordingVideoPath?: string | null; + devInput?: string; + smokeExport?: boolean; +}): "home" | "editor" { + return projectPath || videoPath || recordingVideoPath || devInput || smokeExport + ? "editor" + : "home"; +} diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 0853fc160..c98cad95a 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -1,50 +1,1150 @@ -/// - -declare namespace NodeJS { - interface ProcessEnv { - /** - * The built directory structure - * - * ```tree - * ├─┬─┬ dist - * │ │ └── index.html - * │ │ - * │ ├─┬ dist-electron - * │ │ ├── main.js - * │ │ └── preload.js - * │ - * ``` - */ - APP_ROOT: string - /** /dist/ or /public/ */ - VITE_PUBLIC: string - } -} - -// Used in Renderer process, expose in `preload.ts` -interface Window { - electronAPI: { - getSources: (opts: Electron.SourcesOptions) => Promise - switchToEditor: () => Promise - openSourceSelector: () => Promise - selectSource: (source: any) => Promise - getSelectedSource: () => Promise - startMouseTracking: () => Promise - stopMouseTracking: () => Promise - storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }> - storeMouseTrackingData: (fileName: string) => Promise<{ success: boolean; path?: string; eventCount?: number; message?: string }> - getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }> - setRecordingState: (recording: boolean) => Promise - onStopRecordingFromTray: (callback: () => void) => () => void - openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }> - saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }> - } -} - -interface ProcessedDesktopSource { - id: string - name: string - display_id: string - thumbnail: string | null - appIcon: string | null -} +/// + +declare namespace NodeJS { + interface ProcessEnv { + /** + * The built directory structure + * + * ```tree + * ├─┬─┬ dist + * │ │ └── index.html + * │ │ + * │ ├─┬ dist-electron + * │ │ ├── main.js + * │ │ └── preload.js + * │ + * ``` + */ + APP_ROOT: string; + /** /dist/ or /public/ */ + VITE_PUBLIC: string; + } +} + +// Used in Renderer process, expose in `preload.ts` +interface NativeCaptureDiagnostics { + backend: "windows-wgc" | "mac-screencapturekit" | "browser-store" | "ffmpeg"; + phase: "availability" | "start" | "stop" | "mux"; + timestamp: string; + sourceId?: string | null; + sourceType?: "screen" | "window" | "unknown"; + displayId?: number | null; + displayBounds?: { x: number; y: number; width: number; height: number } | null; + windowHandle?: number | null; + helperPath?: string | null; + outputPath?: string | null; + systemAudioPath?: string | null; + microphonePath?: string | null; + osRelease?: string; + supported?: boolean; + helperExists?: boolean; + fileSizeBytes?: number | null; + processOutput?: string; + error?: string; +} + +interface UpdateToastState { + version: string; + detail: string; + phase: "available" | "downloading" | "ready" | "error"; + delayMs: number; + isPreview?: boolean; + isExperimental?: boolean; + progressPercent?: number; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; +} + +interface UpdateStatusSummary { + status: "idle" | "checking" | "up-to-date" | "available" | "downloading" | "ready" | "error"; + currentVersion: string; + availableVersion: string | null; + detail?: string; +} + +type RendererRecordingSessionData = import("./ipc/types").RecordingSessionData; + +interface RendererFfmpegAudioMuxMetrics { + tempVideoWriteMs?: number; + tempEditedAudioWriteMs?: number; + ffmpegExecMs?: number; + muxedVideoReadMs?: number; + tempVideoBytes?: number; + tempEditedAudioBytes?: number; + muxedVideoBytes?: number; +} + +interface RendererWindowsGpuExportSummary { + success?: boolean; + width?: number; + height?: number; + fps?: number; + seconds?: number; + mediaMs?: number; + frames?: number; + gpuDecodeSurface?: boolean; + webcamOverlay?: boolean; + cursorOverlay?: boolean; + zoomOverlay?: boolean; + surfacePoolSize?: number; + adapterIndex?: number; + adapterVendorId?: number; + adapterDeviceId?: number; + adapterDedicatedVideoMemoryMB?: number; + encoderBackend?: string; + encoderTuningApplied?: boolean; + nvencOutputBytes?: number; + initializeMs?: number; + initCoInitializeMs?: number; + initMfStartupMs?: number; + initD3DDeviceMs?: number; + initSourceReaderMs?: number; + initWebcamReaderMs?: number; + initVideoProcessorMs?: number; + initTexturesMs?: number; + initShaderPipelineMs?: number; + initSinkWriterMs?: number; + totalMs?: number; + readMs?: number; + clearMs?: number; + videoProcessMs?: number; + writeSampleMs?: number; + finalizeMs?: number; + realtimeMultiplier?: number; +} + +interface RendererNativeStaticLayoutChunkMetric { + index: number; + startSec: number; + durationSec: number; + backend: + | "cuda-overlay" + | "cuda-scale-cpu-pad" + | "cuda-static-composite" + | "nvidia-cuda-compositor" + | "windows-d3d11-compositor"; + elapsedMs: number; + outputBytes: number; + fallbackReason?: string; + windowsGpuSummary?: RendererWindowsGpuExportSummary; +} + +interface RendererNativeStaticLayoutMetrics extends RendererFfmpegAudioMuxMetrics { + chunkCount: number; + chunkDurationSec: number; + chunkExecMs: number; + concatExecMs?: number; + staticAssetExecMs?: number; + fallbackChunkCount: number; + videoOnlyBytes?: number; + chunks: RendererNativeStaticLayoutChunkMetric[]; +} + +interface RendererNativeStaticLayoutProgress { + sessionId?: string; + backend?: RendererNativeStaticLayoutChunkMetric["backend"]; + stage?: "preparing" | "finalizing"; + elapsedMs?: number; + averageFps?: number; + instantFps?: number; + intervalMs?: number; + intervalFrames?: number; + intervalDecodeWallMs?: number; + intervalEncodeMs?: number; + intervalPipelineWaitMs?: number; + intervalCompositeMs?: number; + intervalNvencMs?: number; + intervalPacketWriteMs?: number; + intervalWebcamDecodeMs?: number; + intervalWebcamCopyMs?: number; + intervalRoiCompositeFrames?: number; + intervalMonolithicCompositeFrames?: number; + intervalCopyCompositeFrames?: number; + currentFrame: number; + totalFrames: number; + percentage: number; +} + +interface RendererNativeVideoMetadataProbe { + width: number; + height: number; + duration: number; + mediaStartTime?: number; + streamStartTime?: number; + streamDuration?: number; + frameRate: number; + codec: string; + hasAudio: boolean; + audioCodec?: string; + audioSampleRate?: number; +} + +interface RendererNativeExportCapabilities { + platform: NodeJS.Platform; + nvidiaCuda: { + available: boolean; + skipReason: string | null; + hasNvidiaGpu: boolean | null; + hasWrapper: boolean; + explicitEnabled: boolean; + explicitDisabled: boolean; + userOptInRequired: boolean; + }; +} + +interface RendererExportHardwareInfo { + platform: NodeJS.Platform; + release: string; + arch: string; + cpuModel: string | null; + logicalProcessors: number; + totalMemoryGb: number; + machineModel: string | null; + gpus: Array<{ + name: string; + vendor: string | null; + active: boolean | null; + }>; + gpuFeatures: { + videoDecode: string | null; + videoEncode: string | null; + webgl: string | null; + webgpu: string | null; + }; +} + +interface Window { + electronAPI: { + hudOverlaySetIgnoreMouse: (ignore: boolean) => void; + hudOverlaySetSourceSelectionActive: (active: boolean) => void; + hudOverlayDrag: (phase: "start" | "move" | "end", screenX: number, screenY: number) => void; + hudOverlayHide: () => void; + hudOverlayClose: () => void; + getEditorMode: () => Promise; + onEditorModeChanged: (callback: (inEditor: boolean) => void) => () => void; + hudOverlayRendererReady: () => void; + hudOverlaySetWebcamPreviewVisible: (visible: boolean) => void; + getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>; + getHudOverlayMousePassthroughSupported: () => Promise<{ + success: boolean; + supported: boolean; + }>; + setHudOverlayCaptureProtection: ( + enabled: boolean, + ) => Promise<{ success: boolean; enabled: boolean }>; + getAssetBasePath: () => Promise; + getSources: (opts: Electron.SourcesOptions) => Promise; + showProjectDashboard: () => Promise; + switchToEditor: () => Promise; + openSourceSelector: () => Promise; + selectSource: (source: ProcessedDesktopSource) => Promise; + showSourceHighlight: (source: ProcessedDesktopSource) => Promise<{ success: boolean }>; + /** Opens the capture picker on every display and resolves when it closes. */ + pickCaptureTarget: () => Promise<{ + success: boolean; + canceled?: boolean; + record?: boolean; + source?: ProcessedDesktopSource; + message?: string; + }>; + completeCapturePick: (pick: CapturePick | null) => Promise; + getCapturePickerContext: (displayId: number) => Promise<{ + displayBounds: CaptureArea | null; + lastArea: CaptureArea | null; + allowArea: boolean; + /** On-screen windows on this display, front to back. */ + windows: CapturePickerWindow[]; + cursor: { x: number; y: number } | null; + }>; + /** Reveals the picker window once it has painted its first state. */ + capturePickerReady: () => void; + getSelectedSource: () => Promise; + onSelectedSourceChanged: ( + callback: (source: ProcessedDesktopSource | null) => void, + ) => () => void; + startNativeScreenRecording: ( + source: ProcessedDesktopSource, + options?: { + capturesSystemAudio?: boolean; + capturesMicrophone?: boolean; + microphoneDeviceId?: string; + microphoneLabel?: string; + }, + ) => Promise<{ + success: boolean; + path?: string; + message?: string; + error?: string; + userNotified?: boolean; + microphoneFallbackRequired?: boolean; + }>; + stopNativeScreenRecording: () => Promise<{ + success: boolean; + path?: string; + message?: string; + error?: string; + }>; + recoverNativeScreenRecording: () => Promise<{ + success: boolean; + path?: string; + message?: string; + error?: string; + }>; + getLastNativeCaptureDiagnostics: () => Promise<{ + success: boolean; + diagnostics?: NativeCaptureDiagnostics | null; + }>; + pauseNativeScreenRecording: () => Promise<{ + success: boolean; + message?: string; + error?: string; + }>; + resumeNativeScreenRecording: () => Promise<{ + success: boolean; + message?: string; + error?: string; + }>; + pauseCursorCapture: (pausedAtMs?: number) => Promise<{ + success: boolean; + message?: string; + error?: string; + }>; + resumeCursorCapture: (resumedAtMs?: number) => Promise<{ + success: boolean; + message?: string; + error?: string; + }>; + startFfmpegRecording: ( + source: ProcessedDesktopSource, + ) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>; + stopFfmpegRecording: () => Promise<{ + success: boolean; + path?: string; + message?: string; + error?: string; + }>; + storeRecordedVideo: ( + videoData: ArrayBuffer, + fileName: string, + ) => Promise<{ success: boolean; path?: string; message?: string }>; + storeMicrophoneSidecar: ( + audioData: ArrayBuffer, + videoPath: string, + options?: { + startDelayMs?: number; + browserMicrophoneProfile?: string; + requestedBrowserMicrophoneProfile?: string | null; + requestedConstraints?: unknown; + mediaTrackSettings?: Record; + audioInputDevices?: Array<{ + deviceId: string; + groupId?: string; + label: string; + }>; + mediaRecorder?: { + mimeType?: string; + audioBitsPerSecond?: number; + timesliceMs?: number; + }; + chunkEvents?: Array<{ + index: number; + size: number; + elapsedMs: number; + deltaMs: number | null; + recordedElapsedMs?: number; + recordedDeltaMs?: number | null; + }>; + pauseIntervals?: Array<{ + startElapsedMs: number; + endElapsedMs?: number; + durationMs?: number; + }>; + }, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>; + listAssetDirectory: (relativeDir: string) => Promise<{ + success: boolean; + files?: string[]; + error?: string; + }>; + readLocalFile: ( + filePath: string, + ) => Promise<{ success: boolean; data?: Uint8Array; error?: string }>; + generateWallpaperThumbnail: ( + filePath: string, + ) => Promise<{ success: boolean; data?: Uint8Array; error?: string }>; + probeNativeVideoMetadata: (filePath: string) => Promise<{ + success: boolean; + metadata?: RendererNativeVideoMetadataProbe; + error?: string; + }>; + getNativeExportCapabilities: () => Promise<{ + success: boolean; + capabilities?: RendererNativeExportCapabilities; + error?: string; + }>; + getExportHardwareInfo: () => Promise<{ + success: boolean; + hardware?: RendererExportHardwareInfo; + error?: string; + }>; + nativeStaticLayoutExport: (options: { + sessionId?: string; + inputPath: string; + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: "fast" | "balanced" | "quality"; + durationSec: number; + contentWidth: number; + contentHeight: number; + offsetX: number; + offsetY: number; + sourceCropX?: number; + sourceCropY?: number; + sourceCropWidth?: number; + sourceCropHeight?: number; + backgroundColor: string; + backgroundImagePath?: string | null; + backgroundBlurPx?: number; + borderRadius?: number; + shadowIntensity?: number; + webcamInputPath?: string | null; + webcamLeft?: number; + webcamTop?: number; + webcamSize?: number; + webcamRadius?: number; + webcamShadowIntensity?: number; + webcamMirror?: boolean; + webcamTimeOffsetMs?: number; + cursorTelemetry?: Array<{ + timeMs: number; + cx: number; + cy: number; + cursorTypeIndex?: number; + bounceScale?: number; + visible?: boolean; + }>; + cursorSize?: number; + cursorAtlasPngDataUrl?: string | null; + cursorAtlasEntries?: Array<{ + index: number; + x: number; + y: number; + width: number; + height: number; + anchorX: number; + anchorY: number; + aspectRatio: number; + }>; + zoomTelemetry?: Array<{ timeMs: number; scale: number; x: number; y: number }>; + timelineSegments?: Array<{ + sourceStartMs: number; + sourceEndMs: number; + outputStartMs: number; + outputEndMs: number; + speed: number; + }>; + chunkDurationSec?: number; + experimentalWindowsGpuCompositor?: boolean; + experimentalNvidiaCudaExport?: boolean; + audioOptions?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }; + }) => Promise<{ + success: boolean; + tempPath?: string; + encoderName?: string; + error?: string; + metrics?: RendererNativeStaticLayoutMetrics; + }>; + nativeStaticLayoutExportCancel: (sessionId: string) => Promise<{ + success: boolean; + }>; + onNativeStaticLayoutExportProgress: ( + callback: (progress: RendererNativeStaticLayoutProgress) => void, + ) => () => void; + nativeVideoExportStart: (options: { + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: "fast" | "balanced" | "quality"; + inputMode?: "rawvideo" | "h264-stream"; + }) => Promise<{ + success: boolean; + sessionId?: string; + encoderName?: string; + error?: string; + }>; + nativeVideoExportWriteFrame: ( + sessionId: string, + frameData: Uint8Array, + ) => Promise<{ success: boolean; error?: string }>; + nativeVideoExportWriteFrames: ( + sessionId: string, + frameDataList: Uint8Array[], + ) => Promise<{ success: boolean; error?: string }>; + nativeVideoExportFinish: ( + sessionId: string, + options?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }, + ) => Promise<{ + success: boolean; + tempPath?: string; + encoderName?: string; + error?: string; + metrics?: RendererFfmpegAudioMuxMetrics; + }>; + nativeVideoExportCancel: ( + sessionId: string, + ) => Promise<{ success: boolean; error?: string }>; + muxExportedVideoAudio: ( + videoData: ArrayBuffer, + options?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }, + ) => Promise<{ + success: boolean; + tempPath?: string; + error?: string; + metrics?: RendererFfmpegAudioMuxMetrics; + }>; + muxExportedVideoAudioFromPath: ( + videoPath: string, + options?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }, + ) => Promise<{ + success: boolean; + tempPath?: string; + error?: string; + metrics?: RendererFfmpegAudioMuxMetrics; + }>; + openExportStream: (options?: { extension?: string }) => Promise<{ + success: boolean; + streamId?: string; + tempPath?: string; + error?: string; + }>; + writeExportStreamChunk: ( + streamId: string, + position: number, + chunk: Uint8Array, + ) => Promise<{ success: boolean; error?: string }>; + closeExportStream: ( + streamId: string, + options?: { abort?: boolean }, + ) => Promise<{ + success: boolean; + tempPath?: string; + bytesWritten?: number; + error?: string; + }>; + finalizeExportedVideo: (payload: { + tempPath: string; + fileName: string; + outputPath?: string | null; + captionSidecar?: { + format: "srt" | "vtt" | "both"; + cues: Array<{ + startMs: number; + endMs: number; + text: string; + }>; + }; + }) => Promise<{ + success: boolean; + path?: string; + canceled?: boolean; + message?: string; + error?: string; + }>; + discardExportedTemp: (tempPath: string) => Promise<{ success: boolean; error?: string }>; + getVideoAudioFallbackPaths: (videoPath: string) => Promise<{ + success: boolean; + paths: string[]; + startDelayMsByPath?: Record; + error?: string; + }>; + setRecordingState: (recording: boolean) => Promise; + getCursorTelemetry: (videoPath?: string) => Promise<{ + success: boolean; + samples: CursorTelemetryPoint[]; + message?: string; + error?: string; + }>; + setCursorTelemetry: ( + videoPath: string | undefined, + samples: CursorTelemetryPoint[], + ) => Promise<{ + success: boolean; + samples: CursorTelemetryPoint[]; + message?: string; + error?: string; + }>; + getSystemCursorAssets: () => Promise<{ + success: boolean; + cursors: Record; + error?: string; + }>; + onStopRecordingFromTray: (callback: () => void) => () => void; + onRecordingStateChanged: ( + callback: (state: { recording: boolean; sourceName: string }) => void, + ) => () => void; + onRecordingSessionChanged: ( + callback: (session: RendererRecordingSessionData | null) => void, + ) => () => void; + onRecordingInterrupted: ( + callback: (state: { reason: string; message: string }) => void, + ) => () => void; + onCursorStateChanged: ( + callback: (state: { cursorType: CursorTelemetryPoint["cursorType"] }) => void, + ) => () => void; + openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>; + getAccessibilityPermissionStatus: () => Promise<{ + success: boolean; + trusted: boolean; + prompted: boolean; + error?: string; + }>; + requestAccessibilityPermission: () => Promise<{ + success: boolean; + trusted: boolean; + prompted: boolean; + error?: string; + }>; + getScreenRecordingPermissionStatus: () => Promise<{ + success: boolean; + status: string; + error?: string; + }>; + openScreenRecordingPreferences: () => Promise<{ success: boolean; error?: string }>; + openAccessibilityPreferences: () => Promise<{ success: boolean; error?: string }>; + saveExportedVideo: ( + videoData: ArrayBuffer, + fileName: string, + captionSidecar?: { + format: "srt" | "vtt" | "both"; + cues: Array<{ + startMs: number; + endMs: number; + text: string; + }>; + }, + ) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>; + writeExportedVideoToPath: ( + videoData: ArrayBuffer, + outputPath: string, + captionSidecar?: { + format: "srt" | "vtt" | "both"; + cues: Array<{ + startMs: number; + endMs: number; + text: string; + }>; + }, + ) => Promise<{ + success: boolean; + path?: string; + message?: string; + error?: string; + canceled?: boolean; + }>; + openVideoFilePicker: (options?: { includeProjects?: boolean }) => Promise<{ + success: boolean; + kind?: "media" | "project"; + path?: string; + project?: unknown; + extension?: string; + message?: string; + canceled?: boolean; + error?: string; + }>; + openAudioFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>; + openWhisperExecutablePicker: () => Promise<{ + success: boolean; + path?: string; + canceled?: boolean; + error?: string; + }>; + openWhisperModelPicker: () => Promise<{ + success: boolean; + path?: string; + canceled?: boolean; + error?: string; + }>; + getWhisperSmallModelStatus: () => Promise<{ + success: boolean; + exists: boolean; + path?: string | null; + error?: string; + }>; + downloadWhisperSmallModel: () => Promise<{ + success: boolean; + path?: string; + alreadyDownloaded?: boolean; + error?: string; + }>; + deleteWhisperSmallModel: () => Promise<{ success: boolean; error?: string }>; + onWhisperSmallModelDownloadProgress: ( + callback: (state: { + status: "idle" | "downloading" | "downloaded" | "error"; + progress: number; + path?: string | null; + error?: string; + }) => void, + ) => () => void; + generateAutoCaptions: (options: { + videoPath: string; + whisperExecutablePath?: string; + whisperModelPath: string; + language?: string; + }) => Promise<{ + success: boolean; + cues?: AutoCaptionCue[]; + message?: string; + error?: string; + }>; + setCurrentVideoPath: ( + path: string, + options?: { + preserveProjectPath?: boolean; + hideOverlayCursorByDefault?: boolean; + }, + ) => Promise<{ success: boolean; webcamPath: string | null }>; + setCurrentRecordingSession: ( + session: { + videoPath: string; + webcamPath?: string | null; + timeOffsetMs?: number; + hideOverlayCursorByDefault?: boolean; + }, + options?: { preserveProjectPath?: boolean }, + ) => Promise<{ success: boolean }>; + getCurrentRecordingSession: () => Promise<{ + success: boolean; + session?: { + videoPath: string; + webcamPath?: string | null; + timeOffsetMs?: number; + hideOverlayCursorByDefault?: boolean; + }; + }>; + getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>; + clearCurrentVideoPath: () => Promise<{ success: boolean }>; + getRecordingThumbnail: ( + filePath: string, + ) => Promise>; + finishRecordingImport: ( + keepPath: string, + commit?: boolean, + ) => Promise<{ success: boolean; error?: string }>; + cancelRecordingImport: () => Promise<{ success: boolean }>; + getProjectPreview: ( + projectPath: string, + ) => Promise< + import("../src/types/recordingLibrary").LibraryResult< + import("../src/types/projectPreview").ProjectPreviewData + > + >; + listRecordings: ( + includeSources?: boolean, + ) => Promise< + import("../src/types/recordingLibrary").LibraryResult< + import("../src/types/recordingLibrary").RecordingLibraryEntry[] + > + >; + setRecordingsRemoved: ( + paths: string[], + removed: boolean, + ) => Promise>; + importRecording: ( + currentPath: string, + recordingPath: string, + webcam?: import("../src/types/recordingLibrary").RecordingWebcamSource, + ) => Promise< + import("../src/types/recordingLibrary").LibraryResult< + import("../src/types/recordingLibrary").RecordingImportResult + > + >; + deleteRecordingFile: (filePath: string) => Promise<{ success: boolean; error?: string }>; + getLocalMediaUrl: ( + filePath: string, + ) => Promise<{ success: true; url: string } | { success: false }>; + saveProjectFile: ( + projectData: unknown, + suggestedName?: string, + existingProjectPath?: string, + thumbnailDataUrl?: string | null, + ) => Promise<{ + success: boolean; + path?: string; + projectId?: string; + message?: string; + canceled?: boolean; + error?: string; + }>; + saveProjectFileNamed: ( + projectData: unknown, + projectName: string, + thumbnailDataUrl?: string | null, + mode?: "rename" | "copy", + ) => Promise<{ + success: boolean; + path?: string; + projectId?: string; + message?: string; + canceled?: boolean; + error?: string; + }>; + loadProjectFile: () => Promise<{ + success: boolean; + path?: string; + project?: unknown; + message?: string; + canceled?: boolean; + error?: string; + }>; + loadCurrentProjectFile: () => Promise<{ + success: boolean; + path?: string; + project?: unknown; + message?: string; + canceled?: boolean; + error?: string; + }>; + getProjectsDirectory: () => Promise<{ + success: boolean; + path?: string; + error?: string; + }>; + showRecordingPermissions: () => Promise; + onRecordingPermissionsRequested: (callback: () => void) => () => void; + showRecordingHud: () => Promise; + createProjectFile: ( + data: unknown, + thumbnail?: string | null, + ) => Promise<{ + success: boolean; + path?: string; + projectId?: string; + message?: string; + canceled?: boolean; + }>; + renameLibraryProject: ( + path: string, + name: string, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + trashProjectFiles: ( + paths: string[], + ) => Promise<{ success: boolean; deleted: string[]; errors: string[] }>; + onProjectThumbnailReady: ( + callback: (ready: { path: string; thumbnailPath: string; updatedAt: number }) => void, + ) => () => void; + listProjectFiles: () => Promise<{ + success: boolean; + projectsDir?: string | null; + entries: Array<{ + path: string; + name: string; + createdAt?: number; + updatedAt: number; + thumbnailPath: string | null; + isCurrent: boolean; + isInProjectsDirectory: boolean; + }>; + error?: string; + }>; + openProjectFileAtPath: (filePath: string) => Promise<{ + success: boolean; + path?: string; + project?: unknown; + message?: string; + canceled?: boolean; + error?: string; + }>; + openProjectsDirectory: () => Promise<{ + success: boolean; + path?: string; + message?: string; + error?: string; + }>; + installDownloadedUpdate: () => Promise<{ success: boolean }>; + downloadAvailableUpdate: ( + installAfterDownload?: boolean, + ) => Promise<{ success: boolean; message?: string }>; + deferDownloadedUpdate: (delayMs?: number) => Promise<{ + success: boolean; + message?: string; + }>; + dismissUpdateToast: () => Promise<{ success: boolean }>; + skipUpdateVersion: () => Promise<{ success: boolean; message?: string }>; + getCurrentUpdateToastPayload: () => Promise; + getUpdateStatusSummary: () => Promise; + getExperimentalUpdatesEnabled: () => Promise; + setExperimentalUpdatesEnabled: (enabled: boolean) => Promise<{ + success: boolean; + enabled: boolean; + error?: string; + }>; + previewUpdateToast: () => Promise<{ success: boolean }>; + checkForAppUpdates: () => Promise<{ success: boolean; logPath: string }>; + onUpdateToastStateChanged: ( + callback: (payload: UpdateToastState | null) => void, + ) => () => void; + onUpdateReadyToast: ( + callback: (payload: { + version: string; + detail: string; + delayMs: number; + isPreview?: boolean; + }) => void, + ) => () => void; + onMenuLoadProject: (callback: () => void) => () => void; + onMenuSaveProject: (callback: () => void) => () => void; + onMenuSaveProjectAs: (callback: () => void) => () => void; + getWindowChrome: () => Promise<{ trafficLightsVisible: boolean }>; + onWindowChromeChanged: ( + callback: (chrome: { trafficLightsVisible: boolean }) => void, + ) => () => void; + getPlatform: () => Promise; + isWindowFullscreen: () => Promise; + onWindowFullscreenChanged: (callback: (isFullscreen: boolean) => void) => () => void; + getLinuxWindowSystem: () => Promise<"wayland" | "x11" | null>; + ackAuthCallbackUrl: (url: string) => Promise; + getPendingAuthCallbackUrl: () => Promise; + onAuthCallbackUrl: (callback: (url: string) => void) => () => void; + revealInFolder: ( + filePath: string, + ) => Promise<{ success: boolean; error?: string; message?: string }>; + cloudShareUpload: (input: { + filePath: string; + endpoint: string; + token?: string; + title?: string; + notes?: string; + uploadId?: string; + }) => Promise<{ + success: boolean; + uploadId?: string; + shareUrl?: string; + canceled?: boolean; + error?: string; + }>; + cloudShareManage: (input: { + endpoint: string; + token: string; + action: "list" | "delete"; + shareCode?: string; + }) => Promise<{ + success: boolean; + error?: string; + videos?: Array<{ + code: string; + createdAt?: string; + title: string; + size: number; + ready: boolean; + url: string; + }>; + }>; + cloudShareCancel: (uploadId: string) => Promise<{ success: boolean }>; + onCloudShareProgress: ( + callback: (progress: { + uploadId: string; + uploadedBytes: number; + totalBytes: number; + }) => void, + ) => () => void; + openRecordingsFolder: () => Promise<{ success: boolean; error?: string; message?: string }>; + getRecordingsDirectory: () => Promise<{ + success: boolean; + path: string; + isDefault: boolean; + error?: string; + }>; + chooseRecordingsDirectory: () => Promise<{ + success: boolean; + canceled?: boolean; + path?: string; + isDefault?: boolean; + message?: string; + error?: string; + }>; + getShortcuts: () => Promise | null>; + saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>; + getEditorStartupView: () => "home" | "editor"; + getAppSetting: (key: string) => unknown; + setAppSetting: (key: string, value: unknown) => boolean; + setHasUnsavedChanges: (hasChanges: boolean) => void; + onRequestSaveBeforeClose: (callback: () => Promise) => () => void; + isNativeWindowsCaptureAvailable: () => Promise<{ available: boolean }>; + muxNativeWindowsRecording: (expectedDurationMs?: number) => Promise<{ + success: boolean; + path?: string; + message?: string; + error?: string; + }>; + /** Returns the app version from package.json */ + getAppVersion: () => Promise; + /** Returns the configured remote announcement feed, or null when unavailable. */ + getAnnouncements: () => Promise; + /** Hide the OS cursor before browser capture starts. */ + hideOsCursor: () => Promise<{ success: boolean }>; + /** Recording preferences (mic, system audio, webcam) */ + getRecordingPreferences: () => Promise<{ + success: boolean; + microphoneEnabled: boolean; + microphoneDeviceId?: string; + systemAudioEnabled: boolean; + webcamEnabled: boolean; + webcamDeviceId?: string; + }>; + getRecordingAudioLabConfig: () => Promise<{ + browserMicrophoneProfile: string; + requestedBrowserMicrophoneProfile: string | null; + }>; + setRecordingPreferences: (prefs: { + microphoneEnabled?: boolean; + microphoneDeviceId?: string; + systemAudioEnabled?: boolean; + webcamEnabled?: boolean; + webcamDeviceId?: string; + }) => Promise<{ success: boolean; error?: string }>; + /** Countdown timer before recording */ + getCountdownDelay: () => Promise<{ success: boolean; delay: number }>; + setCountdownDelay: (delay: number) => Promise<{ success: boolean; error?: string }>; + finishRecordingStartup: () => Promise; + startCountdown: (seconds: number) => Promise<{ success: boolean; cancelled?: boolean }>; + cancelCountdown: () => Promise<{ success: boolean }>; + getActiveCountdown: () => Promise<{ success: boolean; seconds: number | null }>; + onCountdownTick: (callback: (seconds: number) => void) => () => void; + }; +} + +/** A rectangle in global display points (top-left origin). */ +interface CaptureArea { + x: number; + y: number; + width: number; + height: number; +} + +/** A window the capture picker can highlight, framed in global display points. */ +interface CapturePickerWindow extends CaptureArea { + /** The window's source id, `window::0`. */ + id: string; + appName: string; + title: string; + display_id?: string; +} + +/** + * What the capture picker reports when the user confirms: an area they drew, a + * window they clicked, or a whole screen. `record` starts recording right away. + */ +type CapturePick = + | (CaptureArea & { kind: "area"; displayId: number; record: boolean }) + | { kind: "window"; windowId: string; displayId: number; record: boolean } + | { kind: "screen"; displayId: number; record: boolean }; + +interface ProcessedDesktopSource { + id: string; + name: string; + display_id: string; + thumbnail: string | null; + appIcon: string | null; + originalName?: string; + sourceType?: "screen" | "window" | "area"; + appName?: string; + windowTitle?: string; + /** For area sources, the recorded part of the display. */ + area?: CaptureArea; +} + +interface CursorTelemetryPoint { + timeMs: number; + cx: number; + cy: number; + pressure?: number; + interactionType?: + | "move" + | "click" + | "double-click" + | "right-click" + | "middle-click" + | "mouseup"; + cursorType?: + | "arrow" + | "text" + | "pointer" + | "crosshair" + | "open-hand" + | "closed-hand" + | "resize-ew" + | "resize-ns" + | "not-allowed"; +} + +interface SystemCursorAsset { + dataUrl: string; + hotspotX: number; + hotspotY: number; + width: number; + height: number; +} + +interface AutoCaptionCue { + id: string; + startMs: number; + endMs: number; + text: string; + words?: Array<{ + text: string; + startMs: number; + endMs: number; + leadingSpace?: boolean; + }>; +} diff --git a/electron/gpuSwitches.test.ts b/electron/gpuSwitches.test.ts new file mode 100644 index 000000000..4cbb1cdf4 --- /dev/null +++ b/electron/gpuSwitches.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; + +import { getGpuSwitches, shouldForceLinuxEgl } from "./gpuSwitches"; + +describe("shouldForceLinuxEgl", () => { + it("does not force EGL in a Wayland session", () => { + expect( + shouldForceLinuxEgl({ + XDG_SESSION_TYPE: "wayland", + WAYLAND_DISPLAY: "wayland-0", + }), + ).toBe(false); + }); + + it("does not force EGL when Wayland is explicitly requested via Ozone", () => { + expect( + shouldForceLinuxEgl({ + OZONE_PLATFORM: "wayland", + XDG_SESSION_TYPE: "x11", + }), + ).toBe(false); + }); + + it("falls back to Electron's ozone hint when OZONE_PLATFORM is invalid", () => { + expect( + shouldForceLinuxEgl({ + OZONE_PLATFORM: "auto", + ELECTRON_OZONE_PLATFORM_HINT: "wayland", + XDG_SESSION_TYPE: "x11", + }), + ).toBe(false); + }); + + it("forces EGL in an X11 session", () => { + expect(shouldForceLinuxEgl({ XDG_SESSION_TYPE: "x11" })).toBe(true); + }); + + it("forces EGL when x11 is explicitly requested via Electron's ozone hint", () => { + expect( + shouldForceLinuxEgl({ + ELECTRON_OZONE_PLATFORM_HINT: "x11", + WAYLAND_DISPLAY: "wayland-0", + }), + ).toBe(true); + }); +}); + +describe("getGpuSwitches", () => { + it("returns the Linux VAAPI workaround without forcing EGL on Wayland", () => { + expect( + getGpuSwitches("linux", { + XDG_SESSION_TYPE: "wayland", + WAYLAND_DISPLAY: "wayland-0", + }), + ).toEqual({ + useGl: undefined, + disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], + }); + }); + + it("returns the X11 EGL workaround on Linux X11", () => { + expect(getGpuSwitches("linux", { XDG_SESSION_TYPE: "x11" })).toEqual({ + useGl: "egl", + disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], + }); + }); +}); diff --git a/electron/gpuSwitches.ts b/electron/gpuSwitches.ts new file mode 100644 index 000000000..7b7c81ee8 --- /dev/null +++ b/electron/gpuSwitches.ts @@ -0,0 +1,66 @@ +export interface GpuSwitches { + useAngle?: string; + useGl?: string; + disableFeatures?: string[]; +} + +function normalizeLinuxWindowSystem(value: string | undefined): "wayland" | "x11" | null { + const normalized = value?.trim().toLowerCase(); + if (normalized === "wayland" || normalized === "x11") { + return normalized; + } + + return null; +} + +function getForcedLinuxWindowSystem(env: NodeJS.ProcessEnv): "wayland" | "x11" | null { + return ( + normalizeLinuxWindowSystem(env.OZONE_PLATFORM) ?? + normalizeLinuxWindowSystem(env.ELECTRON_OZONE_PLATFORM_HINT) + ); +} + +export function shouldForceLinuxEgl(env: NodeJS.ProcessEnv): boolean { + const forcedWindowSystem = getForcedLinuxWindowSystem(env); + if (forcedWindowSystem === "wayland") { + return false; + } + if (forcedWindowSystem === "x11") { + return true; + } + + const sessionType = env.XDG_SESSION_TYPE?.toLowerCase(); + if (sessionType === "wayland") { + return false; + } + if (sessionType === "x11") { + return true; + } + + return !env.WAYLAND_DISPLAY; +} + +export function getGpuSwitches( + platform: NodeJS.Platform, + env: NodeJS.ProcessEnv = process.env, +): GpuSwitches { + if (platform === "darwin") { + return { + useAngle: "metal", + disableFeatures: ["MacCatapLoopbackAudioForScreenShare"], + }; + } + + if (platform === "win32") { + return { useAngle: "d3d11" }; + } + + if (platform === "linux") { + return { + useGl: shouldForceLinuxEgl(env) ? "egl" : undefined, + disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], + }; + } + + return {}; +} diff --git a/electron/hudCaptureProtection.test.ts b/electron/hudCaptureProtection.test.ts new file mode 100644 index 000000000..28f807222 --- /dev/null +++ b/electron/hudCaptureProtection.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { + getHudCaptureExcludedProcessIds, + shouldProtectHudCapture, + supportsHudCaptureProtection, +} from "../src/lib/hudCaptureProtection"; + +describe("HUD capture protection lifecycle", () => { + it("uses window protection on Windows and macOS only", () => { + expect(supportsHudCaptureProtection("win32")).toBe(true); + expect(supportsHudCaptureProtection("darwin")).toBe(true); + expect(supportsHudCaptureProtection("linux")).toBe(false); + }); + + it("only builds a macOS process exclusion when protection is enabled", () => { + expect(getHudCaptureExcludedProcessIds("darwin", true, 734)).toEqual([734]); + expect(getHudCaptureExcludedProcessIds("darwin", false, 734)).toEqual([]); + expect(getHudCaptureExcludedProcessIds("win32", true, 734)).toEqual([]); + expect(getHudCaptureExcludedProcessIds("linux", true, 734)).toEqual([]); + }); +}); + +it("protects initial capture frames while keeping idle and failed starts visible", () => { + expect(shouldProtectHudCapture(true, false, false)).toBe(false); + expect(shouldProtectHudCapture(true, false, true)).toBe(true); + expect(shouldProtectHudCapture(true, true, false)).toBe(true); + expect(shouldProtectHudCapture(true, false, false)).toBe(false); + expect(shouldProtectHudCapture(false, true, true)).toBe(false); +}); diff --git a/electron/hudEditorMode.test.ts b/electron/hudEditorMode.test.ts new file mode 100644 index 000000000..b8d886845 --- /dev/null +++ b/electron/hudEditorMode.test.ts @@ -0,0 +1,13 @@ +import { expect, it } from "vitest"; +import { isHudInEditorMode } from "./hudEditorMode"; +it("allows the webcam when New prepares a recording despite an existing editor", () => { + expect(isHudInEditorMode(1, false, false)).toBe(true); + expect(isHudInEditorMode(1, true, false)).toBe(false); + expect(isHudInEditorMode(1, true, true)).toBe(false); + // Cancelling leaves the HUD ready to retry, including its webcam preview. + expect(isHudInEditorMode(1, true, false)).toBe(false); + // Returning Home stops an idle camera, but does not stop an active capture preview. + expect(isHudInEditorMode(1, false, false)).toBe(true); + expect(isHudInEditorMode(1, false, true)).toBe(false); + expect(isHudInEditorMode(0, false, false)).toBe(false); +}); diff --git a/electron/hudEditorMode.ts b/electron/hudEditorMode.ts new file mode 100644 index 000000000..f87ddcd6e --- /dev/null +++ b/electron/hudEditorMode.ts @@ -0,0 +1,8 @@ +/** An editor may stay open behind the HUD while a new capture is prepared. */ +export function isHudInEditorMode( + editorCount: number, + preparingRecording: boolean, + recording: boolean, +) { + return editorCount > 0 && !preparingRecording && !recording; +} diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts new file mode 100644 index 000000000..db21e92bd --- /dev/null +++ b/electron/hudOverlayBounds.test.ts @@ -0,0 +1,188 @@ +import { describe, expect, it } from "vitest"; + +import { + getHudOverlayWindowBounds, + resizeHudOverlayFallbackBounds, + shouldExpandHudOverlayFallback, +} from "./hudOverlayBounds"; + +describe("getHudOverlayWindowBounds", () => { + const workArea = { + x: 120, + y: 40, + width: 1920, + height: 1040, + }; + + it("uses the full work area when mouse passthrough is supported", () => { + expect(getHudOverlayWindowBounds(workArea, true)).toEqual(workArea); + }); + + it("uses a bottom-centered compact fallback when mouse passthrough is unavailable", () => { + expect(getHudOverlayWindowBounds(workArea, false)).toEqual({ + x: 650, + y: 920, + width: 860, + height: 160, + }); + }); + + it("expands the non-passthrough fallback for HUD menus and hover interaction", () => { + expect(getHudOverlayWindowBounds(workArea, false, true)).toEqual({ + x: 650, + y: 540, + width: 860, + height: 540, + }); + }); + + it("keeps the compact fallback inside small displays", () => { + expect( + getHudOverlayWindowBounds( + { + x: -100, + y: 20, + width: 640, + height: 420, + }, + false, + ), + ).toEqual({ + x: -100, + y: 280, + width: 640, + height: 160, + }); + }); + + it("fits the expanded fallback inside small displays", () => { + expect( + getHudOverlayWindowBounds( + { + x: -100, + y: 20, + width: 640, + height: 420, + }, + false, + true, + ), + ).toEqual({ + x: -100, + y: 20, + width: 640, + height: 420, + }); + }); +}); + +describe("resizeHudOverlayFallbackBounds", () => { + const workArea = { + x: 0, + y: 0, + width: 1920, + height: 1080, + }; + + it("preserves the dragged bottom edge when expanding", () => { + expect( + resizeHudOverlayFallbackBounds( + workArea, + { + x: 420, + y: 700, + width: 860, + height: 160, + }, + true, + ), + ).toEqual({ + x: 420, + y: 320, + width: 860, + height: 540, + }); + }); + + it("preserves the dragged bottom edge when compacting", () => { + expect( + resizeHudOverlayFallbackBounds( + workArea, + { + x: 420, + y: 320, + width: 860, + height: 540, + }, + false, + ), + ).toEqual({ + x: 420, + y: 700, + width: 860, + height: 160, + }); + }); + + it("keeps resized fallback bounds inside the display work area", () => { + expect( + resizeHudOverlayFallbackBounds( + workArea, + { + x: 1500, + y: 900, + width: 860, + height: 160, + }, + true, + ), + ).toEqual({ + x: 1060, + y: 520, + width: 860, + height: 540, + }); + }); +}); + +describe("shouldExpandHudOverlayFallback", () => { + it("expands while recording only when the floating webcam preview is visible", () => { + expect( + shouldExpandHudOverlayFallback({ + fallbackExpanded: false, + recordingActive: true, + webcamPreviewVisible: true, + }), + ).toBe(true); + }); + + it("keeps the compact recording fallback when there is no webcam preview", () => { + expect( + shouldExpandHudOverlayFallback({ + fallbackExpanded: false, + recordingActive: true, + webcamPreviewVisible: false, + }), + ).toBe(false); + }); + + it("preserves manual fallback expansion outside recording", () => { + expect( + shouldExpandHudOverlayFallback({ + fallbackExpanded: true, + recordingActive: false, + webcamPreviewVisible: false, + }), + ).toBe(true); + }); + + it("does not expand for webcam visibility outside recording", () => { + expect( + shouldExpandHudOverlayFallback({ + fallbackExpanded: false, + recordingActive: false, + webcamPreviewVisible: true, + }), + ).toBe(false); + }); +}); diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts new file mode 100644 index 000000000..8c51b88c7 --- /dev/null +++ b/electron/hudOverlayBounds.ts @@ -0,0 +1,67 @@ +export interface HudOverlayWorkArea { + x: number; + y: number; + width: number; + height: number; +} + +const NON_PASSTHROUGH_HUD_WIDTH_DIP = 860; +const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 160; +const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 540; + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export function getHudOverlayWindowBounds( + workArea: HudOverlayWorkArea, + mousePassthroughSupported: boolean, + fallbackExpanded = false, +): HudOverlayWorkArea { + if (mousePassthroughSupported) { + return { ...workArea }; + } + + const width = Math.min(workArea.width, NON_PASSTHROUGH_HUD_WIDTH_DIP); + const height = Math.min( + workArea.height, + fallbackExpanded + ? NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP + : NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP, + ); + + return { + x: Math.round(workArea.x + (workArea.width - width) / 2), + y: Math.round(workArea.y + workArea.height - height), + width, + height, + }; +} + +export function shouldExpandHudOverlayFallback({ + fallbackExpanded, + recordingActive, + webcamPreviewVisible, +}: { + fallbackExpanded: boolean; + recordingActive: boolean; + webcamPreviewVisible: boolean; +}): boolean { + return fallbackExpanded || (recordingActive && webcamPreviewVisible); +} + +export function resizeHudOverlayFallbackBounds( + workArea: HudOverlayWorkArea, + currentBounds: HudOverlayWorkArea, + fallbackExpanded: boolean, +): HudOverlayWorkArea { + const nextBounds = getHudOverlayWindowBounds(workArea, false, fallbackExpanded); + const maxX = workArea.x + workArea.width - nextBounds.width; + const maxY = workArea.y + workArea.height - nextBounds.height; + + return { + ...nextBounds, + x: clamp(currentBounds.x, workArea.x, maxX), + y: clamp(currentBounds.y + currentBounds.height - nextBounds.height, workArea.y, maxY), + }; +} diff --git a/electron/hudOverlayWindowOptions.test.ts b/electron/hudOverlayWindowOptions.test.ts new file mode 100644 index 000000000..08f8c6531 --- /dev/null +++ b/electron/hudOverlayWindowOptions.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { getHudOverlayTaskbarOptions } from "./hudOverlayWindowOptions"; + +describe("getHudOverlayTaskbarOptions", () => { + it("keeps a focusable HUD in the Windows taskbar", () => { + expect(getHudOverlayTaskbarOptions("win32")).toEqual({ + skipTaskbar: false, + focusable: true, + }); + }); + + it.each([ + "darwin", + "linux", + ] as const)("keeps the HUD non-focusable and out of the taskbar on %s", (platform) => { + expect(getHudOverlayTaskbarOptions(platform)).toEqual({ + skipTaskbar: true, + focusable: false, + }); + }); +}); diff --git a/electron/hudOverlayWindowOptions.ts b/electron/hudOverlayWindowOptions.ts new file mode 100644 index 000000000..27fcd2dea --- /dev/null +++ b/electron/hudOverlayWindowOptions.ts @@ -0,0 +1,12 @@ +export interface HudOverlayTaskbarOptions { + skipTaskbar: boolean; + focusable: boolean; +} + +export function getHudOverlayTaskbarOptions(platform: NodeJS.Platform): HudOverlayTaskbarOptions { + const showInWindowsTaskbar = platform === "win32"; + return { + skipTaskbar: !showInWindowsTaskbar, + focusable: showInWindowsTaskbar, + }; +} diff --git a/electron/ipc/captions/audioCandidates.test.ts b/electron/ipc/captions/audioCandidates.test.ts new file mode 100644 index 000000000..c08b49ff1 --- /dev/null +++ b/electron/ipc/captions/audioCandidates.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { getCaptionCompanionAudioCandidates } from "./audioCandidates"; + +describe("getCaptionCompanionAudioCandidates", () => { + it("prefers microphone audio and includes system audio when both sidecars exist", () => { + expect( + getCaptionCompanionAudioCandidates([ + { + platform: "win", + micPath: "recording.mic.wav", + systemPath: "recording.system.wav", + usablePaths: ["recording.system.wav", "recording.mic.wav"], + }, + ]), + ).toEqual([ + { path: "recording.mic.wav", label: "microphone audio sidecar" }, + { path: "recording.system.wav", label: "system audio sidecar" }, + ]); + }); + + it("omits companion paths that were not found on disk", () => { + expect( + getCaptionCompanionAudioCandidates([ + { + platform: "win", + micPath: "recording.mic.wav", + systemPath: "recording.system.wav", + usablePaths: ["recording.mic.wav"], + }, + ]), + ).toEqual([{ path: "recording.mic.wav", label: "microphone audio sidecar" }]); + }); +}); diff --git a/electron/ipc/captions/audioCandidates.ts b/electron/ipc/captions/audioCandidates.ts new file mode 100644 index 000000000..33e62d2e1 --- /dev/null +++ b/electron/ipc/captions/audioCandidates.ts @@ -0,0 +1,21 @@ +import type { CompanionAudioCandidate } from "../types"; + +export type CaptionAudioCandidate = { + path: string; + label: string; +}; + +export function getCaptionCompanionAudioCandidates( + companions: CompanionAudioCandidate[], +): CaptionAudioCandidate[] { + return companions.flatMap((companion) => { + const candidates: CaptionAudioCandidate[] = []; + if (companion.usablePaths.includes(companion.micPath)) { + candidates.push({ path: companion.micPath, label: "microphone audio sidecar" }); + } + if (companion.usablePaths.includes(companion.systemPath)) { + candidates.push({ path: companion.systemPath, label: "system audio sidecar" }); + } + return candidates; + }); +} diff --git a/electron/ipc/captions/generate.test.ts b/electron/ipc/captions/generate.test.ts new file mode 100644 index 000000000..9844fecc8 --- /dev/null +++ b/electron/ipc/captions/generate.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { isMissingWindowsWhisperRuntimeDependency } from "./runtimeErrors"; + +describe("isMissingWindowsWhisperRuntimeDependency", () => { + it("recognizes the signed and unsigned STATUS_DLL_NOT_FOUND exit codes on Windows", () => { + if (process.platform !== "win32") return; + + expect(isMissingWindowsWhisperRuntimeDependency({ code: -1073741515 })).toBe(true); + expect(isMissingWindowsWhisperRuntimeDependency({ code: 3221225781 })).toBe(true); + }); + + it("does not classify ordinary Whisper failures as missing runtimes", () => { + expect(isMissingWindowsWhisperRuntimeDependency({ code: 1 })).toBe(false); + expect(isMissingWindowsWhisperRuntimeDependency(new Error("bad model"))).toBe(false); + }); +}); diff --git a/electron/ipc/captions/generate.ts b/electron/ipc/captions/generate.ts new file mode 100644 index 000000000..1e1184348 --- /dev/null +++ b/electron/ipc/captions/generate.ts @@ -0,0 +1,367 @@ +import { execFile, spawnSync } from "node:child_process"; +import { constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { app } from "electron"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; +import { getBundledWhisperExecutableCandidates } from "../paths/binaries"; +import { resolveRecordingSession } from "../project/session"; +import { + getCompanionAudioStartDelayMs, + getUsableCompanionAudioCandidates, +} from "../recording/diagnostics"; +import { normalizeVideoSourcePath } from "../utils"; +import { type CaptionAudioCandidate, getCaptionCompanionAudioCandidates } from "./audioCandidates"; +import { shouldRetryWhisperWithoutJson } from "./parser"; +import { readWhisperCaptionOutput } from "./output"; +import { isMissingWindowsWhisperRuntimeDependency } from "./runtimeErrors"; +import { mergeCaptionSources } from "./mergeSources"; +import { segmentCuesIntoPhrases } from "./segment"; +import { + parseSilenceIntervals, + SILENCE_DETECT_MIN_S, + SILENCE_NOISE_DB, + type SilenceInterval, +} from "./silence"; + +const execFileAsync = promisify(execFile); + +class NoCaptionAudioError extends Error {} + +async function executeWhisper(whisperExecutablePath: string, args: string[]) { + try { + await execFileAsync(whisperExecutablePath, args, { + timeout: 30 * 60 * 1000, + maxBuffer: 20 * 1024 * 1024, + }); + } catch (error) { + if (isMissingWindowsWhisperRuntimeDependency(error)) { + throw new Error( + "Whisper could not start because the Microsoft Visual C++ x64 Redistributable is missing. Install it from https://aka.ms/vc14/vc_redist.x64.exe, then restart Recordly.", + ); + } + throw error; + } +} + +export async function ensureReadableFile(filePath: string, options?: { executable?: boolean }) { + await fs.access(filePath, fsConstants.R_OK); + if (options?.executable) { + try { + await fs.access(filePath, fsConstants.X_OK); + } catch { + throw new Error("The selected Whisper executable is not marked as executable."); + } + } +} + +export async function isExecutableFile(filePath: string) { + try { + await fs.access(filePath, fsConstants.R_OK | fsConstants.X_OK); + return true; + } catch { + return false; + } +} + +export async function resolveWhisperExecutablePath(preferredPath?: string | null) { + const candidatePaths = [ + preferredPath?.trim() || null, + ...getBundledWhisperExecutableCandidates(), + process.env["WHISPER_CPP_PATH"]?.trim() || null, + process.platform === "darwin" ? "/opt/homebrew/bin/whisper-cli" : null, + process.platform === "darwin" ? "/usr/local/bin/whisper-cli" : null, + process.platform === "darwin" ? "/opt/homebrew/bin/whisper-cpp" : null, + process.platform === "darwin" ? "/usr/local/bin/whisper-cpp" : null, + ].filter((value): value is string => Boolean(value)); + + for (const candidate of candidatePaths) { + const normalized = path.resolve(candidate); + if (await isExecutableFile(normalized)) { + return normalized; + } + } + + const pathCommand = process.platform === "win32" ? "where" : "which"; + const binaryNames = + process.platform === "win32" + ? ["whisper-cli.exe", "whisper.exe", "main.exe"] + : ["whisper-cli", "whisper-cpp", "whisper", "main"]; + + for (const binaryName of binaryNames) { + const result = spawnSync(pathCommand, [binaryName], { encoding: "utf-8" }); + if (result.status === 0) { + const resolvedPath = result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .find(Boolean); + + if (resolvedPath && (await isExecutableFile(resolvedPath))) { + return resolvedPath; + } + } + } + + throw new Error( + `No Whisper runtime was found for ${process.platform}/${process.arch}. ` + + "This Recordly build is missing its bundled caption runtime. Reinstall or update Recordly, or select a whisper-cli executable in Caption settings.", + ); +} + +export async function resolveCaptionAudioCandidates(videoPath: string) { + const candidates: Array<{ path: string; label: string }> = []; + const seenPaths = new Set(); + + const pushCandidate = (candidatePath: string | null | undefined, label: string) => { + const normalizedCandidatePath = normalizeVideoSourcePath(candidatePath); + if (!normalizedCandidatePath || seenPaths.has(normalizedCandidatePath)) { + return; + } + + seenPaths.add(normalizedCandidatePath); + candidates.push({ path: normalizedCandidatePath, label }); + }; + + const companionAudio = await getUsableCompanionAudioCandidates(videoPath); + const companions = getCaptionCompanionAudioCandidates(companionAudio); + for (const candidate of companions.filter( + (candidate) => candidate.label === "microphone audio sidecar", + )) { + pushCandidate(candidate.path, candidate.label); + } + pushCandidate(videoPath, "recording"); + for (const candidate of companions) { + pushCandidate(candidate.path, candidate.label); + } + + const requestedRecordingSession = await resolveRecordingSession(videoPath); + pushCandidate(requestedRecordingSession?.webcamPath, "linked webcam recording"); + + return candidates; +} + +export async function extractCaptionAudioSource(options: { + videoPath: string; + ffmpegPath: string; + wavPath: string; + candidates?: CaptionAudioCandidate[]; +}) { + const candidates = + options.candidates ?? (await resolveCaptionAudioCandidates(options.videoPath)); + const attemptedCandidates: Array<{ + path: string; + label: string; + readable: boolean; + extractedAudio: boolean; + error?: string; + }> = []; + + for (const candidate of candidates) { + try { + await ensureReadableFile(candidate.path); + const delayMs = candidate.label.endsWith("audio sidecar") + ? ((await getCompanionAudioStartDelayMs(candidate.path)) ?? 0) + : 0; + await execFileAsync( + options.ffmpegPath, + [ + "-y", + "-i", + candidate.path, + "-map", + "0:a:0", + "-vn", + ...(delayMs > 0 ? ["-af", `adelay=${delayMs}:all=1`] : []), + "-ac", + "1", + "-ar", + "16000", + "-c:a", + "pcm_s16le", + options.wavPath, + ], + { timeout: 5 * 60 * 1000, maxBuffer: 20 * 1024 * 1024 }, + ); + attemptedCandidates.push({ ...candidate, readable: true, extractedAudio: true }); + return candidate; + } catch (error) { + attemptedCandidates.push({ + ...candidate, + readable: true, + extractedAudio: false, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + console.warn( + "[auto-captions] No audio source candidate could be extracted:", + attemptedCandidates, + ); + + throw new NoCaptionAudioError( + "No audio was found to transcribe in the saved recording file. Captions need an audio track. If this recording should have contained sound, the recording was saved without an audio stream.", + ); +} + +export async function detectSilenceIntervals(options: { + ffmpegPath: string; + wavPath: string; +}): Promise { + // ffmpeg writes silencedetect results to stderr; the null muxer just runs the filter. + const { stderr } = await execFileAsync( + options.ffmpegPath, + [ + "-hide_banner", + "-nostats", + "-i", + options.wavPath, + "-af", + `silencedetect=noise=${SILENCE_NOISE_DB}dB:d=${SILENCE_DETECT_MIN_S}`, + "-f", + "null", + "-", + ], + { timeout: 5 * 60 * 1000, maxBuffer: 20 * 1024 * 1024 }, + ); + + return parseSilenceIntervals(stderr ?? ""); +} + +async function generateCaptionsForSource(options: { + videoPath: string; + whisperExecutablePath?: string; + whisperModelPath: string; + language?: string; + candidates: CaptionAudioCandidate[]; +}) { + const ffmpegPath = getFfmpegBinaryPath(); + const normalizedVideoPath = normalizeVideoSourcePath(options.videoPath); + if (!normalizedVideoPath) { + throw new Error("Missing source video path."); + } + + const whisperExecutablePath = await resolveWhisperExecutablePath(options.whisperExecutablePath); + const whisperModelPath = path.resolve(options.whisperModelPath); + await ensureReadableFile(whisperExecutablePath, { executable: true }); + await ensureReadableFile(whisperModelPath); + + const tempBase = path.join( + app.getPath("temp"), + `recordly-captions-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + const wavPath = `${tempBase}.wav`; + const outputBase = `${tempBase}-whisper`; + const srtPath = `${outputBase}.srt`; + const jsonPath = `${outputBase}.json`; + + try { + const audioSource = await extractCaptionAudioSource({ + videoPath: normalizedVideoPath, + ffmpegPath, + wavPath, + candidates: options.candidates, + }); + + const language = + options.language && options.language.trim() ? options.language.trim() : "auto"; + const whisperBaseArgs = [ + "-m", + whisperModelPath, + "-f", + wavPath, + "-osrt", + "-of", + outputBase, + "-l", + language, + "-np", + // Do not carry a hallucinated phrase through later decoding windows. + "-mc", + "0", + ]; + + let jsonEnabled = true; + try { + await executeWhisper(whisperExecutablePath, [...whisperBaseArgs, "-ojf"]); + } catch (error) { + if (!shouldRetryWhisperWithoutJson(error)) { + throw error; + } + + jsonEnabled = false; + console.warn( + "[auto-captions] Whisper runtime does not support JSON full output, retrying with SRT only:", + error, + ); + await executeWhisper(whisperExecutablePath, whisperBaseArgs); + } + + const cues = await readWhisperCaptionOutput(outputBase, jsonEnabled); + + // Whisper cues run sentences together and don't break on pauses. Re-segment them + // into one caption per sentence/phrase using Whisper's own word stream (punctuation + // + pauses), backed by ground-truth acoustic silence (ffmpeg `silencedetect`). + // Failure here must not block caption generation — fall back to raw. + let cuesToReturn = cues; + try { + const silences = await detectSilenceIntervals({ ffmpegPath, wavPath }); + // An empty result is a valid resegmentation (e.g. every transcribed word fell + // inside a long detected silence and was dropped as a hallucination), so take it + // as-is. Only a thrown exception should fall back to the raw cues. + cuesToReturn = segmentCuesIntoPhrases(cues, silences); + } catch (error) { + console.warn( + "[auto-captions] Silence-aware re-segmentation failed, using raw cues:", + error, + ); + } + + return { + cues: cuesToReturn, + audioSourceLabel: audioSource.label, + }; + } finally { + await Promise.allSettled([ + fs.rm(wavPath, { force: true }), + fs.rm(srtPath, { force: true }), + fs.rm(jsonPath, { force: true }), + ]); + } +} + +export async function generateAutoCaptionsFromVideo(options: { + videoPath: string; + whisperExecutablePath?: string; + whisperModelPath: string; + language?: string; +}) { + const candidates = await resolveCaptionAudioCandidates(options.videoPath); + const microphone = candidates.filter((source) => source.label === "microphone audio sidecar"); + const system = candidates.filter((source) => source.label === "system audio sidecar"); + const secondary = candidates.filter( + (source) => !microphone.includes(source) && !system.includes(source), + ); + if (microphone.length === 0) { + return generateCaptionsForSource({ ...options, candidates: [...system, ...secondary] }); + } + // Decode independently so simultaneous voices do not confuse recognition. The + // sidecar replaces embedded system audio to avoid transcribing it twice. + const transcribeTrack = async (sources: CaptionAudioCandidate[]) => { + try { + return (await generateCaptionsForSource({ ...options, candidates: sources })).cues; + } catch (error) { + if (!(error instanceof NoCaptionAudioError)) throw error; + return null; + } + }; + const micCues = await transcribeTrack(microphone); + const systemCues = await transcribeTrack([...system, ...secondary]); + if (micCues === null && systemCues === null) + throw new NoCaptionAudioError("No audio could be extracted from the recording."); + return { + cues: mergeCaptionSources(micCues ?? [], systemCues ?? []), + audioSourceLabel: "microphone and system audio", + }; +} diff --git a/electron/ipc/captions/generation.test.ts b/electron/ipc/captions/generation.test.ts new file mode 100644 index 000000000..70ba26588 --- /dev/null +++ b/electron/ipc/captions/generation.test.ts @@ -0,0 +1,166 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + exec: vi.fn(), + readFile: vi.fn(), + rm: vi.fn(), + companions: vi.fn(), + delay: vi.fn(), + session: vi.fn(), +})); +vi.mock("node:child_process", () => ({ + execFile: (...args: unknown[]) => { + const callback = args.pop() as (error: Error | null, result: unknown) => void; + Promise.resolve() + .then(() => mocks.exec(...args)) + .then( + (result) => callback(null, result ?? { stderr: "" }), + (error) => callback(error, undefined), + ); + }, + spawnSync: vi.fn(), +})); +vi.mock("node:fs/promises", () => ({ + default: { + access: vi.fn().mockResolvedValue(undefined), + readFile: mocks.readFile, + rm: mocks.rm, + }, +})); +vi.mock("electron", () => ({ app: { getPath: () => "/tmp" } })); +vi.mock("../ffmpeg/binary", () => ({ getFfmpegBinaryPath: () => "/ffmpeg" })); +vi.mock("../paths/binaries", () => ({ getBundledWhisperExecutableCandidates: () => ["/whisper"] })); +vi.mock("../project/session", () => ({ resolveRecordingSession: mocks.session })); +vi.mock("../recording/diagnostics", () => ({ + getUsableCompanionAudioCandidates: mocks.companions, + getCompanionAudioStartDelayMs: mocks.delay, +})); +vi.mock("../utils", () => ({ normalizeVideoSourcePath: (value: string) => value })); + +import { generateAutoCaptionsFromVideo } from "./generate"; + +const options = { videoPath: "/video.mp4", whisperModelPath: "/model.bin" }; +const srt = "1\n00:00:00,000 --> 00:00:02,000\nHello world.\n"; +const json = JSON.stringify({ + transcription: [ + { + offsets: { from: 0, to: 2000 }, + text: "Hello world.", + tokens: [ + { text: "Hello", offsets: { from: 0, to: 900 } }, + { text: " world.", offsets: { from: 900, to: 2000 } }, + ], + }, + ], +}); + +beforeEach(() => { + vi.clearAllMocks(); + mocks.session.mockReset().mockResolvedValue(null); + mocks.companions.mockReset().mockResolvedValue([]); + mocks.delay.mockReset().mockResolvedValue(null); + mocks.exec.mockReset().mockResolvedValue({ stderr: "" }); + mocks.readFile + .mockReset() + .mockImplementation(async (file: string) => (file.endsWith(".json") ? json : srt)); + mocks.rm.mockResolvedValue(undefined); +}); + +const whisperCalls = () => mocks.exec.mock.calls.filter(([file]) => file === "/whisper"); + +describe("caption generation pipeline", () => { + it("transcribes both sidecars independently, preserving the microphone delay", async () => { + mocks.companions.mockResolvedValue([ + { + platform: "mac", + micPath: "/video.mic.wav", + systemPath: "/video.system.wav", + usablePaths: ["/video.system.wav", "/video.mic.wav"], + }, + ]); + mocks.delay.mockResolvedValue(450); + const result = await generateAutoCaptionsFromVideo(options); + expect(result.audioSourceLabel).toBe("microphone and system audio"); + expect(whisperCalls()).toHaveLength(2); + expect(mocks.exec.mock.calls.some(([, args]) => args.includes("/video.system.wav"))).toBe( + true, + ); + expect(mocks.exec.mock.calls[0][1]).toEqual( + expect.arrayContaining(["-i", "/video.mic.wav", "-af", "adelay=450:all=1"]), + ); + }); + + it("falls back to embedded audio when the microphone cannot be extracted", async () => { + mocks.companions.mockResolvedValue([ + { + platform: "mac", + micPath: "/video.mic.wav", + systemPath: "/video.system.wav", + usablePaths: ["/video.mic.wav"], + }, + ]); + mocks.exec.mockImplementation(async (_file: string, args: string[]) => { + if (args.includes("/video.mic.wav")) throw new Error("unreadable microphone"); + return { stderr: "" }; + }); + expect((await generateAutoCaptionsFromVideo(options)).cues[0].text).toBe("Hello world."); + }); + + it("extracts mono audio, transcribes, segments word timings and cleans up", async () => { + const result = await generateAutoCaptionsFromVideo(options); + expect(result.cues[0].text).toBe("Hello world."); + expect(result.cues[0].words).toHaveLength(2); + expect(mocks.exec.mock.calls[0][1]).toEqual( + expect.arrayContaining(["-ac", "1", "-ar", "16000"]), + ); + expect(whisperCalls()).toHaveLength(1); + expect(whisperCalls()[0][1]).toEqual(expect.arrayContaining(["-mc", "0"])); + expect(mocks.rm).toHaveBeenCalledTimes(3); + }); + + it("retries SRT only for a runtime that rejects full JSON", async () => { + mocks.exec.mockImplementation(async (file: string, args: string[]) => { + if (file === "/whisper" && args.includes("-ojf")) + throw new Error("unknown argument: -ojf"); + return { stderr: "" }; + }); + expect((await generateAutoCaptionsFromVideo(options)).cues).toHaveLength(1); + expect(whisperCalls()).toHaveLength(2); + expect(whisperCalls()[1][1]).not.toContain("-ojf"); + }); + + it("keeps transcription when optional silence detection fails", async () => { + mocks.exec.mockImplementation(async (_file: string, args: string[]) => { + if (args.includes("-af")) throw new Error("silence filter failed"); + return { stderr: "" }; + }); + expect((await generateAutoCaptionsFromVideo(options)).cues[0].text).toBe("Hello world."); + }); +}); + +it("falls back to linked webcam audio when mic exists and other secondary sources fail", async () => { + mocks.session.mockResolvedValue({ webcamPath: "/webcam.mp4" }); + mocks.companions.mockResolvedValue([ + { + platform: "mac", + micPath: "/video.mic.wav", + systemPath: "/video.system.wav", + usablePaths: ["/video.mic.wav", "/video.system.wav"], + }, + ]); + mocks.exec.mockImplementation(async (file: string, args: string[]) => { + if (file === "/ffmpeg" && args.includes("pcm_s16le")) { + const source = args[args.indexOf("-i") + 1]; + if (source === "/video.mp4" || source === "/video.system.wav") + throw new Error("No audio"); + } + return { stderr: "" }; + }); + const result = await generateAutoCaptionsFromVideo(options); + expect(result.cues.length).toBeGreaterThan(0); + expect( + mocks.exec.mock.calls.some( + ([file, args]) => file === "/ffmpeg" && args.includes("/webcam.mp4"), + ), + ).toBe(true); +}); diff --git a/electron/ipc/captions/mergeSources.test.ts b/electron/ipc/captions/mergeSources.test.ts new file mode 100644 index 000000000..01a3cc4ee --- /dev/null +++ b/electron/ipc/captions/mergeSources.test.ts @@ -0,0 +1,117 @@ +import { expect, it } from "vitest"; +import { mergeCaptionSources } from "./mergeSources"; +import { parseWhisperJsonCues } from "./parser"; + +it("keeps system speech when the microphone has no speech", () => { + const system = [{ id: "s", startMs: 0, endMs: 1000, text: "System speech" }]; + expect(mergeCaptionSources([], system)[0].text).toBe("System speech"); +}); +it("keeps both tracks, choosing mic only for overlapping words", () => { + const mic = [{ id: "m", startMs: 1000, endMs: 2000, text: "Mic speech" }]; + const system = [ + { + id: "s", + startMs: 0, + endMs: 3000, + text: "Before conflict after", + words: [ + { text: "Before", startMs: 0, endMs: 900 }, + { text: "conflict", startMs: 1100, endMs: 1900, leadingSpace: true }, + { text: "after", startMs: 2100, endMs: 3000, leadingSpace: true }, + ], + }, + ]; + expect(mergeCaptionSources(mic, system).map((cue) => cue.text)).toEqual([ + "Before", + "Mic speech", + "after", + ]); +}); +it("retains real Whisper word timing through control tokens and punctuation", () => { + const cues = parseWhisperJsonCues( + JSON.stringify({ + transcription: [ + { + offsets: { from: 0, to: 1000 }, + text: "Hello.", + tokens: [ + { text: "[_BEG_]", offsets: { from: 0, to: 0 } }, + { text: " Hello", offsets: { from: 0, to: 900 } }, + { text: ".", offsets: { from: 900, to: 900 } }, + { text: "[_TT_50]", offsets: { from: 1000, to: 1000 } }, + ], + }, + ], + }), + ); + expect(cues[0].words).toEqual([{ text: "Hello.", startMs: 0, endMs: 900 }]); +}); + +it("does not let mic sound labels replace system speech", () => { + expect( + mergeCaptionSources( + [{ id: "m", startMs: 0, endMs: 2000, text: "[Coughing]" }], + [{ id: "s", startMs: 0, endMs: 2000, text: "The spoken paragraph." }], + ).map((c) => c.text), + ).toEqual(["The spoken paragraph."]); +}); + +it("preserves untimed speech next to timed words", async () => { + const { segmentCuesIntoPhrases } = await import("./segment"); + const result = segmentCuesIntoPhrases( + [ + { + id: "a", + startMs: 0, + endMs: 1000, + text: "First.", + words: [{ text: "First.", startMs: 0, endMs: 1000 }], + }, + { id: "b", startMs: 1000, endMs: 3000, text: "There is a timeline editor." }, + ], + [], + ); + expect(result.map((c) => c.text).join(" ")).toContain("There is a timeline editor."); +}); + +it("preserves system speech between timed microphone words", () => { + const result = mergeCaptionSources( + [ + { + id: "mic", + startMs: 0, + endMs: 3000, + text: "Hello again", + words: [ + { text: "Hello", startMs: 0, endMs: 600 }, + { text: "again", startMs: 2400, endMs: 3000 }, + ], + }, + ], + [{ id: "system", startMs: 1000, endMs: 1800, text: "In the gap" }], + ); + expect(result.map((cue) => cue.text)).toContain("In the gap"); +}); + +it("retains the unopposed portions of an untimed system cue", () => { + const result = mergeCaptionSources( + [ + { + id: "mic", + startMs: 0, + endMs: 3000, + text: "Mic", + words: [{ text: "Mic", startMs: 1000, endMs: 2000 }], + }, + ], + [{ id: "system", startMs: 0, endMs: 3000, text: "System paragraph" }], + ); + expect( + result + .filter((cue) => cue.text === "System paragraph") + .map(({ startMs, endMs }) => [startMs, endMs]), + ).toEqual([ + [0, 1000], + [2000, 3000], + ]); +}); diff --git a/electron/ipc/captions/mergeSources.ts b/electron/ipc/captions/mergeSources.ts new file mode 100644 index 000000000..1eab4c6c3 --- /dev/null +++ b/electron/ipc/captions/mergeSources.ts @@ -0,0 +1,62 @@ +import type { CaptionCuePayload, CaptionWordPayload } from "../types"; +import { buildCaptionTextFromWords } from "./parser"; + +/** Keep both speakers, with microphone speech taking priority during overlap. */ +export function mergeCaptionSources( + microphone: CaptionCuePayload[], + system: CaptionCuePayload[], +): CaptionCuePayload[] { + // Sound-event labels are not competing microphone speech. + const micSpeech = microphone.filter((cue) => !/^(?:\s*[[(][^\])]+[\])]\s*)+$/.test(cue.text)); + const micSpans = micSpeech.flatMap((cue) => (cue.words?.length ? cue.words : [cue])); + const overlapsMic = (startMs: number, endMs: number) => + micSpans.some((span) => startMs < span.endMs && endMs > span.startMs); + const systemCues: CaptionCuePayload[] = []; + for (const cue of system) { + if (!overlapsMic(cue.startMs, cue.endMs)) { + systemCues.push(cue); + continue; + } + if (!cue.words?.length) { + let spans = [{ startMs: cue.startMs, endMs: cue.endMs }]; + for (const mic of micSpans) { + spans = spans.flatMap((span) => { + if (mic.endMs <= span.startMs || mic.startMs >= span.endMs) return [span]; + return [ + { startMs: span.startMs, endMs: Math.min(span.endMs, mic.startMs) }, + { startMs: Math.max(span.startMs, mic.endMs), endMs: span.endMs }, + ].filter((part) => part.endMs > part.startMs); + }); + } + // SRT has no word boundaries: retain its text during the unopposed portions. + systemCues.push(...spans.map((span) => ({ ...cue, ...span }))); + continue; + } + // Preserve words outside the conflict, rather than dropping a whole paragraph. + let run: CaptionWordPayload[] = []; + const flush = () => { + if (run.length === 0) return; + systemCues.push({ + id: cue.id, + startMs: run[0].startMs, + endMs: run[run.length - 1].endMs, + text: buildCaptionTextFromWords(run), + words: run, + }); + run = []; + }; + for (const word of cue.words ?? []) { + if (overlapsMic(word.startMs, word.endMs)) flush(); + else run.push(word); + } + flush(); + } + const micEvents = microphone.filter( + (cue) => + !micSpeech.includes(cue) && + !system.some((other) => cue.startMs < other.endMs && cue.endMs > other.startMs), + ); + return [...micSpeech, ...micEvents, ...systemCues] + .sort((a, b) => a.startMs - b.startMs || a.endMs - b.endMs) + .map((cue, index) => ({ ...cue, id: `caption-${index + 1}` })); +} diff --git a/electron/ipc/captions/output.test.ts b/electron/ipc/captions/output.test.ts new file mode 100644 index 000000000..8b000966f --- /dev/null +++ b/electron/ipc/captions/output.test.ts @@ -0,0 +1,51 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, expect, it } from "vitest"; +import { readWhisperCaptionOutput } from "./output"; +let directory: string; +let output: string; +beforeEach(async () => { + directory = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-caption-output-")); + output = path.join(directory, "speech"); + await fs.writeFile(`${output}.srt`, "1\n00:00:01,200 --> 00:00:02,400\nFallback speech\n"); +}); +afterEach(async () => { + await fs.rm(directory, { recursive: true, force: true }); +}); +it.each([ + undefined, + "invalid json", + '{"transcription":[]}', +])("uses SRT when JSON is unusable: %s", async (json) => { + if (json !== undefined) await fs.writeFile(`${output}.json`, json); + const cues = await readWhisperCaptionOutput(output, true); + expect(cues).toEqual([ + { id: "caption-1", startMs: 1200, endMs: 2400, text: "Fallback speech" }, + ]); +}); +it("prefers valid JSON word timing", async () => { + await fs.writeFile( + `${output}.json`, + JSON.stringify({ + transcription: [ + { + offsets: { from: 1200, to: 2400 }, + text: "Timed speech", + tokens: [ + { text: "Timed", offsets: { from: 1200, to: 1800 } }, + { text: " speech", offsets: { from: 1800, to: 2400 } }, + ], + }, + ], + }), + ); + const cues = await readWhisperCaptionOutput(output, true); + expect(cues[0].text).toBe("Timed speech"); + expect(cues[0].words).toHaveLength(2); + expect(cues[0].words?.[1].startMs).toBe(1800); +}); +it("still surfaces missing subtitle output", async () => { + await fs.rm(`${output}.srt`); + await expect(readWhisperCaptionOutput(output, false)).rejects.toMatchObject({ code: "ENOENT" }); +}); diff --git a/electron/ipc/captions/output.ts b/electron/ipc/captions/output.ts new file mode 100644 index 000000000..a48f43f05 --- /dev/null +++ b/electron/ipc/captions/output.ts @@ -0,0 +1,18 @@ +import fs from "node:fs/promises"; +import { parseSrtCues, parseWhisperJsonCues } from "./parser"; + +/** Older Whisper builds may emit SRT without the requested word-timing JSON. */ +export async function readWhisperCaptionOutput(outputBase: string, jsonEnabled: boolean) { + if (jsonEnabled) { + let json = ""; + try { + json = await fs.readFile(`${outputBase}.json`, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + const timed = json ? parseWhisperJsonCues(json) : []; + if (timed.length) return timed; + console.warn("[auto-captions] No usable JSON timing output; falling back to SRT."); + } + return parseSrtCues(await fs.readFile(`${outputBase}.srt`, "utf8")); +} diff --git a/electron/ipc/captions/parser.ts b/electron/ipc/captions/parser.ts new file mode 100644 index 000000000..7ab213183 --- /dev/null +++ b/electron/ipc/captions/parser.ts @@ -0,0 +1,187 @@ +import type { + CaptionCuePayload, + CaptionWordPayload, + WhisperJsonSegment, + WhisperJsonToken, +} from "../types"; + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +export function buildCaptionTextFromWords(words: CaptionWordPayload[]): string { + return words + .map((word, index) => `${index > 0 && word.leadingSpace ? " " : ""}${word.text}`) + .join("") + .trim(); +} + +export function parseWhisperJsonWords(tokens: unknown): CaptionWordPayload[] { + if (!Array.isArray(tokens)) { + return []; + } + + const words: CaptionWordPayload[] = []; + let nextLeadingSpace = false; + + for (const token of tokens) { + if (!token || typeof token !== "object") { + continue; + } + + const tokenData = token as WhisperJsonToken; + const tokenText = typeof tokenData.text === "string" ? tokenData.text : ""; + if (!tokenText || /^\[_[^\]]+\]$/.test(tokenText)) { + continue; + } + + const tokenStartMs = isFiniteNumber(tokenData.offsets?.from) + ? Math.round(tokenData.offsets.from) + : null; + const tokenEndMs = isFiniteNumber(tokenData.offsets?.to) + ? Math.round(tokenData.offsets.to) + : null; + const parts = tokenText.match(/\s+|[^\s]+/g) ?? []; + + for (const part of parts) { + if (/^\s+$/.test(part)) { + nextLeadingSpace = words.length > 0; + continue; + } + + const previousWord = words.length > 0 ? words[words.length - 1] : null; + if (previousWord && !nextLeadingSpace && /^\p{P}+$/u.test(part)) { + previousWord.text += part; + continue; + } + if (tokenStartMs == null || tokenEndMs == null || tokenEndMs <= tokenStartMs) return []; + + if (!previousWord || nextLeadingSpace) { + words.push({ + text: part, + startMs: tokenStartMs, + endMs: tokenEndMs, + ...(words.length > 0 && nextLeadingSpace ? { leadingSpace: true } : {}), + }); + } else { + previousWord.text += part; + previousWord.endMs = Math.max(previousWord.endMs, tokenEndMs); + } + + nextLeadingSpace = false; + } + } + + return words.filter((word) => word.text.trim().length > 0); +} + +export function parseWhisperJsonCues(content: string): CaptionCuePayload[] { + try { + const parsed = JSON.parse(content) as { + transcription?: unknown; + }; + + if (!Array.isArray(parsed.transcription)) { + return []; + } + + return parsed.transcription + .map((segment, index) => { + if (!segment || typeof segment !== "object") { + return null; + } + + const segmentData = segment as WhisperJsonSegment; + const startMs = isFiniteNumber(segmentData.offsets?.from) + ? Math.round(segmentData.offsets.from) + : null; + const endMs = isFiniteNumber(segmentData.offsets?.to) + ? Math.round(segmentData.offsets.to) + : null; + const segmentText = + typeof segmentData.text === "string" ? segmentData.text.trim() : ""; + + if (startMs == null || endMs == null || endMs <= startMs) { + return null; + } + + const words = parseWhisperJsonWords(segmentData.tokens); + const text = words.length > 0 ? buildCaptionTextFromWords(words) : segmentText; + + if (!text) { + return null; + } + + return { + id: `caption-${index + 1}`, + startMs, + endMs, + text, + ...(words.length > 0 ? { words } : {}), + }; + }) + .filter((cue): cue is CaptionCuePayload => cue != null); + } catch (error) { + console.warn("[auto-captions] Failed to parse Whisper JSON output:", error); + return []; + } +} + +export function parseSrtTimestamp(value: string): number | null { + const match = value.trim().match(/^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/); + if (!match) { + return null; + } + + const [, hours, minutes, seconds, milliseconds] = match; + return ( + Number(hours) * 60 * 60 * 1000 + + Number(minutes) * 60 * 1000 + + Number(seconds) * 1000 + + Number(milliseconds) + ); +} + +export function parseSrtCues(content: string): CaptionCuePayload[] { + return content + .split(/\r?\n\r?\n/) + .map((block, index) => { + const lines = block.split(/\r?\n/).map((line) => line.trim()); + const timingLine = lines.find((line) => line.includes("-->")); + if (!timingLine) { + return null; + } + + const [rawStart, rawEnd] = timingLine.split("-->").map((part) => part.trim()); + const startMs = parseSrtTimestamp(rawStart); + const endMs = parseSrtTimestamp(rawEnd); + if (startMs == null || endMs == null || endMs <= startMs) { + return null; + } + + const text = lines + .slice(lines.indexOf(timingLine) + 1) + .filter((line) => line.length > 0) + .join("\n") + .trim(); + + if (!text) { + return null; + } + + return { + id: `caption-${index + 1}`, + startMs, + endMs, + text, + }; + }) + .filter((cue): cue is CaptionCuePayload => cue != null); +} + +export function shouldRetryWhisperWithoutJson(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + return /(?:unknown|unrecognized|unsupported) (?:argument|option)[:\s=]+['"]?--?(?:ojf|output-json-full)\b/i.test( + message, + ); +} diff --git a/electron/ipc/captions/runtimeErrors.ts b/electron/ipc/captions/runtimeErrors.ts new file mode 100644 index 000000000..707848dd6 --- /dev/null +++ b/electron/ipc/captions/runtimeErrors.ts @@ -0,0 +1,10 @@ +const WINDOWS_MISSING_RUNTIME_EXIT_CODES = new Set([-1073741515, 3221225781]); + +export function isMissingWindowsWhisperRuntimeDependency(error: unknown) { + if (process.platform !== "win32" || !error || typeof error !== "object") { + return false; + } + + const code = (error as { code?: unknown }).code; + return typeof code === "number" && WINDOWS_MISSING_RUNTIME_EXIT_CODES.has(code); +} diff --git a/electron/ipc/captions/segment.test.ts b/electron/ipc/captions/segment.test.ts new file mode 100644 index 000000000..57d86dab3 --- /dev/null +++ b/electron/ipc/captions/segment.test.ts @@ -0,0 +1,391 @@ +import { describe, expect, it } from "vitest"; +import type { CaptionCuePayload } from "../types"; +import { endsSentence, segmentCuesIntoPhrases } from "./segment"; + +describe("endsSentence", () => { + it("detects terminal punctuation, ignoring trailing closers", () => { + expect(endsSentence("world.")).toBe(true); + expect(endsSentence("you?")).toBe(true); + expect(endsSentence("stop!")).toBe(true); + expect(endsSentence("wait…")).toBe(true); + expect(endsSentence('said."')).toBe(true); + expect(endsSentence("done,")).toBe(false); + expect(endsSentence("hello")).toBe(false); + }); + + it("does not treat common abbreviations or initialisms as a sentence end", () => { + expect(endsSentence("Mr.")).toBe(false); + expect(endsSentence("Dr.")).toBe(false); + expect(endsSentence("etc.")).toBe(false); + expect(endsSentence("e.g.")).toBe(false); + expect(endsSentence("U.S.")).toBe(false); + expect(endsSentence("J.")).toBe(false); + // real sentence ends are unaffected + expect(endsSentence("Smith.")).toBe(true); + expect(endsSentence("5.")).toBe(true); + // `?`/`!` after an abbreviation-like token still end the sentence + expect(endsSentence("really?")).toBe(true); + }); +}); + +describe("segmentCuesIntoPhrases", () => { + it("splits back-to-back sentences with no pause into separate captions", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 1_800, + text: "Hello world. How are you?", + words: [ + { text: "Hello", startMs: 0, endMs: 400 }, + { text: "world.", startMs: 400, endMs: 800, leadingSpace: true }, + { text: "How", startMs: 800, endMs: 1_100, leadingSpace: true }, + { text: "are", startMs: 1_100, endMs: 1_400, leadingSpace: true }, + { text: "you?", startMs: 1_400, endMs: 1_800, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(2); + expect(result[0].text).toBe("Hello world."); + expect(result[1].text).toBe("How are you?"); + expect(result[0].endMs).toBeLessThanOrEqual(result[1].startMs); + }); + + it("does not leak the first word of the next sentence into the previous caption", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 1_800, + text: "Hello world. How are you?", + words: [ + { text: "Hello", startMs: 0, endMs: 400 }, + { text: "world.", startMs: 400, endMs: 800, leadingSpace: true }, + { text: "How", startMs: 800, endMs: 1_100, leadingSpace: true }, + { text: "are", startMs: 1_100, endMs: 1_400, leadingSpace: true }, + { text: "you?", startMs: 1_400, endMs: 1_800, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result[1].words?.[0].text).toBe("How"); + expect(result[1].words?.[0].startMs).toBe(800); + // the first word of a phrase has no leading space + expect(result[1].words?.[0].leadingSpace).toBeUndefined(); + }); + + it("merges rapid-fire short sentences into one caption (word-timed)", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 800, + text: "Okay. Great.", + words: [ + { text: "Okay.", startMs: 0, endMs: 400 }, + { text: "Great.", startMs: 400, endMs: 800, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("Okay. Great."); + expect(result[0].words?.map((word) => word.text)).toEqual(["Okay.", "Great."]); + }); + + it("merges rapid-fire short sentences into one caption (word-less)", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 0, endMs: 400, text: "Okay." }, + { id: "caption-2", startMs: 400, endMs: 800, text: "Great." }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("Okay. Great."); + }); + + it("does not merge two short captions across a real pause just above the merge gap", () => { + // Two short, sentence-ended phrases separated by a 450ms gap (just above the 400ms + // mergeGapMs). Edge padding shrinks the apparent gap, so if merge eligibility ran on + // padded timings it would wrongly fuse them. Merge must run on the true speech gap. + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 1_250, + text: "Okay. Great.", + words: [ + { text: "Okay.", startMs: 0, endMs: 400 }, + { text: "Great.", startMs: 850, endMs: 1_250, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(2); + expect(result[0].text).toBe("Okay."); + expect(result[1].text).toBe("Great."); + // Padding still applies, but cues stay separate and non-overlapping. + expect(result[0].endMs).toBeLessThanOrEqual(result[1].startMs); + }); + + it("does not merge a short sentence into a full-length one", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 2_400, + text: "Okay. This is a full length sentence that should stand alone.", + words: [ + { text: "Okay.", startMs: 0, endMs: 400 }, + { text: "This", startMs: 400, endMs: 700, leadingSpace: true }, + { text: "is", startMs: 700, endMs: 900, leadingSpace: true }, + { text: "a", startMs: 900, endMs: 1_050, leadingSpace: true }, + { text: "full", startMs: 1_050, endMs: 1_350, leadingSpace: true }, + { text: "length", startMs: 1_350, endMs: 1_650, leadingSpace: true }, + { text: "sentence", startMs: 1_650, endMs: 1_950, leadingSpace: true }, + { text: "that", startMs: 1_950, endMs: 2_100, leadingSpace: true }, + { text: "should", startMs: 2_100, endMs: 2_250, leadingSpace: true }, + { text: "stand", startMs: 2_250, endMs: 2_350, leadingSpace: true }, + { text: "alone.", startMs: 2_350, endMs: 2_400, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(2); + expect(result[0].text).toBe("Okay."); + expect(result[1].text).toBe("This is a full length sentence that should stand alone."); + }); + + it("keeps a comma/clause inside one caption", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 1_500, + text: "When I'm done, I start.", + words: [ + { text: "When", startMs: 0, endMs: 300 }, + { text: "I'm", startMs: 300, endMs: 600, leadingSpace: true }, + { text: "done,", startMs: 600, endMs: 900, leadingSpace: true }, + { text: "I", startMs: 900, endMs: 1_100, leadingSpace: true }, + { text: "start.", startMs: 1_100, endMs: 1_500, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("When I'm done, I start."); + }); + + it("does not split on an abbreviation, only on the real sentence end", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 1_800, + text: "Hello Mr. Smith. How are you?", + words: [ + { text: "Hello", startMs: 0, endMs: 300 }, + { text: "Mr.", startMs: 300, endMs: 600, leadingSpace: true }, + { text: "Smith.", startMs: 600, endMs: 900, leadingSpace: true }, + { text: "How", startMs: 900, endMs: 1_200, leadingSpace: true }, + { text: "are", startMs: 1_200, endMs: 1_500, leadingSpace: true }, + { text: "you?", startMs: 1_500, endMs: 1_800, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(2); + expect(result[0].text).toBe("Hello Mr. Smith."); + expect(result[1].text).toBe("How are you?"); + }); + + it("splits on a real pause even without punctuation", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 2_000, + text: "one two three four", + words: [ + { text: "one", startMs: 0, endMs: 300 }, + { text: "two", startMs: 300, endMs: 600, leadingSpace: true }, + // 800ms gap (>= 700ms pause) -> phrase boundary + { text: "three", startMs: 1_400, endMs: 1_700, leadingSpace: true }, + { text: "four", startMs: 1_700, endMs: 2_000, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(2); + expect(result[0].text).toBe("one two"); + expect(result[1].text).toBe("three four"); + }); + + it("splits on a long acoustic silence in a word gap even when the word gap is short", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 2_600, + text: "alpha beta", + words: [ + { text: "alpha", startMs: 0, endMs: 600 }, + { text: "beta", startMs: 2_200, endMs: 2_600, leadingSpace: true }, + ], + }, + ]; + // pauseMs high so the word gap (1600ms) wouldn't split on its own; the 1500ms + // silence sitting in the gap is what forces the break. beta starts after the + // silence ends, so it is not dropped as a hallucination. + const result = segmentCuesIntoPhrases(cues, [{ startMs: 600, endMs: 2_100 }], { + pauseMs: 5_000, + }); + expect(result).toHaveLength(2); + expect(result[0].text).toBe("alpha"); + expect(result[1].text).toBe("beta"); + }); + + it("does not split that same input when there is no silence and the gap is below the pause", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 2_600, + text: "alpha beta", + words: [ + { text: "alpha", startMs: 0, endMs: 600 }, + { text: "beta", startMs: 2_200, endMs: 2_600, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, [], { pauseMs: 5_000 }); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("alpha beta"); + }); + + it("splits a runaway phrase with no punctuation and no pause via the safety cap", () => { + const words = Array.from({ length: 12 }, (_, index) => ({ + text: `w${index}`, + startMs: index * 300, + endMs: index * 300 + 300, + ...(index > 0 ? { leadingSpace: true } : {}), + })); + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 3_600, + text: words.map((w) => w.text).join(" "), + words, + }, + ]; + const result = segmentCuesIntoPhrases(cues, [], { maxPhraseMs: 2_000 }); + expect(result.length).toBeGreaterThan(1); + expect(result.map((cue) => cue.text).join(" ")).toBe(words.map((w) => w.text).join(" ")); + }); + + it("drops words that fall entirely inside a long silence (hallucination)", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 5_000, + endMs: 6_000, + text: "Thank you.", + words: [ + { text: "Thank", startMs: 5_000, endMs: 5_500 }, + { text: "you.", startMs: 5_500, endMs: 6_000, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, [{ startMs: 4_000, endMs: 9_000 }]); + expect(result).toHaveLength(0); + }); + + it("produces sorted, non-overlapping cues with sequential ids", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 2_400, + text: "First phrase here. Second phrase here. Third one.", + words: [ + { text: "First", startMs: 0, endMs: 300 }, + { text: "phrase", startMs: 300, endMs: 600, leadingSpace: true }, + { text: "here.", startMs: 600, endMs: 900, leadingSpace: true }, + { text: "Second", startMs: 900, endMs: 1_200, leadingSpace: true }, + { text: "phrase", startMs: 1_200, endMs: 1_500, leadingSpace: true }, + { text: "here.", startMs: 1_500, endMs: 1_800, leadingSpace: true }, + { text: "Third", startMs: 1_800, endMs: 2_100, leadingSpace: true }, + { text: "one.", startMs: 2_100, endMs: 2_400, leadingSpace: true }, + ], + }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(3); + for (let index = 0; index < result.length - 1; index += 1) { + expect(result[index].startMs).toBeLessThanOrEqual(result[index + 1].startMs); + expect(result[index].endMs).toBeLessThanOrEqual(result[index + 1].startMs); + } + expect(result.map((cue) => cue.id)).toEqual( + result.map((_, index) => `caption-${index + 1}`), + ); + }); + + it("keeps a single-sentence word-less cue intact (legacy silence trim preserved)", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 0, endMs: 13_920, text: "Hello, this is a test" }, + ]; + // One sentence -> unchanged: trim leading silence, keep text (legacy behavior). + const result = segmentCuesIntoPhrases(cues, [{ startMs: 0, endMs: 3_000 }]); + expect(result).toHaveLength(1); + expect(result[0].startMs).toBe(3_000 - 80); + expect(result[0].endMs).toBe(13_920 + 80); + expect(result[0].text).toBe("Hello, this is a test"); + }); + + it("splits a continuous word-less paragraph into one caption per sentence", () => { + // The reported bug: no word timings (SRT path) + continuous speech collapsed into one + // caption. It must now split on sentence punctuation even without word timings. + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 0, endMs: 3_000, text: "This is one." }, + { id: "caption-2", startMs: 3_000, endMs: 6_000, text: "This is two." }, + { id: "caption-3", startMs: 6_000, endMs: 9_000, text: "This is three." }, + ]; + const result = segmentCuesIntoPhrases(cues, []); + expect(result).toHaveLength(3); + expect(result.map((cue) => cue.text)).toEqual([ + "This is one.", + "This is two.", + "This is three.", + ]); + for (let index = 0; index < result.length - 1; index += 1) { + expect(result[index].endMs).toBeLessThanOrEqual(result[index + 1].startMs); + } + expect(result.map((cue) => cue.id)).toEqual(["caption-1", "caption-2", "caption-3"]); + }); + + it("returns an empty array for empty input", () => { + expect(segmentCuesIntoPhrases([], [])).toEqual([]); + }); +}); + +it("keeps the last timed word when an adjacent untimed cue overlaps padded edges", () => { + const result = segmentCuesIntoPhrases( + [ + { + id: "timed", + startMs: 0, + endMs: 1200, + text: "Hello.", + words: [{ text: "Hello.", startMs: 0, endMs: 1000 }], + }, + { id: "untimed", startMs: 950, endMs: 2000, text: "Next sentence." }, + ], + [], + { minCaptionMs: 0, edgePadMs: 100 }, + ); + const timed = result.find((cue) => cue.words?.length)!; + expect(timed.endMs).toBeGreaterThanOrEqual(1000); + expect(result.some((cue) => cue.text === "Next sentence.")).toBe(true); +}); diff --git a/electron/ipc/captions/segment.ts b/electron/ipc/captions/segment.ts new file mode 100644 index 000000000..f1c889a70 --- /dev/null +++ b/electron/ipc/captions/segment.ts @@ -0,0 +1,475 @@ +import type { CaptionCuePayload, CaptionWordPayload } from "../types"; +import { buildCaptionTextFromWords } from "./parser"; +import { padSpans, resegmentCuesBySilence, type SilenceInterval } from "./silence"; + +/** + * Phrase-aware caption segmentation. + * + * Whisper breaks speech on its own internal boundaries — not on sentences — so a single + * cue can run two phrases together, and re-segmenting purely on acoustic silence merges + * back-to-back sentences and misassigns boundary words. We instead walk Whisper's own + * word stream (which carries punctuation) and start a new caption at a real boundary: + * - the end of a sentence (`.`, `?`, `!`, `…`), or + * - a real pause — a large gap between two consecutive words, or a long ffmpeg + * `silencedetect` interval sitting in that gap. + * + * Because every break happens *between two consecutive words*, a word can never leak into + * the wrong caption (the failure mode of center-time region assignment). Commas/clauses + * stay inside a caption and a whole sentence is allowed to be one caption; only a high + * safety cap splits a runaway phrase with no punctuation and no pause. + * + * When the transcript has no word timings (SRT fallback) we first re-segment by acoustic + * silence (`resegmentCuesBySilence`) and then split each cue on its sentence punctuation, so + * a continuous paragraph still becomes one caption per sentence (timing is proportional). + */ + +/** A gap (ms) between two consecutive words this long or longer starts a new phrase. */ +const DEFAULT_PHRASE_PAUSE_MS = 700; +/** An ffmpeg `silencedetect` interval this long inside a word gap also starts a new phrase. */ +const DEFAULT_SPLIT_SILENCE_MS = 1_500; +/** Padding (ms) kept around each phrase so captions don't feel clipped. */ +const DEFAULT_EDGE_PAD_MS = 80; +/** Safety cap: a phrase with no sentence end and no pause is split once it gets this long. */ +const DEFAULT_MAX_PHRASE_MS = 12_000; + +/** + * A caption shorter than this (ms) is "too quick" and may be merged with an adjacent short + * caption so rapid-fire one-word sentences ("Okay." "Great.") don't each flash by alone. + */ +const DEFAULT_MIN_CAPTION_MS = 800; +/** Only merge short captions separated by at most this gap (ms) — never across a real pause. */ +const DEFAULT_MERGE_GAP_MS = 400; +/** A merged short-caption run never grows past this duration (ms) or character count. */ +const DEFAULT_MAX_MERGED_MS = 2_500; +const DEFAULT_MAX_MERGED_CHARS = 80; + +export interface SegmentOptions { + /** Word gap (ms) that splits one phrase into two. Lower = more, shorter captions. */ + pauseMs?: number; + /** Minimum acoustic silence (ms) inside a word gap that also splits a phrase. */ + splitSilenceMs?: number; + /** Padding (ms) kept around each phrase. */ + edgePadMs?: number; + /** Safety cap (ms) that splits a punctuation-less, pause-less runaway phrase. */ + maxPhraseMs?: number; + /** A caption shorter than this (ms) may be merged with an adjacent short caption. */ + minCaptionMs?: number; + /** Only merge short captions separated by at most this gap (ms). */ + mergeGapMs?: number; + /** A merged short-caption run never grows past this duration (ms). */ + maxMergedMs?: number; + /** A merged short-caption run never grows past this character count. */ + maxMergedChars?: number; +} + +interface CaptionPiece { + startMs: number; + endMs: number; + text: string; + words: CaptionWordPayload[]; +} + +const SENTENCE_END = /[.?!…。!?]$/; +/** Closing quotes/brackets that can trail terminal punctuation, e.g. `said."` */ +const TRAILING_CLOSERS = /[)\]}"'”’»」』)】]}>]+$/u; + +/** + * Unambiguous English titles that take a trailing period mid-sentence. Kept deliberately + * short: only words that are never themselves a sentence (so we don't suppress a real + * break — e.g. "no" is excluded because "No." is a valid sentence). Dotted initialisms + * like "e.g."/"U.S."/"a.m." are handled by the regex below, not this list. `?`/`!`/`…` + * always end a sentence. For non-English audio this simply never matches. + */ +const ABBREVIATIONS = new Set(["mr", "mrs", "ms", "dr", "prof", "sr", "jr", "st", "vs", "etc"]); + +/** A trailing period belongs to an abbreviation/initialism rather than ending a sentence. */ +function isAbbreviation(text: string): boolean { + const trimmed = text.trim().replace(TRAILING_CLOSERS, "").trim(); + if (!trimmed.endsWith(".")) { + return false; // only a plain period can be an abbreviation marker + } + const core = trimmed.slice(0, -1).toLowerCase(); + if (core.length === 0) { + return false; + } + // Single-letter initial ("J.", "U.") or dotted initialism ("U.S.", "e.g.", "a.m."). + if (/^[a-z]$/.test(core) || /^[a-z](\.[a-z])+$/.test(core)) { + return true; + } + return ABBREVIATIONS.has(core); +} + +/** + * True when a word's text ends a sentence, ignoring trailing closing quotes/brackets and + * common abbreviations (so "Mr. Smith" or "e.g." don't start a new caption). + */ +export function endsSentence(text: string): boolean { + const trimmed = text.trim().replace(TRAILING_CLOSERS, "").trim(); + if (!SENTENCE_END.test(trimmed)) { + return false; + } + return !isAbbreviation(text); +} + +/** Every cue carries usable word timing, so we can segment on the word stream. */ +function hasWordTimings(cues: CaptionCuePayload[]): boolean { + return cues.length > 0 && cues.every((cue) => Array.isArray(cue.words) && cue.words.length > 0); +} + +/** Flatten all cues' words into one time-ordered stream, spacing across cue joins. */ +function flattenWords(cues: CaptionCuePayload[]): CaptionWordPayload[] { + const stream: CaptionWordPayload[] = []; + for (const cue of cues) { + const words = (cue.words ?? []) as CaptionWordPayload[]; + words.forEach((word, index) => { + // A new cue continues the speech, so its first word leads with a space. + const leadingSpace = + stream.length > 0 && (index === 0 ? true : word.leadingSpace !== false); + stream.push({ + text: word.text, + startMs: word.startMs, + endMs: word.endMs, + ...(leadingSpace ? { leadingSpace: true } : {}), + }); + }); + } + return stream.sort((left, right) => left.startMs - right.startMs || left.endMs - right.endMs); +} + +/** Drop words that sit entirely inside a long detected silence (Whisper hallucinations). */ +function dropHallucinations( + words: CaptionWordPayload[], + silences: SilenceInterval[], + splitSilenceMs: number, +): CaptionWordPayload[] { + const longSilences = silences.filter( + (silence) => silence.endMs - silence.startMs >= splitSilenceMs, + ); + if (longSilences.length === 0) { + return words; + } + return words.filter( + (word) => + !longSilences.some( + (silence) => silence.startMs <= word.startMs && word.endMs <= silence.endMs, + ), + ); +} + +/** A long silence interval overlaps the gap between two consecutive words. */ +function silenceInGap( + gapStartMs: number, + gapEndMs: number, + silences: SilenceInterval[], + splitSilenceMs: number, +): boolean { + return silences.some( + (silence) => + silence.endMs - silence.startMs >= splitSilenceMs && + silence.startMs < gapEndMs && + silence.endMs > gapStartMs, + ); +} + +/** Reset the first word's leading space so a phrase reads as its own line. */ +function normalizePhraseWords(words: CaptionWordPayload[]): CaptionWordPayload[] { + return words.map((word, index) => { + if (index === 0 && word.leadingSpace) { + const { leadingSpace: _leadingSpace, ...rest } = word; + return rest; + } + return word; + }); +} + +/** Group a cue's words into runs that each end on a sentence boundary. */ +function groupWordsBySentence(words: CaptionWordPayload[]): CaptionWordPayload[][] { + const groups: CaptionWordPayload[][] = []; + let current: CaptionWordPayload[] = []; + words.forEach((word, index) => { + current.push(word); + if (endsSentence(word.text) && index < words.length - 1) { + groups.push(current); + current = []; + } + }); + if (current.length > 0) { + groups.push(current); + } + return groups; +} + +/** + * Split a word-less cue's text into one cue per sentence, distributing the cue's time span + * across sentences by character length. Used on the fallback (no word timing) path so a + * continuous paragraph still becomes one caption per sentence. + */ +function splitTextBySentence(cue: CaptionCuePayload): CaptionCuePayload[] { + const tokens = cue.text.trim().split(/\s+/).filter(Boolean); + if (tokens.length <= 1) { + return [cue]; + } + + const groups: string[][] = []; + let current: string[] = []; + tokens.forEach((token, index) => { + current.push(token); + if (endsSentence(token) && index < tokens.length - 1) { + groups.push(current); + current = []; + } + }); + if (current.length > 0) { + groups.push(current); + } + if (groups.length <= 1) { + return [cue]; + } + + const texts = groups.map((group) => group.join(" ")); + const totalChars = texts.reduce((sum, text) => sum + text.length, 0) || 1; + const spanMs = Math.max(1, cue.endMs - cue.startMs); + let cursorMs = cue.startMs; + return texts.map((text, index) => { + const startMs = cursorMs; + const endMs = + index === texts.length - 1 + ? cue.endMs + : Math.min( + cue.endMs - 1, + Math.round(startMs + (spanMs * text.length) / totalChars), + ); + cursorMs = Math.max(startMs + 1, endMs); + return { id: cue.id, startMs, endMs: Math.max(startMs + 1, endMs), text }; + }); +} + +/** Split one re-segmented cue into one cue per sentence (by words if present, else text). */ +function splitCueBySentences(cue: CaptionCuePayload): CaptionCuePayload[] { + const words = Array.isArray(cue.words) ? (cue.words as CaptionWordPayload[]) : []; + if (words.length === 0) { + return splitTextBySentence(cue); + } + + const groups = groupWordsBySentence(words); + if (groups.length <= 1) { + return [cue]; + } + return groups.map((group) => { + const phraseWords = normalizePhraseWords(group); + return { + id: cue.id, + startMs: phraseWords[0].startMs, + endMs: phraseWords[phraseWords.length - 1].endMs, + text: buildCaptionTextFromWords(phraseWords), + words: phraseWords, + }; + }); +} + +/** Concatenate two adjacent cues into one, joining words (with a space) when both have them. */ +function mergeTwoCues(left: CaptionCuePayload, right: CaptionCuePayload): CaptionCuePayload { + const leftWords = Array.isArray(left.words) ? (left.words as CaptionWordPayload[]) : []; + const rightWords = Array.isArray(right.words) ? (right.words as CaptionWordPayload[]) : []; + if (leftWords.length > 0 && rightWords.length > 0) { + // The right cue's first word started its own phrase (no leading space) — restore it. + const joined = normalizePhraseWords([ + ...leftWords, + ...rightWords.map((word, index) => + index === 0 ? { ...word, leadingSpace: true } : word, + ), + ]); + return { + id: left.id, + startMs: left.startMs, + endMs: right.endMs, + text: buildCaptionTextFromWords(joined), + words: joined, + }; + } + return { + id: left.id, + startMs: left.startMs, + endMs: right.endMs, + text: `${left.text} ${right.text}`.trim(), + }; +} + +interface MergeOptions { + minCaptionMs: number; + mergeGapMs: number; + maxMergedMs: number; + maxMergedChars: number; +} + +/** + * Merge adjacent captions that are BOTH short and rapid-fire (tiny gap), so quick one-word + * sentences like "Okay." "Great." read as one caption instead of flashing by individually. + * Only merges when both sides are short, so a short caption never absorbs a full-length one, + * and never across a real pause or past the size caps. + */ +function mergeShortAdjacentCaptions( + cues: CaptionCuePayload[], + options: MergeOptions, +): CaptionCuePayload[] { + if (cues.length <= 1) { + return cues; + } + + const merged: CaptionCuePayload[] = []; + let group = cues[0]; + for (let index = 1; index < cues.length; index += 1) { + const next = cues[index]; + const groupDurationMs = group.endMs - group.startMs; + const nextDurationMs = next.endMs - next.startMs; + const gapMs = next.startMs - group.endMs; + const combinedDurationMs = next.endMs - group.startMs; + const combinedChars = group.text.length + next.text.length + 1; + + const canMerge = + groupDurationMs < options.minCaptionMs && + nextDurationMs < options.minCaptionMs && + gapMs <= options.mergeGapMs && + combinedDurationMs <= options.maxMergedMs && + combinedChars <= options.maxMergedChars; + + if (canMerge) { + group = mergeTwoCues(group, next); + } else { + merged.push(group); + group = next; + } + } + merged.push(group); + return merged; +} + +/** Assign sequential, stable ids to the final cue list. */ +function renumberCues(cues: CaptionCuePayload[]): CaptionCuePayload[] { + return cues.map((cue, index) => ({ ...cue, id: `caption-${index + 1}` })); +} + +/** + * Re-segment Whisper cues into one caption per sentence/phrase, then merge rapid-fire short + * sentences back together. Returns sorted cues with fresh ids, preserving overlaps required by word timing. Falls back to + * silence-only re-segmentation (plus sentence splitting) when the transcript has no word timings. + */ +export function segmentCuesIntoPhrases( + cues: CaptionCuePayload[], + silences: SilenceInterval[], + options: SegmentOptions = {}, +): CaptionCuePayload[] { + const pauseMs = options.pauseMs ?? DEFAULT_PHRASE_PAUSE_MS; + const splitSilenceMs = options.splitSilenceMs ?? DEFAULT_SPLIT_SILENCE_MS; + const edgePadMs = options.edgePadMs ?? DEFAULT_EDGE_PAD_MS; + const maxPhraseMs = options.maxPhraseMs ?? DEFAULT_MAX_PHRASE_MS; + const mergeOptions: MergeOptions = { + minCaptionMs: options.minCaptionMs ?? DEFAULT_MIN_CAPTION_MS, + mergeGapMs: options.mergeGapMs ?? DEFAULT_MERGE_GAP_MS, + maxMergedMs: options.maxMergedMs ?? DEFAULT_MAX_MERGED_MS, + maxMergedChars: options.maxMergedChars ?? DEFAULT_MAX_MERGED_CHARS, + }; + + if (cues.length === 0) { + return []; + } + + // Keep untimed segments separate: merging them into timed segments would + // rebuild the text from words and silently discard the untimed speech. + if (!hasWordTimings(cues) && cues.some((cue) => (cue.words?.length ?? 0) > 0)) { + const segmented = cues.flatMap((cue) => segmentCuesIntoPhrases([cue], silences, options)); + segmented.sort((a, b) => a.startMs - b.startMs || a.endMs - b.endMs); + for (let index = 0; index < segmented.length - 1; index++) { + const cue = segmented[index]; + const lastWordEnd = Math.max( + cue.startMs, + ...(cue.words ?? []).map((word) => word.endMs), + ); + cue.endMs = Math.max(lastWordEnd, Math.min(cue.endMs, segmented[index + 1].startMs)); + } + return renumberCues(segmented.filter((cue) => cue.endMs > cue.startMs)); + } + + // No word timings (SRT path): the silence-only segmenter trims/merges by acoustic + // silence but can't see sentence boundaries, so a continuous paragraph would collapse + // into one caption. Re-segment by silence first, then split each cue on its sentence + // punctuation so we still get one caption per sentence. + if (!hasWordTimings(cues)) { + const base = resegmentCuesBySilence(cues, silences, { splitSilenceMs, edgePadMs }); + const sentences = base.flatMap(splitCueBySentences); + return renumberCues(mergeShortAdjacentCaptions(sentences, mergeOptions)); + } + + const sortedCues = [...cues].sort( + (left, right) => left.startMs - right.startMs || left.endMs - right.endMs, + ); + const stream = dropHallucinations(flattenWords(sortedCues), silences, splitSilenceMs); + if (stream.length === 0) { + return []; + } + + const phrases: CaptionWordPayload[][] = []; + let current: CaptionWordPayload[] = []; + + for (let index = 0; index < stream.length; index += 1) { + const word = stream[index]; + current.push(word); + + const next = stream[index + 1]; + if (!next) { + break; + } + + const gapMs = next.startMs - word.endMs; + const phraseDurationMs = word.endMs - current[0].startMs; + const shouldBreak = + endsSentence(word.text) || + gapMs >= pauseMs || + // Only consult acoustic silence when there's a real gap between the words. When + // consecutive words overlap or abut (gapMs <= 0) a silence interval spanning that + // region must not manufacture a bogus split. + (gapMs > 0 && silenceInGap(word.endMs, next.startMs, silences, splitSilenceMs)) || + phraseDurationMs >= maxPhraseMs; + + if (shouldBreak) { + phrases.push(current); + current = []; + } + } + if (current.length > 0) { + phrases.push(current); + } + + const pieces: CaptionPiece[] = phrases + .map((words) => { + const phraseWords = normalizePhraseWords(words); + return { + startMs: phraseWords[0].startMs, + endMs: phraseWords[phraseWords.length - 1].endMs, + text: buildCaptionTextFromWords(phraseWords), + words: phraseWords, + }; + }) + .filter((piece) => piece.text.trim().length > 0); + + if (pieces.length === 0) { + return []; + } + + pieces.sort((left, right) => left.startMs - right.startMs || left.endMs - right.endMs); + + const sentenceCues: CaptionCuePayload[] = pieces.map((piece) => ({ + id: "", + startMs: piece.startMs, + endMs: piece.endMs, + text: piece.text, + ...(piece.words.length > 0 ? { words: piece.words } : {}), + })); + // Merge rapid-fire short captions BEFORE padding so merge eligibility sees the true + // speech gaps. Padding pulls cue edges toward each other, which would shrink the + // apparent gap and could merge two captions across a real pause sitting just above + // mergeGapMs. Pad the survivors afterward so envelopes still get their edge padding. + const merged = mergeShortAdjacentCaptions(sentenceCues, mergeOptions); + padSpans(merged, edgePadMs); + return renumberCues(merged); +} diff --git a/electron/ipc/captions/silence.test.ts b/electron/ipc/captions/silence.test.ts new file mode 100644 index 000000000..941d1076f --- /dev/null +++ b/electron/ipc/captions/silence.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it } from "vitest"; +import type { CaptionCuePayload } from "../types"; +import { parseSilenceIntervals, resegmentCuesBySilence } from "./silence"; + +describe("parseSilenceIntervals", () => { + it("pairs silence_start / silence_end lines into ms intervals", () => { + const stderr = [ + "[silencedetect @ 0x1] silence_start: 0", + "[silencedetect @ 0x1] silence_end: 3.2 | silence_duration: 3.2", + "[silencedetect @ 0x1] silence_start: 8.5", + "[silencedetect @ 0x1] silence_end: 9.3 | silence_duration: 0.8", + ].join("\n"); + expect(parseSilenceIntervals(stderr)).toEqual([ + { startMs: 0, endMs: 3_200 }, + { startMs: 8_500, endMs: 9_300 }, + ]); + }); + + it("treats a trailing silence_start with no end as running to end-of-audio", () => { + const stderr = "[silencedetect] silence_start: 12.0"; + expect(parseSilenceIntervals(stderr)).toEqual([ + { startMs: 12_000, endMs: Number.POSITIVE_INFINITY }, + ]); + }); + + it("returns an empty array when there is no silence output", () => { + expect(parseSilenceIntervals("some unrelated ffmpeg log\n")).toEqual([]); + }); +}); + +describe("resegmentCuesBySilence", () => { + it("trims leading silence off a word-less cue", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 0, endMs: 13_920, text: "Hello, this is a test" }, + ]; + const result = resegmentCuesBySilence(cues, [{ startMs: 0, endMs: 3_000 }]); + expect(result).toHaveLength(1); + // speech span [3000, 13920]; only an 80ms pad re-enters the silence + expect(result[0].startMs).toBe(3_000 - 80); + expect(result[0].endMs).toBe(13_920 + 80); + expect(result[0].text).toBe("Hello, this is a test"); + }); + + it("does NOT split on a short pause (slowing down stays one phrase)", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 0, endMs: 8_000, text: "one two three four" }, + ]; + // 800ms pause is below the 1500ms split threshold -> absorbed, stays one phrase + const result = resegmentCuesBySilence(cues, [{ startMs: 3_000, endMs: 3_800 }]); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("one two three four"); + }); + + it("merges adjacent cues that are not separated by a long pause", () => { + const cues: CaptionCuePayload[] = [ + { id: "a", startMs: 0, endMs: 3_000, text: "hello world" }, + { id: "b", startMs: 3_000, endMs: 6_000, text: "how are you" }, + ]; + // continuous speech, no silence -> one merged phrase + const result = resegmentCuesBySilence(cues, []); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("hello world how are you"); + }); + + it("splits only at a long pause (>= 1500ms)", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 0, endMs: 10_000, text: "one two three four" }, + ]; + // 2000ms pause -> a real boundary + const result = resegmentCuesBySilence(cues, [{ startMs: 4_000, endMs: 6_000 }]); + expect(result).toHaveLength(2); + expect(result[0].endMs).toBeLessThanOrEqual(result[1].startMs); + expect(`${result[0].text} ${result[1].text}`).toBe("one two three four"); + }); + + it("does not over-split: two short pauses inside a sentence stay one phrase", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 0, endMs: 9_000, text: "so this is a slow sentence" }, + ]; + const result = resegmentCuesBySilence(cues, [ + { startMs: 2_000, endMs: 2_700 }, // 700ms + { startMs: 5_000, endMs: 5_900 }, // 900ms + ]); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("so this is a slow sentence"); + }); + + it("drops a cue that falls entirely inside silence (hallucination)", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 5_000, endMs: 7_000, text: "Thank you." }, + ]; + const result = resegmentCuesBySilence(cues, [{ startMs: 4_000, endMs: 9_000 }]); + expect(result).toHaveLength(0); + }); + + it("keeps a continuous cue (no silence) as a single phrase", () => { + const cues: CaptionCuePayload[] = [ + { id: "caption-1", startMs: 1_000, endMs: 4_000, text: "continuous speech" }, + ]; + const result = resegmentCuesBySilence(cues, []); + expect(result).toHaveLength(1); + expect(result[0].text).toBe("continuous speech"); + }); + + it("splits a word-timed cue by assigning each word to its speech region", () => { + const cues: CaptionCuePayload[] = [ + { + id: "caption-1", + startMs: 0, + endMs: 10_000, + text: "alpha beta gamma", + words: [ + { text: "alpha", startMs: 500, endMs: 1_000 }, + { text: "beta", startMs: 1_000, endMs: 1_500, leadingSpace: true }, + { text: "gamma", startMs: 7_000, endMs: 7_500, leadingSpace: true }, + ], + }, + ]; + // 4000ms pause puts alpha+beta in region 1, gamma in region 2 + const result = resegmentCuesBySilence(cues, [{ startMs: 2_000, endMs: 6_000 }]); + expect(result).toHaveLength(2); + expect(result[0].text).toBe("alpha beta"); + expect(result[1].text).toBe("gamma"); + expect(result[0].endMs).toBeLessThanOrEqual(result[1].startMs); + }); + + it("produces sorted, non-overlapping cues with sequential ids", () => { + const cues: CaptionCuePayload[] = [ + { id: "x", startMs: 0, endMs: 6_000, text: "first phrase here" }, + { id: "y", startMs: 6_000, endMs: 14_000, text: "second phrase here" }, + ]; + const result = resegmentCuesBySilence(cues, [{ startMs: 6_000, endMs: 8_000 }]); + for (let index = 0; index < result.length - 1; index += 1) { + expect(result[index].startMs).toBeLessThanOrEqual(result[index + 1].startMs); + expect(result[index].endMs).toBeLessThanOrEqual(result[index + 1].startMs); + } + expect(result.map((cue) => cue.id)).toEqual( + result.map((_, index) => `caption-${index + 1}`), + ); + }); +}); diff --git a/electron/ipc/captions/silence.ts b/electron/ipc/captions/silence.ts new file mode 100644 index 000000000..c8750ca1c --- /dev/null +++ b/electron/ipc/captions/silence.ts @@ -0,0 +1,323 @@ +import type { CaptionCuePayload, CaptionWordPayload } from "../types"; +import { buildCaptionTextFromWords } from "./parser"; + +/** + * Silence-aware caption re-segmentation. + * + * Whisper breaks speech on its own internal boundaries, not on real pauses, so cues can + * cover silence and continuous speech gets chopped arbitrarily. We re-segment against + * ground-truth silence from ffmpeg `silencedetect` instead: + * ffmpeg silencedetect (stderr) -> parseSilenceIntervals -> resegmentCuesBySilence + * + * A phrase only breaks at a long pause (>= splitSilenceMs) or at silence touching the + * transcript edges (leading/trailing trim). Short pauses are absorbed; speech not separated + * by a long pause is merged; a region with no speech is dropped (drops hallucinations). + */ + +/** ffmpeg `silencedetect` noise floor. Quieter than this counts as silence. */ +export const SILENCE_NOISE_DB = -30; +/** ffmpeg `silencedetect` minimum silence duration (seconds) it should report at all. */ +export const SILENCE_DETECT_MIN_S = 0.5; + +/** A pause must be at least this long (ms) to break one phrase into two. The "sensitivity" knob. */ +const DEFAULT_SPLIT_SILENCE_MS = 1_500; +/** Padding (ms) kept around each phrase so captions don't feel clipped. */ +const DEFAULT_EDGE_PAD_MS = 80; +/** Speech regions shorter than this (ms) are dropped as artifacts. */ +const DEFAULT_MIN_SPEECH_MS = 150; + +export interface SilenceInterval { + startMs: number; + /** `Number.POSITIVE_INFINITY` for a trailing silence that runs to end-of-audio. */ + endMs: number; +} + +export interface ResegmentOptions { + /** Minimum pause (ms) that splits a phrase. Higher = fewer splits. */ + splitSilenceMs?: number; + /** Padding (ms) kept around each phrase. */ + edgePadMs?: number; + /** Speech regions shorter than this (ms) are dropped. */ + minSpeechMs?: number; +} + +interface Span { + startMs: number; + endMs: number; +} + +interface CaptionPiece extends Span { + text: string; + words?: CaptionWordPayload[]; +} + +/** + * Parse ffmpeg `silencedetect` stderr into ordered, non-overlapping silence intervals. + * Lines look like: + * [silencedetect @ 0x..] silence_start: 12.34 + * [silencedetect @ 0x..] silence_end: 15.67 | silence_duration: 3.33 + */ +export function parseSilenceIntervals(stderr: string): SilenceInterval[] { + const intervals: SilenceInterval[] = []; + let pendingStartMs: number | null = null; + + for (const line of stderr.split(/\r?\n/)) { + const startMatch = line.match(/silence_start:\s*(-?[\d.]+)/); + if (startMatch) { + pendingStartMs = Math.max(0, Math.round(Number.parseFloat(startMatch[1]) * 1000)); + continue; + } + + const endMatch = line.match(/silence_end:\s*(-?[\d.]+)/); + if (endMatch && pendingStartMs !== null) { + const endMs = Math.round(Number.parseFloat(endMatch[1]) * 1000); + if (endMs > pendingStartMs) { + intervals.push({ startMs: pendingStartMs, endMs }); + } + pendingStartMs = null; + } + } + + // A trailing silence_start with no matching end runs to the end of the audio. + if (pendingStartMs !== null) { + intervals.push({ startMs: pendingStartMs, endMs: Number.POSITIVE_INFINITY }); + } + + return intervals.sort((left, right) => left.startMs - right.startMs); +} + +/** Subtract a set of (sorted, non-overlapping) intervals from [startMs, endMs]. */ +function subtractIntervals(startMs: number, endMs: number, intervals: Span[]): Span[] { + const spans: Span[] = []; + let cursor = startMs; + + for (const interval of intervals) { + if (interval.endMs <= startMs || interval.startMs >= endMs) { + continue; + } + const clippedStart = Math.max(interval.startMs, startMs); + if (clippedStart > cursor) { + spans.push({ startMs: cursor, endMs: clippedStart }); + } + cursor = Math.max(cursor, Math.min(interval.endMs, endMs)); + } + + if (cursor < endMs) { + spans.push({ startMs: cursor, endMs }); + } + + return spans; +} + +function nearestRegionIndex(regions: Span[], timeMs: number): number { + let bestIndex = 0; + let bestDistance = Number.POSITIVE_INFINITY; + regions.forEach((region, index) => { + const distance = + timeMs < region.startMs + ? region.startMs - timeMs + : timeMs > region.endMs + ? timeMs - region.endMs + : 0; + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = index; + } + }); + return bestIndex; +} + +function joinText(existing: string, addition: string): string { + return [existing, addition] + .map((part) => part.trim()) + .filter(Boolean) + .join(" "); +} + +/** + * Distribute a cue's plain text across the regions it overlaps, proportionally to how much + * of the cue's duration falls in each region. Used only when a single Whisper cue straddles + * a long pause (rare) and has no word timing. + */ +function splitTextProportionally(text: string, overlaps: Span[]): string[] { + const tokens = text.trim().split(/\s+/).filter(Boolean); + if (tokens.length === 0) { + return overlaps.map(() => ""); + } + + const totalDurationMs = + overlaps.reduce((sum, span) => sum + (span.endMs - span.startMs), 0) || 1; + const result: string[] = []; + let cursor = 0; + + overlaps.forEach((span, index) => { + const isLast = index === overlaps.length - 1; + const share = (span.endMs - span.startMs) / totalDurationMs; + const count = isLast + ? tokens.length - cursor + : Math.min(tokens.length - cursor, Math.round(tokens.length * share)); + result.push(tokens.slice(cursor, cursor + count).join(" ")); + cursor += count; + }); + + if (cursor < tokens.length) { + result[result.length - 1] = joinText( + result[result.length - 1], + tokens.slice(cursor).join(" "), + ); + } + + return result; +} + +/** Pad spans toward neighbors by at most half the silent gap, so cues never overlap. */ +export function padSpans(spans: Span[], edgePadMs: number): void { + for (let index = 0; index < spans.length; index += 1) { + const prevEndMs = index > 0 ? spans[index - 1].endMs : null; + const nextStartMs = index < spans.length - 1 ? spans[index + 1].startMs : null; + const rawStart = spans[index].startMs; + const rawEnd = spans[index].endMs; + + const leftPad = + prevEndMs === null + ? Math.min(edgePadMs, rawStart) + : Math.min(edgePadMs, Math.floor(Math.max(0, rawStart - prevEndMs) / 2)); + const rightPad = + nextStartMs === null + ? edgePadMs + : Math.min(edgePadMs, Math.floor(Math.max(0, nextStartMs - rawEnd) / 2)); + + spans[index].startMs = Math.max(0, Math.round(rawStart - leftPad)); + spans[index].endMs = Math.max(spans[index].startMs + 1, Math.round(rawEnd + rightPad)); + } +} + +/** + * Re-segment Whisper cues against detected silence into phrases that only break on long + * pauses. Returns sorted, non-overlapping cues with fresh ids. If no silence is detected + * the cues are merged into a single trimmed phrase (continuous speech). + */ +export function resegmentCuesBySilence( + cues: CaptionCuePayload[], + silences: SilenceInterval[], + options: ResegmentOptions = {}, +): CaptionCuePayload[] { + const splitSilenceMs = options.splitSilenceMs ?? DEFAULT_SPLIT_SILENCE_MS; + const edgePadMs = options.edgePadMs ?? DEFAULT_EDGE_PAD_MS; + const minSpeechMs = options.minSpeechMs ?? DEFAULT_MIN_SPEECH_MS; + + if (cues.length === 0) { + return []; + } + + const sortedCues = [...cues].sort( + (left, right) => left.startMs - right.startMs || left.endMs - right.endMs, + ); + const transcriptStartMs = sortedCues[0].startMs; + const transcriptEndMs = sortedCues.reduce( + (max, cue) => Math.max(max, cue.endMs), + transcriptStartMs, + ); + + // Boundaries = long pauses, plus any silence touching the transcript edges (so leading + // and trailing silence is always trimmed even if short). + const boundaries: Span[] = silences + .map((silence) => ({ + startMs: Math.max(silence.startMs, transcriptStartMs), + endMs: Math.min(silence.endMs, transcriptEndMs), + })) + .filter((span) => span.endMs > span.startMs) + .filter( + (span) => + span.endMs - span.startMs >= splitSilenceMs || + span.startMs <= transcriptStartMs || + span.endMs >= transcriptEndMs, + ) + .sort((left, right) => left.startMs - right.startMs); + + const regions = subtractIntervals(transcriptStartMs, transcriptEndMs, boundaries).filter( + (region) => region.endMs - region.startMs >= minSpeechMs, + ); + if (regions.length === 0) { + return []; + } + + const regionTexts = regions.map(() => ""); + const regionWords: CaptionWordPayload[][] = regions.map(() => []); + + for (const cue of sortedCues) { + const overlapping = regions + .map((region, index) => ({ region, index })) + .filter(({ region }) => cue.startMs < region.endMs && cue.endMs > region.startMs); + if (overlapping.length === 0) { + continue; + } + + const words = Array.isArray(cue.words) ? (cue.words as CaptionWordPayload[]) : []; + if (words.length > 0) { + for (const word of words) { + const center = (word.startMs + word.endMs) / 2; + const match = overlapping.find( + ({ region }) => center >= region.startMs && center <= region.endMs, + ); + const targetIndex = + match?.index ?? + overlapping[ + nearestRegionIndex( + overlapping.map((entry) => entry.region), + center, + ) + ].index; + regionWords[targetIndex].push(word); + } + continue; + } + + if (overlapping.length === 1) { + const { index } = overlapping[0]; + regionTexts[index] = joinText(regionTexts[index], cue.text); + continue; + } + + const overlaps = overlapping.map(({ region }) => ({ + startMs: Math.max(cue.startMs, region.startMs), + endMs: Math.min(cue.endMs, region.endMs), + })); + const texts = splitTextProportionally(cue.text, overlaps); + overlapping.forEach(({ index }, position) => { + regionTexts[index] = joinText(regionTexts[index], texts[position]); + }); + } + + const pieces = regions + .map((region, index): CaptionPiece => { + const words = regionWords[index] + .slice() + .sort((left, right) => left.startMs - right.startMs || left.endMs - right.endMs); + if (words.length > 0) { + return { + startMs: words[0].startMs, + endMs: words[words.length - 1].endMs, + text: buildCaptionTextFromWords(words), + words, + }; + } + return { + startMs: region.startMs, + endMs: region.endMs, + text: regionTexts[index].trim(), + }; + }) + .filter((piece) => piece.text.trim().length > 0); + + pieces.sort((left, right) => left.startMs - right.startMs || left.endMs - right.endMs); + padSpans(pieces, edgePadMs); + + return pieces.map((piece, index) => ({ + id: `caption-${index + 1}`, + startMs: piece.startMs, + endMs: piece.endMs, + text: piece.text, + ...(piece.words && piece.words.length > 0 ? { words: piece.words } : {}), + })); +} diff --git a/electron/ipc/captions/whisper.ts b/electron/ipc/captions/whisper.ts new file mode 100644 index 000000000..67b04cd58 --- /dev/null +++ b/electron/ipc/captions/whisper.ts @@ -0,0 +1,155 @@ +import { createWriteStream } from "node:fs"; +import { constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import { get as httpsGet } from "node:https"; +import type Electron from "electron"; +import { + WHISPER_MODEL_DIR, + WHISPER_MODEL_DOWNLOAD_URL, + WHISPER_SMALL_MODEL_PATH, +} from "../constants"; + +export function sendWhisperModelDownloadProgress( + webContents: Electron.WebContents, + payload: { + status: "idle" | "downloading" | "downloaded" | "error"; + progress: number; + path?: string | null; + error?: string; + }, +) { + webContents.send("whisper-small-model-download-progress", payload); +} + +export async function getWhisperSmallModelStatus() { + try { + await fs.access(WHISPER_SMALL_MODEL_PATH, fsConstants.R_OK); + return { + success: true, + exists: true, + path: WHISPER_SMALL_MODEL_PATH, + }; + } catch { + return { + success: true, + exists: false, + path: null, + }; + } +} + +export function downloadFileWithProgress( + url: string, + destinationPath: string, + onProgress: (progress: number) => void, +): Promise { + const request = (currentUrl: string, redirectCount = 0): Promise => { + return new Promise((resolve, reject) => { + const req = httpsGet(currentUrl, { timeout: 30_000 }, (response) => { + const statusCode = response.statusCode ?? 0; + const location = response.headers.location; + + if (statusCode >= 300 && statusCode < 400 && location) { + response.resume(); + if (redirectCount >= 5) { + reject(new Error("Too many redirects while downloading Whisper model.")); + return; + } + + const nextUrl = new URL(location, currentUrl).toString(); + void request(nextUrl, redirectCount + 1) + .then(resolve) + .catch(reject); + return; + } + + if (statusCode < 200 || statusCode >= 300) { + response.resume(); + reject(new Error(`Whisper model download failed with status ${statusCode}.`)); + return; + } + + const totalBytes = Number.parseInt( + String(response.headers["content-length"] ?? "0"), + 10, + ); + let downloadedBytes = 0; + const fileStream = createWriteStream(destinationPath); + + response.on("data", (chunk: Buffer) => { + downloadedBytes += chunk.length; + if (Number.isFinite(totalBytes) && totalBytes > 0) { + onProgress(Math.min(100, Math.round((downloadedBytes / totalBytes) * 100))); + } + }); + + response.on("error", (error) => { + fileStream.destroy(error); + }); + + fileStream.on("error", (error) => { + response.destroy(error); + reject(error); + }); + + fileStream.on("finish", () => { + onProgress(100); + resolve(); + }); + + response.pipe(fileStream); + }); + + req.on("error", reject); + req.on("timeout", () => { + req.destroy(new Error("Whisper model download timed out.")); + }); + }); + }; + + return request(url); +} + +export async function downloadWhisperSmallModel( + webContents: Electron.WebContents, +): Promise { + await fs.mkdir(WHISPER_MODEL_DIR, { recursive: true }); + const tempPath = `${WHISPER_SMALL_MODEL_PATH}.download`; + + sendWhisperModelDownloadProgress(webContents, { + status: "downloading", + progress: 0, + path: null, + }); + + try { + await fs.rm(tempPath, { force: true }); + await downloadFileWithProgress(WHISPER_MODEL_DOWNLOAD_URL, tempPath, (progress) => { + sendWhisperModelDownloadProgress(webContents, { + status: "downloading", + progress, + path: null, + }); + }); + await fs.rename(tempPath, WHISPER_SMALL_MODEL_PATH); + sendWhisperModelDownloadProgress(webContents, { + status: "downloaded", + progress: 100, + path: WHISPER_SMALL_MODEL_PATH, + }); + return WHISPER_SMALL_MODEL_PATH; + } catch (error) { + await fs.rm(tempPath, { force: true }).catch(() => undefined); + sendWhisperModelDownloadProgress(webContents, { + status: "error", + progress: 0, + path: null, + error: String(error), + }); + throw error; + } +} + +export async function deleteWhisperSmallModel(): Promise { + await fs.rm(WHISPER_SMALL_MODEL_PATH, { force: true }); +} diff --git a/electron/ipc/capturePickerSources.test.ts b/electron/ipc/capturePickerSources.test.ts new file mode 100644 index 000000000..f6780f5b0 --- /dev/null +++ b/electron/ipc/capturePickerSources.test.ts @@ -0,0 +1,66 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +const mocks = vi.hoisted(() => ({ + exec: vi.fn(), + sources: vi.fn(), + mac: vi.fn(), + dip: vi.fn(), +})); +vi.mock("electron", () => ({ systemPreferences: { getMediaAccessStatus: () => "granted" } })); +vi.mock("node:util", () => ({ promisify: () => mocks.exec })); +vi.mock("./cursor/bounds", () => ({ + getNativeMacWindowsFrontToBack: mocks.mac, +})); +vi.mock("./register/sources", () => ({ getDesktopSources: mocks.sources })); +vi.mock("./utils", () => ({ + parseWindowId: (id?: string) => Number(id?.split(":")[1]), + getScreen: () => ({ screenToDipRect: mocks.dip }), +})); +import { listCapturePickerWindows } from "./capturePickerSources"; +const platform = process.platform; +afterEach(() => Object.defineProperty(process, "platform", { value: platform })); +beforeEach(() => { + vi.resetAllMocks(); + mocks.sources.mockResolvedValue([ + { id: "window:101:0", name: "Front" }, + { id: "window:102:0", name: "Back" }, + ]); +}); +describe("capture picker source adapters", () => { + it("keeps Linux window selection on its source list without X11 helper probes", async () => { + Object.defineProperty(process, "platform", { value: "linux" }); + await expect(listCapturePickerWindows()).rejects.toThrow("Use the source list on Linux"); + expect(mocks.exec).not.toHaveBeenCalled(); + }); + it("matches Windows handles to real capture IDs and converts physical bounds to DIP", async () => { + Object.defineProperty(process, "platform", { value: "win32" }); + mocks.exec.mockResolvedValue({ + stdout: JSON.stringify([ + { id: 999, x: 0, y: 0, width: 20, height: 20 }, + { id: 101, x: -1600, y: 0, width: 800, height: 600 }, + ]), + }); + mocks.dip.mockReturnValue({ x: -800, y: 0, width: 400, height: 300 }); + expect(await listCapturePickerWindows()).toEqual([ + { + id: "window:101:0", + appName: "", + title: "Front", + display_id: undefined, + x: -800, + y: 0, + width: 400, + height: 300, + }, + ]); + expect(mocks.dip).toHaveBeenCalledWith(null, expect.objectContaining({ x: -1600 })); + }); + it("excludes Recordly's own macOS windows and invalid geometry", async () => { + Object.defineProperty(process, "platform", { value: "darwin" }); + mocks.mac.mockResolvedValue([ + { id: "window:1:0", ownerPid: process.pid, x: 0, y: 0, width: 10, height: 10 }, + { id: "window:2:0", ownerPid: 99, name: "Other", x: 0, y: 0, width: 400, height: 300 }, + { id: "window:3:0", width: Number.NaN }, + ]); + expect((await listCapturePickerWindows()).map((entry) => entry.id)).toEqual(["window:2:0"]); + }); +}); diff --git a/electron/ipc/capturePickerSources.ts b/electron/ipc/capturePickerSources.ts new file mode 100644 index 000000000..37d78bbe2 --- /dev/null +++ b/electron/ipc/capturePickerSources.ts @@ -0,0 +1,106 @@ +import { systemPreferences } from "electron"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { getNativeMacWindowsFrontToBack } from "./cursor/bounds"; +import type { CapturePickerWindow, SelectedSource } from "./types"; +import { getScreen, parseWindowId } from "./utils"; +import { getDesktopSources } from "./register/sources"; + +const exec = promisify(execFile); + +// GetTopWindow/GetWindow walks front to back. Restrict the result to Electron's +// capturable sources rather than inventing source IDs or changing capture routes. +const WINDOWS_SCRIPT = `Add-Type -TypeDefinition @" +using System; using System.Runtime.InteropServices; using System.Collections.Generic; +public static class RecordlyPickerWindows { + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } + [DllImport("user32.dll")] static extern IntPtr GetTopWindow(IntPtr h); + [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint command); + [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h); + [DllImport("user32.dll")] static extern bool IsIconic(IntPtr h); + [DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr h, out RECT rect); + [DllImport("user32.dll")] static extern IntPtr SetThreadDpiAwarenessContext(IntPtr context); + [DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int attribute, out RECT rect, int size); + public class Entry { public long id; public int x, y, width, height; } + public static Entry[] List() { + SetThreadDpiAwarenessContext(new IntPtr(-4)); + var list = new List(); var seen = new HashSet(); + for (var h = GetTopWindow(IntPtr.Zero); h != IntPtr.Zero && seen.Add(h.ToInt64()); h = GetWindow(h, 2)) { + if (!IsWindowVisible(h) || IsIconic(h)) continue; + RECT rect; + if (DwmGetWindowAttribute(h, 9, out rect, Marshal.SizeOf(typeof(RECT))) != 0 && !GetWindowRect(h, out rect)) continue; + if (rect.Right <= rect.Left || rect.Bottom <= rect.Top) continue; + list.Add(new Entry { id = h.ToInt64(), x = rect.Left, y = rect.Top, width = rect.Right - rect.Left, height = rect.Bottom - rect.Top }); + } + return list.ToArray(); + } +} +"@ +ConvertTo-Json -InputObject @([RecordlyPickerWindows]::List()) -Compress`; + +export async function listCapturePickerWindows(): Promise { + if (process.platform === "darwin") { + if (systemPreferences.getMediaAccessStatus("screen") !== "granted") + throw new Error("Allow screen recording in System Settings before picking a window."); + const entries = await getNativeMacWindowsFrontToBack({ strict: true }); + return entries + .filter( + (entry) => + entry.ownerPid !== process.pid && + [entry.x, entry.y, entry.width, entry.height].every(Number.isFinite) && + (entry.width ?? 0) > 0 && + (entry.height ?? 0) > 0, + ) + .map((entry) => ({ + id: entry.id, + appName: entry.appName ?? "", + title: entry.windowTitle ?? entry.name, + display_id: entry.display_id, + x: entry.x!, + y: entry.y!, + width: entry.width!, + height: entry.height!, + })); + } + if (process.platform !== "win32") throw new Error("Use the source list on Linux."); + const sources = await getDesktopSources( + { + types: ["window"], + thumbnailSize: { width: 0, height: 0 }, + fetchWindowIcons: false, + }, + { strict: true }, + ); + const capturable = sources.filter( + (source): source is SelectedSource & { id: string } => + typeof source.id === "string" && source.id.startsWith("window:"), + ); + const byId = new Map(capturable.map((source) => [parseWindowId(source.id), source])); + const { stdout } = await exec( + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_SCRIPT], + { timeout: 10000, maxBuffer: 4 * 1024 * 1024 }, + ); + const entries: unknown = JSON.parse(stdout); + if (!Array.isArray(entries)) throw new Error("Unable to read window positions."); + return entries.flatMap((entry) => { + const source = byId.get(entry.id); + if ( + !source || + ![entry.x, entry.y, entry.width, entry.height].every(Number.isFinite) || + entry.width <= 0 || + entry.height <= 0 + ) + return []; + const bounds = getScreen().screenToDipRect(null, entry); + return [ + { + ...bounds, + id: source.id, + appName: "", + title: source.name, + display_id: source.display_id, + }, + ]; + }); +} diff --git a/electron/ipc/captureSelection.test.ts b/electron/ipc/captureSelection.test.ts new file mode 100644 index 000000000..54d4fe011 --- /dev/null +++ b/electron/ipc/captureSelection.test.ts @@ -0,0 +1,129 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +const mocks = vi.hoisted(() => ({ + read: vi.fn(), + write: vi.fn(), + windows: vi.fn(), + sources: vi.fn(), + current: null as unknown, + displays: [{ id: 1, bounds: { x: 0, y: 0, width: 1440, height: 1000 } }], +})); +vi.mock("../appSettingsStore", () => ({ + readAppSetting: mocks.read, + writeAppSetting: mocks.write, +})); +vi.mock("./capturePickerSources", () => ({ listCapturePickerWindows: mocks.windows })); +vi.mock("./register/sources", () => ({ getDesktopSources: mocks.sources })); +vi.mock("./state", () => ({ + get selectedSource() { + return mocks.current; + }, + setSelectedSource: (value: unknown) => { + mocks.current = value; + }, +})); +vi.mock("./utils", () => ({ getScreen: () => ({ getAllDisplays: () => mocks.displays }) })); +import { getSessionCaptureSource, validateCaptureSource } from "./captureSelection"; +const platform = process.platform; +const source = { id: "window:101:0", name: "Document", windowTitle: "Document", appName: "Notes" }; +beforeEach(() => { + vi.clearAllMocks(); + Object.defineProperty(process, "platform", { value: "darwin" }); + mocks.current = null; + mocks.read.mockReturnValue({ platform: "darwin", source }); + mocks.windows.mockResolvedValue([ + { id: source.id, appName: "Notes", title: "Document", x: 0, y: 0, width: 100, height: 100 }, + ]); +}); +afterEach(() => { + Object.defineProperty(process, "platform", { value: platform }); + vi.unstubAllEnvs(); +}); +describe("session capture selection", () => { + it("starts without a capture source even when the previous window is still available", async () => { + expect(await getSessionCaptureSource()).toBeNull(); + expect(mocks.current).toBeNull(); + }); + it("ignores persisted selection without enumerating capture sources on startup", () => { + expect(getSessionCaptureSource()).toBeNull(); + expect(mocks.read).not.toHaveBeenCalled(); + expect(mocks.windows).not.toHaveBeenCalled(); + expect(mocks.sources).not.toHaveBeenCalled(); + }); + it("rejects a recycled window ID with a different title", async () => { + mocks.windows.mockResolvedValue([ + { id: source.id, title: "Another document", appName: "Notes" }, + ]); + expect(await validateCaptureSource(source)).toBeNull(); + }); + it("rejects disconnected screens and areas that would be resized", async () => { + expect( + await validateCaptureSource({ id: "screen:2:0", name: "Gone", display_id: "2" }), + ).toBeNull(); + expect( + await validateCaptureSource({ + id: "area:1", + name: "Area", + display_id: "1", + area: { x: 1300, y: 0, width: 400, height: 300 }, + }), + ).toBeNull(); + }); + it("does not broaden window capture when detection fails", async () => { + mocks.windows.mockRejectedValue(new Error("permission denied")); + await expect(validateCaptureSource(source)).rejects.toThrow("permission denied"); + expect(mocks.sources).not.toHaveBeenCalled(); + }); + it("keeps a selection only within the current app session", () => { + mocks.current = source; + expect(getSessionCaptureSource()).toEqual(source); + // A fresh main process initializes its in-memory selection to null. + mocks.current = null; + expect(getSessionCaptureSource()).toBeNull(); + expect(mocks.read).not.toHaveBeenCalled(); + expect(mocks.write).not.toHaveBeenCalled(); + }); +}); + +describe("Linux selection", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "linux" }); + vi.stubEnv("XDG_SESSION_TYPE", "x11"); + mocks.sources.mockResolvedValue([{ ...source, display_id: "1" }]); + }); + it("validates X11 windows using live sources without geometry helpers", async () => { + expect(await validateCaptureSource(source)).toMatchObject({ + id: source.id, + name: source.name, + windowTitle: source.windowTitle, + display_id: "1", + sourceType: "window", + }); + expect(mocks.windows).not.toHaveBeenCalled(); + expect(mocks.sources).toHaveBeenCalledWith( + { types: ["window"], thumbnailSize: { width: 0, height: 0 } }, + { strict: true }, + ); + }); + it("rejects missing and recycled X11 windows without substitution", async () => { + mocks.sources.mockResolvedValue([{ id: source.id, name: "Different document" }]); + expect(await validateCaptureSource(source)).toBeNull(); + mocks.sources.mockResolvedValue([{ ...source, id: "window:102:0" }]); + expect(await validateCaptureSource(source)).toBeNull(); + }); + it("starts Wayland unselected without opening the portal", () => { + vi.stubEnv("XDG_SESSION_TYPE", "wayland"); + expect(getSessionCaptureSource()).toBeNull(); + expect(mocks.read).not.toHaveBeenCalled(); + expect(mocks.sources).not.toHaveBeenCalled(); + expect(mocks.windows).not.toHaveBeenCalled(); + }); + it("rejects old X11 IDs on Wayland without enumerating portal sources", async () => { + vi.stubEnv("XDG_SESSION_TYPE", "wayland"); + expect(await validateCaptureSource(source)).toBeNull(); + expect( + await validateCaptureSource({ id: "screen:1:0", name: "Screen", display_id: "1" }), + ).toBeNull(); + expect(mocks.sources).not.toHaveBeenCalled(); + expect(mocks.windows).not.toHaveBeenCalled(); + }); +}); diff --git a/electron/ipc/captureSelection.ts b/electron/ipc/captureSelection.ts new file mode 100644 index 000000000..de29c83d9 --- /dev/null +++ b/electron/ipc/captureSelection.ts @@ -0,0 +1,98 @@ +import { listCapturePickerWindows } from "./capturePickerSources"; +import { + createAreaSource, + createWindowSource, + getSourceArea, + normalizeCapturePick, +} from "./sourceArea"; +import { selectedSource } from "./state"; +import type { SelectedSource } from "./types"; +import { getScreen } from "./utils"; +import { getDesktopSources } from "./register/sources"; +import { + isLikelyLinuxWaylandSession, + LINUX_PORTAL_SCREEN_SOURCE_ID, +} from "./register/sourceMapping"; + +/** Revalidate identity and geometry; never substitute a broader capture source. */ +export async function validateCaptureSource(candidate: unknown): Promise { + if (!candidate || typeof candidate !== "object") return null; + const source = candidate as Partial; + if (typeof source.id !== "string" || typeof source.name !== "string") return null; + if (source.id === LINUX_PORTAL_SCREEN_SOURCE_ID) + return process.platform === "linux" && isLikelyLinuxWaylandSession(process.env) + ? { id: source.id, name: "System picker", sourceType: "screen" } + : null; + if (process.platform === "linux" && isLikelyLinuxWaylandSession(process.env)) return null; + if (source.id.startsWith("window:") && process.platform === "linux") { + const windows = await getDesktopSources( + { types: ["window"], thumbnailSize: { width: 0, height: 0 } }, + { strict: true }, + ); + const match = windows.find( + (entry) => + entry.id === source.id && + (entry.windowTitle ?? entry.name) === (source.windowTitle ?? source.name), + ); + return match + ? { + id: match.id, + name: match.name, + windowTitle: match.windowTitle ?? match.name, + display_id: match.display_id, + sourceType: "window", + } + : null; + } + if (source.id.startsWith("window:")) { + const windows = await listCapturePickerWindows(); + const match = windows.find( + (entry) => + entry.id === source.id && + (entry.title || entry.appName || "Window") === + (source.windowTitle ?? source.name) && + (!source.appName || entry.appName === source.appName), + ); + return match ? createWindowSource(match) : null; + } + const display = getScreen() + .getAllDisplays() + .find((entry) => String(entry.id) === source.display_id); + if (!display) return null; + if (source.id.startsWith("area:")) { + if (process.platform !== "darwin") return null; + const area = getSourceArea(source as SelectedSource); + if (!area) return null; + const normalized = normalizeCapturePick( + { ...area, kind: "area", displayId: display.id, record: false }, + [{ id: display.id, bounds: display.bounds }], + [], + ); + // A changed display must invalidate the area rather than silently resize it. + if ( + !normalized || + normalized.kind !== "area" || + ["x", "y", "width", "height"].some( + (key) => normalized[key as keyof typeof area] !== area[key as keyof typeof area], + ) + ) + return null; + return createAreaSource(normalized); + } + if (!source.id.startsWith("screen:")) return null; + const sources = await getDesktopSources( + { types: ["screen"], thumbnailSize: { width: 0, height: 0 } }, + { strict: true }, + ); + const match = sources.find( + (entry) => entry.id === source.id && entry.display_id === source.display_id, + ); + return match + ? { id: match.id, name: match.name, display_id: match.display_id, sourceType: "screen" } + : null; +} + +/** A new main process starts unselected; old saved preferences are never restored. */ +export function getSessionCaptureSource(): SelectedSource | null { + return selectedSource; +} diff --git a/electron/ipc/cloudShareContract.ts b/electron/ipc/cloudShareContract.ts new file mode 100644 index 000000000..d8dfc8d7b --- /dev/null +++ b/electron/ipc/cloudShareContract.ts @@ -0,0 +1,127 @@ +export type CloudShareTicket = { + uploadUrl: string; + shareUrl: string; + shareCode?: string; + method: "PUT"; + headers: Record; + finalizeUrl?: string; +}; + +const TRUSTED_SHARE_ORIGINS = new Set([ + "https://videos.recordly.dev", + "https://recordly-share-staging.youngchen3442.workers.dev", + "http://localhost:8787", + "http://127.0.0.1:8787", +]); + +function parseUrl(value: unknown, label: string): URL { + if (typeof value !== "string") throw new Error(`${label} is missing.`); + const url = new URL(value); + if (url.username || url.password) throw new Error(`${label} cannot contain credentials.`); + return url; +} + +export function normalizeCloudEndpoint(value: unknown): string { + if (typeof value !== "string" || value.trim().length === 0) { + throw new Error("Enter a cloud share endpoint."); + } + const url = parseUrl(value.trim(), "Cloud share endpoint"); + if (!TRUSTED_SHARE_ORIGINS.has(url.origin)) { + throw new Error("Cloud sharing is only allowed through the Recordly service."); + } + if (url.pathname !== "/api/upload" || url.search) { + throw new Error("The cloud share endpoint is invalid."); + } + url.hash = ""; + return url.toString(); +} + +function parseHttpUrl(value: unknown, label: string): string { + const url = parseUrl(value, `${label} from the server response`); + const localHttp = + url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1"); + if (url.protocol !== "https:" && !localHttp) { + throw new Error(`${label} must use HTTPS (HTTP is allowed for localhost).`); + } + return url.toString(); +} + +export function parseCloudShareTicket(value: unknown): CloudShareTicket { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("The cloud share server returned an invalid response."); + } + const input = value as Record; + const uploadUrl = input.uploadUrl ?? input.uploadURL; + const shareUrl = input.shareUrl ?? input.shareURL; + const shareCode = + typeof input.shareCode === "string" && /^[a-z0-9]+$/.test(input.shareCode) + ? input.shareCode + : undefined; + const method = input.method === undefined ? "PUT" : input.method; + if (method !== "PUT") throw new Error("Only presigned PUT uploads are currently supported."); + const headers: Record = {}; + if (input.headers !== undefined) { + if (!input.headers || typeof input.headers !== "object" || Array.isArray(input.headers)) { + throw new Error("Upload headers must be an object."); + } + for (const [key, headerValue] of Object.entries(input.headers)) { + if (typeof headerValue !== "string") + throw new Error("Upload header values must be strings."); + if ( + [ + "authorization", + "cookie", + "host", + "content-length", + "proxy-authorization", + ].includes(key.toLowerCase()) + ) { + throw new Error(`The upload header ${key} is not allowed.`); + } + headers[key] = headerValue; + } + } + const parsedUploadUrl = parseHttpUrl(uploadUrl, "uploadUrl"); + const parsedShareUrl = parseHttpUrl(shareUrl, "shareUrl"); + let finalizeUrl: string | undefined; + if (typeof input.finalizeUrl === "string") { + finalizeUrl = parseHttpUrl(input.finalizeUrl, "finalizeUrl"); + } else if (typeof input.shareCode === "string" && /^[a-z0-9]+$/.test(input.shareCode)) { + // Voom marks a recording ready after metadata is posted. + finalizeUrl = new URL(`/api/metadata/${input.shareCode}`, parsedShareUrl).toString(); + } + return { + uploadUrl: parsedUploadUrl, + shareUrl: parsedShareUrl, + shareCode, + method, + headers, + finalizeUrl, + }; +} + +/** Validates untrusted library JSON before presenting recording metadata to the renderer. */ +export function parseCloudRecordingList(data: unknown, endpoint: string) { + if (!data || typeof data !== "object" || !("videos" in data) || !Array.isArray(data.videos)) + throw new Error("Invalid recordings response."); + return data.videos.slice(0, 100).map((raw: unknown) => { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) + throw new Error("Invalid recording response."); + const video = raw as Record; + if ( + typeof video.share_code !== "string" || + !/^[a-z0-9]{1,128}$/.test(video.share_code) || + typeof video.title !== "string" + ) + throw new Error("Invalid recording response."); + const size = Number(video.file_size); + return { + code: video.share_code, + title: video.title, + size: Number.isFinite(size) && size >= 0 ? size : 0, + createdAt: typeof video.created_at === "string" ? video.created_at : undefined, + ready: video.upload_completed === 1, + url: new URL(`/s/${video.share_code}`, endpoint).href, + }; + }); +} diff --git a/electron/ipc/constants.ts b/electron/ipc/constants.ts new file mode 100644 index 000000000..9e438b4b4 --- /dev/null +++ b/electron/ipc/constants.ts @@ -0,0 +1,29 @@ +import path from "node:path"; +import { USER_DATA_PATH } from "../appPaths"; + +export const PROJECT_FILE_EXTENSION = "recordly"; +export const LEGACY_PROJECT_FILE_EXTENSIONS = ["openscreen"]; +export const PROJECTS_DIRECTORY_NAME = "Projects"; +export const PROJECT_THUMBNAIL_SUFFIX = ".preview.png"; +export const RECENT_PROJECTS_FILE = path.join(USER_DATA_PATH, "recent-projects.json"); +export const MAX_RECENT_PROJECTS = 16; +export const SHORTCUTS_FILE = path.join(USER_DATA_PATH, "shortcuts.json"); +export const RECORDINGS_SETTINGS_FILE = path.join(USER_DATA_PATH, "recordings-settings.json"); +export const COUNTDOWN_SETTINGS_FILE = path.join(USER_DATA_PATH, "countdown-settings.json"); +export const APP_SETTINGS_FILE = path.join(USER_DATA_PATH, "app-settings.json"); +export const AUTO_RECORDING_PREFIX = "recording-"; +export const ALLOW_RECORDLY_WINDOW_CAPTURE = Boolean(process.env["VITE_DEV_SERVER_URL"]); +export const RECORDING_SESSION_MANIFEST_SUFFIX = ".recordly-session.json"; +export const WHISPER_MODEL_DOWNLOAD_URL = + "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin"; +export const WHISPER_MODEL_DIR = path.join(USER_DATA_PATH, "whisper"); +export const WHISPER_SMALL_MODEL_PATH = path.join(WHISPER_MODEL_DIR, "ggml-small.bin"); +export const COMPANION_AUDIO_LAYOUTS = [ + { platform: "mac" as const, systemSuffix: ".system.m4a", micSuffix: ".mic.m4a" }, + { platform: "win" as const, systemSuffix: ".system.wav", micSuffix: ".mic.wav" }, + { platform: "mac" as const, systemSuffix: ".system.webm", micSuffix: ".mic.webm" }, +]; + +export const CURSOR_TELEMETRY_VERSION = 2; +export const CURSOR_SAMPLE_INTERVAL_MS = 33; +export const MAX_CURSOR_SAMPLES = 60 * 60 * 30; // 1 hour @ 30Hz diff --git a/electron/ipc/cursor/bounds.test.ts b/electron/ipc/cursor/bounds.test.ts new file mode 100644 index 000000000..4e2da0610 --- /dev/null +++ b/electron/ipc/cursor/bounds.test.ts @@ -0,0 +1,29 @@ +import { expect, it, vi } from "vitest"; +const exec = vi.hoisted(() => vi.fn()); +vi.mock("node:child_process", () => ({ execFile: exec })); +vi.mock("node:util", () => ({ promisify: () => exec })); +vi.mock("../paths/binaries", () => ({ ensureNativeWindowListBinary: vi.fn() })); +vi.mock("../state", () => ({})); +vi.mock("../utils", () => ({ + parseWindowId: (id: string) => + id.startsWith("window:") ? Number(id.split(":")[1]) || null : null, +})); +vi.mock("../windowsWindowControl", () => ({ resolveWindowsWindowBounds: vi.fn() })); +import { resolveLinuxWindowBounds } from "./bounds"; + +it("does not track a different same-title X11 window after the selected window disappears", async () => { + exec.mockReset().mockRejectedValue(new Error("No such window")); + expect(await resolveLinuxWindowBounds({ id: "window:101:0", name: "Document" })).toBeNull(); + expect(exec).toHaveBeenCalledExactlyOnceWith("xwininfo", ["-id", "101"], { timeout: 1500 }); +}); +it("returns live X11 bounds by ID", async () => { + exec.mockReset().mockResolvedValue({ + stdout: "Absolute upper-left X: -100\nAbsolute upper-left Y: 50\nWidth: 640\nHeight: 480", + }); + expect(await resolveLinuxWindowBounds({ id: "window:101:0", name: "Document" })).toEqual({ + x: -100, + y: 50, + width: 640, + height: 480, + }); +}); diff --git a/electron/ipc/cursor/bounds.ts b/electron/ipc/cursor/bounds.ts new file mode 100644 index 000000000..bb5da7a64 --- /dev/null +++ b/electron/ipc/cursor/bounds.ts @@ -0,0 +1,246 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { ensureNativeWindowListBinary } from "../paths/binaries"; +import { + cachedNativeMacWindowSources, + cachedNativeMacWindowSourcesAtMs, + interactionCaptureCleanup, + selectedSource, + setCachedNativeMacWindowSources, + setCachedNativeMacWindowSourcesAtMs, + setInteractionCaptureCleanup, + setSelectedWindowBounds, + setWindowBoundsCaptureInterval, + windowBoundsCaptureInterval, +} from "../state"; +import type { NativeMacWindowSource, SelectedSource, WindowBounds } from "../types"; +import { parseWindowId } from "../utils"; +import { resolveWindowsWindowBounds } from "../windowsWindowControl"; + +const execFileAsync = promisify(execFile); + +export async function getNativeMacWindowSources(options?: { maxAgeMs?: number }) { + if (process.platform !== "darwin") { + return [] as NativeMacWindowSource[]; + } + + const maxAgeMs = options?.maxAgeMs ?? 5000; + const now = Date.now(); + if (cachedNativeMacWindowSources && now - cachedNativeMacWindowSourcesAtMs < maxAgeMs) { + return cachedNativeMacWindowSources; + } + + try { + const binaryPath = await ensureNativeWindowListBinary(); + const { stdout } = await execFileAsync(binaryPath, [], { + timeout: 30000, + maxBuffer: 10 * 1024 * 1024, + }); + + const parsed = JSON.parse(stdout); + if (!Array.isArray(parsed)) { + return [] as NativeMacWindowSource[]; + } + + const entries = parsed.filter((entry: unknown): entry is NativeMacWindowSource => { + if (!entry || typeof entry !== "object") { + return false; + } + + const candidate = entry as Partial; + return typeof candidate.id === "string" && typeof candidate.name === "string"; + }); + + setCachedNativeMacWindowSources(entries); + setCachedNativeMacWindowSourcesAtMs(now); + return entries; + } catch { + return cachedNativeMacWindowSources ?? ([] as NativeMacWindowSource[]); + } +} + +/** On-screen windows ordered front to back, for finding the window under a point. */ +export async function getNativeMacWindowsFrontToBack(options?: { + strict?: boolean; +}): Promise { + if (process.platform !== "darwin") { + return []; + } + + try { + const binaryPath = await ensureNativeWindowListBinary(); + const { stdout } = await execFileAsync(binaryPath, ["--z-order"], { + timeout: 10000, + maxBuffer: 10 * 1024 * 1024, + }); + const parsed: unknown = JSON.parse(stdout); + if (!Array.isArray(parsed)) { + if (options?.strict) throw new Error("Unable to read window positions."); + return []; + } + return parsed.filter( + (entry): entry is NativeMacWindowSource => + Boolean(entry) && + typeof entry === "object" && + typeof (entry as NativeMacWindowSource).id === "string", + ); + } catch (error) { + if (options?.strict) + throw new Error( + "Unable to detect windows. Check screen recording permission and try again.", + ); + console.warn("Failed to list windows front to back:", error); + return []; + } +} + +export function getWindowBoundsFromNativeSource( + source?: NativeMacWindowSource | null, +): WindowBounds | null { + if (!source) { + return null; + } + + const { x, y, width, height } = source; + if ( + typeof x !== "number" || + !Number.isFinite(x) || + typeof y !== "number" || + !Number.isFinite(y) || + typeof width !== "number" || + !Number.isFinite(width) || + typeof height !== "number" || + !Number.isFinite(height) + ) { + return null; + } + + if (width <= 0 || height <= 0) { + return null; + } + + return { x, y, width, height }; +} + +export async function resolveMacWindowBounds(source: SelectedSource): Promise { + const windowId = parseWindowId(source.id); + if (!windowId) { + return null; + } + + try { + const nativeSources = await getNativeMacWindowSources({ maxAgeMs: 250 }); + const matchedSource = nativeSources.find((entry) => parseWindowId(entry.id) === windowId); + return getWindowBoundsFromNativeSource(matchedSource); + } catch { + return null; + } +} + +export function parseXwininfoBounds(stdout: string): WindowBounds | null { + const absX = stdout.match(/Absolute upper-left X:\s+(-?\d+)/); + const absY = stdout.match(/Absolute upper-left Y:\s+(-?\d+)/); + const width = stdout.match(/Width:\s+(\d+)/); + const height = stdout.match(/Height:\s+(\d+)/); + + if (!absX || !absY || !width || !height) { + return null; + } + + return { + x: Number.parseInt(absX[1], 10), + y: Number.parseInt(absY[1], 10), + width: Number.parseInt(width[1], 10), + height: Number.parseInt(height[1], 10), + }; +} + +export async function resolveLinuxWindowBounds( + source: SelectedSource, +): Promise { + const windowId = parseWindowId(source?.id); + + if (windowId) { + try { + const { stdout } = await execFileAsync("xwininfo", ["-id", String(windowId)], { + timeout: 1500, + }); + const bounds = parseXwininfoBounds(stdout); + if (bounds && bounds.width > 0 && bounds.height > 0) { + return bounds; + } + } catch { + return null; + } + // Do not track a different same-title window after the selected ID disappears. + return null; + } + + const windowTitle = + typeof source.windowTitle === "string" ? source.windowTitle.trim() : source.name.trim(); + if (!windowTitle) { + return null; + } + + try { + const { stdout } = await execFileAsync("xwininfo", ["-name", windowTitle], { + timeout: 1500, + }); + const bounds = parseXwininfoBounds(stdout); + return bounds && bounds.width > 0 && bounds.height > 0 ? bounds : null; + } catch { + return null; + } +} + +export function stopInteractionCapture() { + if (interactionCaptureCleanup) { + interactionCaptureCleanup(); + setInteractionCaptureCleanup(null); + } +} + +export function stopWindowBoundsCapture() { + if (windowBoundsCaptureInterval) { + clearInterval(windowBoundsCaptureInterval); + setWindowBoundsCaptureInterval(null); + } + setSelectedWindowBounds(null); +} + +async function refreshSelectedWindowBounds() { + if (!selectedSource?.id?.startsWith("window:")) { + setSelectedWindowBounds(null); + return; + } + + let bounds: WindowBounds | null = null; + + if (process.platform === "darwin") { + bounds = await resolveMacWindowBounds(selectedSource); + } else if (process.platform === "win32") { + bounds = await resolveWindowsWindowBounds(selectedSource); + } else if (process.platform === "linux") { + bounds = await resolveLinuxWindowBounds(selectedSource); + } + + setSelectedWindowBounds(bounds); +} + +export function startWindowBoundsCapture() { + stopWindowBoundsCapture(); + + if ( + !["darwin", "win32", "linux"].includes(process.platform) || + !selectedSource?.id?.startsWith("window:") + ) { + return; + } + + void refreshSelectedWindowBounds(); + setWindowBoundsCaptureInterval( + setInterval(() => { + void refreshSelectedWindowBounds(); + }, 250), + ); +} diff --git a/electron/ipc/cursor/interaction.test.ts b/electron/ipc/cursor/interaction.test.ts new file mode 100644 index 000000000..4ea662414 --- /dev/null +++ b/electron/ipc/cursor/interaction.test.ts @@ -0,0 +1,92 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("electron", () => ({ + app: { + getPath: vi.fn(() => "/tmp"), + setPath: vi.fn(), + isReady: vi.fn(() => true), + }, +})); + +import { + repairBundledUiohookBinaryForCurrentArch, + shouldStartGlobalInteractionHook, +} from "./interaction"; + +describe("shouldStartGlobalInteractionHook", () => { + it("does not start the synchronous uiohook event tap on macOS", () => { + expect(shouldStartGlobalInteractionHook("darwin")).toBe(false); + }); + + it("keeps global interaction capture enabled on Windows and Linux", () => { + expect(shouldStartGlobalInteractionHook("win32")).toBe(true); + expect(shouldStartGlobalInteractionHook("linux")).toBe(true); + }); +}); + +describe("repairBundledUiohookBinaryForCurrentArch", () => { + const tempRoots: string[] = []; + + afterEach(async () => { + await Promise.all( + tempRoots + .splice(0) + .map((tempRoot) => fs.rm(tempRoot, { recursive: true, force: true })), + ); + }); + + it("promotes the bundled darwin-arm64 prebuild over a stale incompatible build", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-uiohook-")); + tempRoots.push(tempRoot); + + const packageRoot = path.join(tempRoot, "uiohook-napi"); + const prebuildPath = path.join(packageRoot, "prebuilds", "darwin-arm64", "node.napi.node"); + const buildPath = path.join(packageRoot, "build", "Release", "uiohook_napi.node"); + await fs.mkdir(path.dirname(prebuildPath), { recursive: true }); + await fs.mkdir(path.dirname(buildPath), { recursive: true }); + await fs.writeFile(prebuildPath, "arm64-prebuild"); + await fs.writeFile(buildPath, "x64-build"); + + const log = vi.fn(); + const repaired = repairBundledUiohookBinaryForCurrentArch( + Object.assign( + new Error( + "mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')", + ), + { + code: "ERR_DLOPEN_FAILED", + }, + ), + { packageRoot, platform: "darwin", arch: "arm64", log }, + ); + + expect(repaired).toBe(true); + expect(await fs.readFile(buildPath, "utf8")).toBe("arm64-prebuild"); + expect(log).toHaveBeenCalledWith( + "[CursorTelemetry] Repaired stale uiohook-napi binary using bundled darwin-arm64 prebuild.", + ); + }); + + it("does not rewrite binaries for unrelated load failures", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-uiohook-")); + tempRoots.push(tempRoot); + + const packageRoot = path.join(tempRoot, "uiohook-napi"); + const buildPath = path.join(packageRoot, "build", "Release", "uiohook_napi.node"); + await fs.mkdir(path.dirname(buildPath), { recursive: true }); + await fs.writeFile(buildPath, "existing-build"); + + const repaired = repairBundledUiohookBinaryForCurrentArch( + Object.assign(new Error("some other dlopen failure"), { + code: "ERR_DLOPEN_FAILED", + }), + { packageRoot, platform: "darwin", arch: "arm64" }, + ); + + expect(repaired).toBe(false); + expect(await fs.readFile(buildPath, "utf8")).toBe("existing-build"); + }); +}); diff --git a/electron/ipc/cursor/interaction.ts b/electron/ipc/cursor/interaction.ts new file mode 100644 index 000000000..47c42437f --- /dev/null +++ b/electron/ipc/cursor/interaction.ts @@ -0,0 +1,333 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { + hasLoggedInteractionHookFailure, + interactionCaptureCleanup, + isCursorCaptureActive, + lastLeftClick, + setHasLoggedInteractionHookFailure, + setInteractionCaptureCleanup, + setLastLeftClick, + setLinuxCursorScreenPoint, +} from "../state"; +import type { + CursorInteractionType, + HookMouseEvent, + UiohookLike, + UiohookModuleNamespace, +} from "../types"; +import { + getCursorCaptureElapsedMs, + getHookCursorScreenPoint, + getNormalizedCursorPoint, + isCursorCapturePaused, + pushCursorSample, +} from "./telemetry"; + +const nodeRequire = createRequire(import.meta.url); + +export function normalizeHookMouseButton(rawButton: unknown): 1 | 2 | 3 { + if (typeof rawButton !== "number" || !Number.isFinite(rawButton)) { + return 1; + } + + if (rawButton === 2 || rawButton === 39) { + return 2; + } + + if (rawButton === 3 || rawButton === 38) { + return 3; + } + + return 1; +} + +export function getHookMouseButton(event: HookMouseEvent | null | undefined): 1 | 2 | 3 { + return normalizeHookMouseButton( + event?.button ?? event?.mouseButton ?? event?.data?.button ?? event?.data?.mouseButton, + ); +} + +export function stopInteractionCapture() { + if (interactionCaptureCleanup) { + interactionCaptureCleanup(); + setInteractionCaptureCleanup(null); + } +} + +function isUiohookLike(value: unknown): value is UiohookLike { + const candidate = value as Partial | null; + return typeof candidate?.on === "function" && typeof candidate?.start === "function"; +} + +function resolveUiohookModule(moduleExports: UiohookModuleNamespace) { + const defaultExport = moduleExports.default; + + if (moduleExports.uIOhook) { + return moduleExports.uIOhook; + } + + if (moduleExports.uiohook) { + return moduleExports.uiohook; + } + + if (moduleExports.Uiohook) { + return moduleExports.Uiohook; + } + + if (isUiohookLike(defaultExport)) { + return defaultExport; + } + + if (defaultExport?.uIOhook) { + return defaultExport.uIOhook; + } + + if (defaultExport?.uiohook) { + return defaultExport.uiohook; + } + + if (defaultExport?.Uiohook) { + return defaultExport.Uiohook; + } + + return null; +} + +function shouldRepairBundledUiohookBinary(error: unknown): error is NodeJS.ErrnoException { + if (process.platform !== "darwin") { + return false; + } + + if (process.arch !== "arm64") { + return false; + } + + const candidate = error as NodeJS.ErrnoException | null; + return ( + candidate?.code === "ERR_DLOPEN_FAILED" && + typeof candidate.message === "string" && + candidate.message.includes("incompatible architecture") + ); +} + +export function repairBundledUiohookBinaryForCurrentArch( + error: unknown, + options?: { + packageRoot?: string; + platform?: NodeJS.Platform; + arch?: string; + log?: (message: string) => void; + }, +) { + const platform = options?.platform ?? process.platform; + const arch = options?.arch ?? process.arch; + + if (platform !== "darwin" || arch !== "arm64") { + return false; + } + + const candidate = error as NodeJS.ErrnoException | null; + if ( + candidate?.code !== "ERR_DLOPEN_FAILED" || + typeof candidate.message !== "string" || + !candidate.message.includes("incompatible architecture") + ) { + return false; + } + + const packageRoot = + options?.packageRoot ?? path.dirname(nodeRequire.resolve("uiohook-napi/package.json")); + const prebuildPath = path.join(packageRoot, "prebuilds", `darwin-${arch}`, "node.napi.node"); + const buildPath = path.join(packageRoot, "build", "Release", "uiohook_napi.node"); + + if (!fs.existsSync(prebuildPath)) { + return false; + } + + try { + fs.mkdirSync(path.dirname(buildPath), { recursive: true }); + fs.copyFileSync(prebuildPath, buildPath); + (options?.log ?? console.warn)( + "[CursorTelemetry] Repaired stale uiohook-napi binary using bundled darwin-arm64 prebuild.", + ); + return true; + } catch { + return false; + } +} + +function loadUiohookModule() { + try { + const moduleExports = nodeRequire("uiohook-napi") as UiohookModuleNamespace; + return resolveUiohookModule(moduleExports); + } catch (error) { + if (!shouldRepairBundledUiohookBinary(error)) { + throw error; + } + + if (!repairBundledUiohookBinaryForCurrentArch(error)) { + throw error; + } + + delete nodeRequire.cache[nodeRequire.resolve("uiohook-napi")]; + const moduleExports = nodeRequire("uiohook-napi") as UiohookModuleNamespace; + return resolveUiohookModule(moduleExports); + } +} + +export function shouldStartGlobalInteractionHook(platform: NodeJS.Platform = process.platform) { + // On macOS, uiohook can block forever while its native event tap starts + // (notably when Accessibility permission is unavailable or stale). Because + // start() executes synchronously, that freezes Electron's main thread and + // makes every window, including the recording HUD, unresponsive. Cursor + // position and visual-state telemetry still come from the existing native + // macOS monitor and Electron sampler. + return platform !== "darwin"; +} + +export function recordCursorMouseDown(button: 1 | 2 | 3) { + if (!isCursorCaptureActive || isCursorCapturePaused()) { + return; + } + + const point = getNormalizedCursorPoint(); + if (!point) { + return; + } + + const timeMs = getCursorCaptureElapsedMs(); + let interactionType: CursorInteractionType = "click"; + + if (button === 2) { + interactionType = "right-click"; + } else if (button === 3) { + interactionType = "middle-click"; + } else { + const thresholdMs = 350; + const distance = lastLeftClick + ? Math.hypot(point.cx - lastLeftClick.cx, point.cy - lastLeftClick.cy) + : Number.POSITIVE_INFINITY; + + if (lastLeftClick && timeMs - lastLeftClick.timeMs <= thresholdMs && distance <= 0.04) { + interactionType = "double-click"; + } + + setLastLeftClick({ timeMs, cx: point.cx, cy: point.cy }); + } + + pushCursorSample(point.cx, point.cy, timeMs, interactionType); +} + +export function recordCursorMouseUp() { + if (!isCursorCaptureActive || isCursorCapturePaused()) { + return; + } + + const point = getNormalizedCursorPoint(); + if (!point) { + return; + } + + pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "mouseup"); +} + +export async function startInteractionCapture() { + if (!isCursorCaptureActive) { + return; + } + + if (!["darwin", "win32", "linux"].includes(process.platform)) { + return; + } + + if (!shouldStartGlobalInteractionHook()) { + console.warn("[CursorTelemetry] Skipping the blocking global interaction hook on macOS."); + return; + } + + stopInteractionCapture(); + + try { + const hook = loadUiohookModule(); + console.log( + "[CursorTelemetry] hook loaded:", + !!hook, + "has.on:", + typeof hook?.on, + "has.start:", + typeof hook?.start, + ); + if (!isCursorCaptureActive) { + return; + } + + if (!hook || typeof hook.on !== "function" || typeof hook.start !== "function") { + console.log("[CursorTelemetry] hook unusable — aborting interaction capture"); + return; + } + + const onMouseDown = (event: HookMouseEvent) => { + recordCursorMouseDown(getHookMouseButton(event)); + }; + + const onMouseUp = () => { + recordCursorMouseUp(); + }; + + const onMouseMove = (event: HookMouseEvent) => { + if (process.platform !== "linux" || !isCursorCaptureActive || isCursorCapturePaused()) { + return; + } + + const point = getHookCursorScreenPoint(event); + if (!point) { + return; + } + + setLinuxCursorScreenPoint({ x: point.x, y: point.y, updatedAt: Date.now() }); + }; + + hook.on("mousedown", onMouseDown); + hook.on("mouseup", onMouseUp); + if (process.platform === "linux") { + hook.on("mousemove", onMouseMove); + } + + setInteractionCaptureCleanup(() => { + try { + if (typeof hook.off === "function") { + hook.off("mousedown", onMouseDown); + hook.off("mouseup", onMouseUp); + if (process.platform === "linux") { + hook.off("mousemove", onMouseMove); + } + } else if (typeof hook.removeListener === "function") { + hook.removeListener("mousedown", onMouseDown); + hook.removeListener("mouseup", onMouseUp); + if (process.platform === "linux") { + hook.removeListener("mousemove", onMouseMove); + } + } + } catch { + // ignore listener cleanup errors + } + + try { + if (typeof hook.stop === "function") { + hook.stop(); + } + } catch { + // ignore hook shutdown errors + } + }); + + hook.start(); + } catch (error) { + if (!hasLoggedInteractionHookFailure) { + setHasLoggedInteractionHookFailure(true); + console.warn("[CursorTelemetry] Global interaction capture unavailable:", error); + } + } +} diff --git a/electron/ipc/cursor/monitor.ts b/electron/ipc/cursor/monitor.ts new file mode 100644 index 000000000..8a507d56d --- /dev/null +++ b/electron/ipc/cursor/monitor.ts @@ -0,0 +1,163 @@ +import { spawn } from "node:child_process"; +import { constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import { BrowserWindow } from "electron"; +import { ensureNativeCursorMonitorBinary, getCursorMonitorExePath } from "../paths/binaries"; +import { + currentCursorVisualType, + nativeCursorMonitorOutputBuffer, + nativeCursorMonitorProcess, + setCurrentCursorVisualType, + setNativeCursorMonitorOutputBuffer, + setNativeCursorMonitorProcess, +} from "../state"; +import type { CursorVisualType } from "../types"; +import { recordCursorMouseDown, recordCursorMouseUp } from "./interaction"; + +export function emitCursorStateChanged(cursorType: CursorVisualType) { + BrowserWindow.getAllWindows().forEach((window) => { + if (!window.isDestroyed()) { + window.webContents.send("cursor-state-changed", { cursorType }); + } + }); +} + +export function handleCursorMonitorStdout(chunk: Buffer) { + setNativeCursorMonitorOutputBuffer(nativeCursorMonitorOutputBuffer + chunk.toString()); + const lines = nativeCursorMonitorOutputBuffer.split(/\r?\n/); + setNativeCursorMonitorOutputBuffer(lines.pop() ?? ""); + + for (const line of lines) { + const interactionMatch = line.match(/^INTERACTION:(mousedown|mouseup)(?::([123]))?$/); + if (interactionMatch) { + if (interactionMatch[1] === "mouseup") { + recordCursorMouseUp(); + } else { + const button = Number(interactionMatch[2]); + recordCursorMouseDown(button === 2 || button === 3 ? button : 1); + } + continue; + } + + const match = line.match(/^STATE:(.+)$/); + if (!match) continue; + const next = match[1].trim() as CursorVisualType; + if ( + next === "arrow" || + next === "text" || + next === "pointer" || + next === "crosshair" || + next === "open-hand" || + next === "closed-hand" || + next === "resize-ew" || + next === "resize-ns" || + next === "not-allowed" + ) { + if (currentCursorVisualType !== next) { + setCurrentCursorVisualType(next); + // sampleCursorStateChange is called from cursor/telemetry.ts via the handler + emitCursorStateChanged(next); + } + } + } +} + +export function stopNativeCursorMonitor() { + setCurrentCursorVisualType("arrow"); + + if (!nativeCursorMonitorProcess) { + return; + } + + try { + nativeCursorMonitorProcess.stdin.write("stop\n"); + } catch { + // ignore stop signal issues + } + try { + nativeCursorMonitorProcess.kill(); + } catch { + // ignore kill issues + } + + setNativeCursorMonitorProcess(null); + setNativeCursorMonitorOutputBuffer(""); +} + +export async function startNativeCursorMonitor() { + stopNativeCursorMonitor(); + + if (process.platform !== "darwin" && process.platform !== "win32") { + setCurrentCursorVisualType("arrow"); + return; + } + + try { + let helperPath: string; + if (process.platform === "win32") { + helperPath = getCursorMonitorExePath(); + try { + // Use F_OK on Windows — X_OK is meaningless and can give false positives + await fs.access(helperPath, fsConstants.F_OK); + } catch { + console.warn("Windows cursor monitor helper missing:", helperPath); + setCurrentCursorVisualType("arrow"); + return; + } + } else { + helperPath = await ensureNativeCursorMonitorBinary(); + } + + setNativeCursorMonitorOutputBuffer(""); + setCurrentCursorVisualType("arrow"); + + let proc: ReturnType | null; + try { + proc = spawn(helperPath, [], { + stdio: ["pipe", "pipe", "pipe"], + }); + } catch (spawnError) { + console.warn("Failed to spawn cursor monitor:", spawnError); + setNativeCursorMonitorProcess(null); + setCurrentCursorVisualType("arrow"); + return; + } + + setNativeCursorMonitorProcess(proc as Parameters[0]); + const spawned = proc; + if (!spawned) { + setNativeCursorMonitorProcess(null); + setCurrentCursorVisualType("arrow"); + return; + } + + spawned.once("error", (error) => { + console.warn("Native cursor monitor process error:", error); + if (nativeCursorMonitorProcess === spawned) { + setNativeCursorMonitorProcess(null); + setNativeCursorMonitorOutputBuffer(""); + setCurrentCursorVisualType("arrow"); + } + }); + + if (spawned.stdout) spawned.stdout.on("data", handleCursorMonitorStdout); + if (spawned.stderr) { + spawned.stderr.on("data", () => { + // Drain stderr so helper logging cannot block the process. + }); + } + + spawned.once("close", () => { + if (nativeCursorMonitorProcess === spawned) { + setNativeCursorMonitorProcess(null); + setNativeCursorMonitorOutputBuffer(""); + setCurrentCursorVisualType("arrow"); + } + }); + } catch (error) { + console.warn("Failed to start native cursor monitor:", error); + setNativeCursorMonitorProcess(null); + setNativeCursorMonitorOutputBuffer(""); + setCurrentCursorVisualType("arrow"); + } +} diff --git a/electron/ipc/cursor/telemetry.test.ts b/electron/ipc/cursor/telemetry.test.ts new file mode 100644 index 000000000..03613859b --- /dev/null +++ b/electron/ipc/cursor/telemetry.test.ts @@ -0,0 +1,122 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { CURSOR_TELEMETRY_VERSION } from "../constants"; + +const { writeFile, rm } = vi.hoisted(() => ({ + writeFile: vi.fn(), + rm: vi.fn(), +})); + +vi.mock("node:fs/promises", () => ({ + default: { + writeFile, + rm, + }, +})); + +vi.mock("electron", () => ({ + app: { + getPath: vi.fn(() => "/tmp"), + }, +})); + +vi.mock("../utils", () => ({ + getTelemetryPathForVideo: vi.fn(() => "/tmp/recording.cursor.json"), + getScreen: vi.fn(() => ({ + getCursorScreenPoint: () => ({ x: 0, y: 0 }), + getPrimaryDisplay: () => ({ scaleFactor: 1 }), + getDisplayNearestPoint: () => ({ bounds: { x: 0, y: 0, width: 1, height: 1 } }), + getAllDisplays: () => [], + })), +})); + +import { activeCursorSamples, setActiveCursorSamples, setCursorCaptureStartTimeMs } from "../state"; +import { + getCursorCaptureElapsedMs, + normalizeCursorTelemetrySamples, + pauseCursorCapture, + pauseCursorCaptureAtBoundary, + pushCursorSample, + resetCursorCaptureClock, + resumeCursorCapture, + writeCursorTelemetry, +} from "./telemetry"; + +describe("cursor telemetry pause clock", () => { + beforeEach(() => { + writeFile.mockReset(); + rm.mockReset(); + setCursorCaptureStartTimeMs(1_000); + setActiveCursorSamples([]); + resetCursorCaptureClock(); + }); + + it("subtracts paused time from elapsed cursor timestamps", () => { + expect(getCursorCaptureElapsedMs(1_120)).toBe(120); + + pauseCursorCapture(1_200); + expect(getCursorCaptureElapsedMs(1_450)).toBe(200); + + resumeCursorCapture(1_700); + expect(getCursorCaptureElapsedMs(1_900)).toBe(400); + }); + + it("ignores duplicate pause or resume transitions", () => { + pauseCursorCapture(1_150); + pauseCursorCapture(1_250); + resumeCursorCapture(1_500); + resumeCursorCapture(1_650); + + expect(getCursorCaptureElapsedMs(1_900)).toBe(550); + }); + + it("drops cursor samples captured after the renderer pause boundary", () => { + pushCursorSample(0.1, 0.1, 120, "move"); + pushCursorSample(0.2, 0.2, 205, "move"); + pushCursorSample(0.3, 0.3, 260, "move"); + + pauseCursorCaptureAtBoundary(1_200); + + expect(getCursorCaptureElapsedMs(1_500)).toBe(200); + expect(activeCursorSamples.map((sample) => sample.timeMs)).toEqual([120]); + + resumeCursorCapture(1_700); + expect(getCursorCaptureElapsedMs(1_900)).toBe(400); + }); + + it("normalizes cursor telemetry samples before persisting them", async () => { + const samples = normalizeCursorTelemetrySamples([ + { timeMs: 30, cx: 2, cy: -1, interactionType: "click", cursorType: "pointer" }, + { timeMs: -10, cx: Number.NaN, cy: 0.2, interactionType: "drag", cursorType: "ibeam" }, + { timeMs: 10, cx: 0.25, cy: 0.75, interactionType: "move", cursorType: "text" }, + ]); + + expect(samples).toEqual([ + { timeMs: 0, cx: 0.5, cy: 0.2, interactionType: undefined, cursorType: undefined }, + { timeMs: 10, cx: 0.25, cy: 0.75, interactionType: "move", cursorType: "text" }, + { timeMs: 30, cx: 1, cy: 0, interactionType: "click", cursorType: "pointer" }, + ]); + + await writeCursorTelemetry("/tmp/recording.mp4", samples); + + expect(writeFile).toHaveBeenCalledWith( + "/tmp/recording.cursor.json", + JSON.stringify( + { + version: CURSOR_TELEMETRY_VERSION, + samples, + }, + null, + 2, + ), + "utf-8", + ); + expect(rm).not.toHaveBeenCalled(); + }); + + it("removes the sidecar when saving an empty cursor telemetry payload", async () => { + await writeCursorTelemetry("/tmp/recording.mp4", []); + + expect(rm).toHaveBeenCalledWith("/tmp/recording.cursor.json", { force: true }); + expect(writeFile).not.toHaveBeenCalled(); + }); +}); diff --git a/electron/ipc/cursor/telemetry.ts b/electron/ipc/cursor/telemetry.ts new file mode 100644 index 000000000..8318497bd --- /dev/null +++ b/electron/ipc/cursor/telemetry.ts @@ -0,0 +1,335 @@ +import fs from "node:fs/promises"; +import { + CURSOR_SAMPLE_INTERVAL_MS, + CURSOR_TELEMETRY_VERSION, + MAX_CURSOR_SAMPLES, +} from "../constants"; +import { + activeCursorSamples, + currentCursorVisualType, + cursorCaptureAccumulatedPausedMs, + cursorCaptureInterval, + cursorCapturePauseStartedAtMs, + cursorCaptureStartTimeMs, + isCursorCaptureActive, + linuxCursorScreenPoint, + pendingCursorSamples, + selectedSource, + selectedWindowBounds, + setActiveCursorSamples, + setCursorCaptureAccumulatedPausedMs, + setCursorCaptureInterval, + setCursorCapturePauseStartedAtMs, + setPendingCursorSamples, +} from "../state"; +import type { CursorInteractionType, CursorTelemetryPoint, CursorVisualType } from "../types"; +import { getSourceArea } from "../sourceArea"; +import { getScreen, getTelemetryPathForVideo } from "../utils"; + +export function clamp(value: number, min: number, max: number) { + return Math.min(max, Math.max(min, value)); +} + +export function normalizeCursorTelemetrySamples(rawSamples: unknown): CursorTelemetryPoint[] { + const samples = Array.isArray(rawSamples) + ? rawSamples + : Array.isArray((rawSamples as { samples?: unknown[] } | null | undefined)?.samples) + ? ((rawSamples as { samples: unknown[] }).samples ?? []) + : []; + const boundedSamples = samples.slice(0, MAX_CURSOR_SAMPLES); + + return boundedSamples + .filter((sample: unknown) => Boolean(sample && typeof sample === "object")) + .map((sample: unknown) => { + const point = sample as Partial; + return { + timeMs: + typeof point.timeMs === "number" && Number.isFinite(point.timeMs) + ? Math.max(0, point.timeMs) + : 0, + cx: + typeof point.cx === "number" && Number.isFinite(point.cx) + ? clamp(point.cx, 0, 1) + : 0.5, + cy: + typeof point.cy === "number" && Number.isFinite(point.cy) + ? clamp(point.cy, 0, 1) + : 0.5, + interactionType: + point.interactionType === "click" || + point.interactionType === "double-click" || + point.interactionType === "right-click" || + point.interactionType === "middle-click" || + point.interactionType === "move" || + point.interactionType === "mouseup" + ? point.interactionType + : undefined, + cursorType: + point.cursorType === "arrow" || + point.cursorType === "text" || + point.cursorType === "pointer" || + point.cursorType === "crosshair" || + point.cursorType === "open-hand" || + point.cursorType === "closed-hand" || + point.cursorType === "resize-ew" || + point.cursorType === "resize-ns" || + point.cursorType === "not-allowed" + ? point.cursorType + : undefined, + }; + }) + .sort((a, b) => a.timeMs - b.timeMs); +} + +export async function writeCursorTelemetry(videoPath: string, samples: unknown) { + const telemetryPath = getTelemetryPathForVideo(videoPath); + const normalizedSamples = normalizeCursorTelemetrySamples(samples); + + if (normalizedSamples.length === 0) { + await fs.rm(telemetryPath, { force: true }); + return normalizedSamples; + } + + await fs.writeFile( + telemetryPath, + JSON.stringify({ version: CURSOR_TELEMETRY_VERSION, samples: normalizedSamples }, null, 2), + "utf-8", + ); + + return normalizedSamples; +} + +export function stopCursorCapture() { + if (cursorCaptureInterval) { + clearTimeout(cursorCaptureInterval); + setCursorCaptureInterval(null); + } +} + +export function resetCursorCaptureClock() { + setCursorCaptureAccumulatedPausedMs(0); + setCursorCapturePauseStartedAtMs(null); +} + +export function isCursorCapturePaused() { + return cursorCapturePauseStartedAtMs !== null; +} + +export function pauseCursorCapture(pausedAtMs: number) { + if (cursorCapturePauseStartedAtMs !== null) { + return; + } + + setCursorCapturePauseStartedAtMs(pausedAtMs); +} + +export function pauseCursorCaptureAtBoundary(pausedAtMs: number) { + if (cursorCapturePauseStartedAtMs !== null) { + return; + } + + const pausedElapsedMs = getCursorCaptureElapsedMs(pausedAtMs); + setActiveCursorSamples( + activeCursorSamples.filter((sample) => sample.timeMs <= pausedElapsedMs), + ); + setCursorCapturePauseStartedAtMs(pausedAtMs); +} + +export function resumeCursorCapture(resumedAtMs: number) { + if (cursorCapturePauseStartedAtMs === null) { + return; + } + + const pauseDurationMs = Math.max(0, resumedAtMs - cursorCapturePauseStartedAtMs); + setCursorCaptureAccumulatedPausedMs(cursorCaptureAccumulatedPausedMs + pauseDurationMs); + setCursorCapturePauseStartedAtMs(null); +} + +export function getCursorCaptureElapsedMs(nowMs = Date.now()) { + if (!Number.isFinite(cursorCaptureStartTimeMs) || cursorCaptureStartTimeMs <= 0) { + return 0; + } + + const safeNowMs = Math.max(cursorCaptureStartTimeMs, nowMs); + const activePauseDurationMs = + cursorCapturePauseStartedAtMs === null + ? 0 + : Math.max(0, safeNowMs - cursorCapturePauseStartedAtMs); + + return Math.max( + 0, + safeNowMs - + cursorCaptureStartTimeMs - + Math.max(0, cursorCaptureAccumulatedPausedMs) - + activePauseDurationMs, + ); +} + +export function getNormalizedCursorPoint() { + const fallbackCursor = getScreen().getCursorScreenPoint(); + const linuxCursorCache = process.platform === "linux" ? linuxCursorScreenPoint : null; + const isLinuxCacheFresh = !!linuxCursorCache && Date.now() - linuxCursorCache.updatedAt <= 1000; + + const primarySf = + process.platform !== "darwin" ? getScreen().getPrimaryDisplay().scaleFactor || 1 : 1; + + const cursor = isLinuxCacheFresh + ? { x: linuxCursorCache.x / primarySf, y: linuxCursorCache.y / primarySf } + : fallbackCursor; + + // Areas are fixed rectangles, so they normalize like a window that never moves. + const windowBounds = selectedSource?.id?.startsWith("window:") + ? selectedWindowBounds + : getSourceArea(selectedSource); + if (windowBounds) { + const sf = + process.platform === "win32" || process.platform === "darwin" + ? 1 + : getScreen().getDisplayNearestPoint({ + x: windowBounds.x / primarySf, + y: windowBounds.y / primarySf, + }).scaleFactor || 1; + const width = Math.max(1, windowBounds.width / sf); + const height = Math.max(1, windowBounds.height / sf); + + return { + cx: clamp((cursor.x - windowBounds.x / sf) / width, 0, 1), + cy: clamp((cursor.y - windowBounds.y / sf) / height, 0, 1), + }; + } + + const sourceDisplayId = Number(selectedSource?.display_id); + const sourceDisplay = Number.isFinite(sourceDisplayId) + ? (getScreen() + .getAllDisplays() + .find((display) => display.id === sourceDisplayId) ?? null) + : null; + const display = sourceDisplay ?? getScreen().getDisplayNearestPoint(cursor); + const bounds = display.bounds; + const width = Math.max(1, bounds.width); + const height = Math.max(1, bounds.height); + + const cx = clamp((cursor.x - bounds.x) / width, 0, 1); + const cy = clamp((cursor.y - bounds.y) / height, 0, 1); + return { cx, cy }; +} + +export function getHookCursorScreenPoint( + event: + | { + x?: number; + y?: number; + data?: { x?: number; y?: number; screenX?: number; screenY?: number }; + screenX?: number; + screenY?: number; + } + | null + | undefined, +): { x: number; y: number } | null { + const rawX = event?.x ?? event?.data?.x ?? event?.screenX ?? event?.data?.screenX; + const rawY = event?.y ?? event?.data?.y ?? event?.screenY ?? event?.data?.screenY; + + if ( + typeof rawX !== "number" || + !Number.isFinite(rawX) || + typeof rawY !== "number" || + !Number.isFinite(rawY) + ) { + return null; + } + + return { x: rawX, y: rawY }; +} + +export function pushCursorSample( + cx: number, + cy: number, + timeMs: number, + interactionType: CursorInteractionType = "move", + cursorType?: CursorVisualType, +) { + activeCursorSamples.push({ + timeMs: Math.max(0, timeMs), + cx, + cy, + interactionType, + cursorType: cursorType ?? currentCursorVisualType, + } as CursorTelemetryPoint); + + if (activeCursorSamples.length > MAX_CURSOR_SAMPLES) { + activeCursorSamples.shift(); + } +} + +export function sampleCursorPoint() { + const point = getNormalizedCursorPoint(); + pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "move"); +} + +export async function persistPendingCursorTelemetry(videoPath: string) { + const telemetryPath = getTelemetryPathForVideo(videoPath); + if (pendingCursorSamples.length > 0) { + await fs.writeFile( + telemetryPath, + JSON.stringify( + { version: CURSOR_TELEMETRY_VERSION, samples: pendingCursorSamples }, + null, + 2, + ), + "utf-8", + ); + } + setPendingCursorSamples([]); +} + +export function snapshotCursorTelemetryForPersistence() { + if (activeCursorSamples.length === 0) { + return; + } + + if (pendingCursorSamples.length === 0) { + setPendingCursorSamples([...activeCursorSamples]); + return; + } + + const lastPendingTimeMs = pendingCursorSamples[pendingCursorSamples.length - 1]?.timeMs ?? -1; + setPendingCursorSamples([ + ...pendingCursorSamples, + ...activeCursorSamples.filter((sample) => sample.timeMs > lastPendingTimeMs), + ]); +} + +export function startCursorSampling() { + stopCursorCapture(); + + // Use recursive setTimeout with drift compensation instead of setInterval. + // Under CPU load setInterval bunches or skips callbacks, creating large gaps + // in telemetry data. This approach measures wall-clock drift each tick and + // adjusts the next delay so samples stay close to the target interval. + let nextExpectedMs = Date.now() + CURSOR_SAMPLE_INTERVAL_MS; + + const tick = () => { + if (isCursorCaptureActive && !isCursorCapturePaused()) { + sampleCursorPoint(); + } + + const now = Date.now(); + const drift = now - nextExpectedMs; + nextExpectedMs += CURSOR_SAMPLE_INTERVAL_MS; + + // If we fell behind by more than one full interval, reset the baseline + // so we don't try to "catch up" with a burst of rapid samples. + if (drift > CURSOR_SAMPLE_INTERVAL_MS) { + nextExpectedMs = now + CURSOR_SAMPLE_INTERVAL_MS; + } + + const delay = Math.max(1, nextExpectedMs - now); + setCursorCaptureInterval(setTimeout(tick, delay)); + }; + + setCursorCaptureInterval(setTimeout(tick, CURSOR_SAMPLE_INTERVAL_MS)); +} + +export { CURSOR_SAMPLE_INTERVAL_MS } from "../constants"; +// Re-export for consumers that use it from this module +export { getTelemetryPathForVideo } from "../utils"; diff --git a/electron/ipc/export/exportStream.test.ts b/electron/ipc/export/exportStream.test.ts new file mode 100644 index 000000000..d8889437d --- /dev/null +++ b/electron/ipc/export/exportStream.test.ts @@ -0,0 +1,139 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const TMP_ROOT = path.join(os.tmpdir(), `recordly-export-stream-test-${Date.now()}`); + +vi.mock("electron", () => ({ + app: { + getPath: (key: string) => { + if (key === "temp") { + return TMP_ROOT; + } + throw new Error(`Unexpected app.getPath key: ${key}`); + }, + }, +})); + +import { + cleanupAllExportStreams, + closeExportStream, + hasExportStream, + isOwnedExportPath, + openExportStream, + writeToExportStream, +} from "./exportStream"; + +async function readBytes(filePath: string): Promise { + return new Uint8Array(await fs.readFile(filePath)); +} + +describe("exportStream", () => { + const openedTempPaths: string[] = []; + + beforeAll(async () => { + await fs.mkdir(TMP_ROOT, { recursive: true }); + }); + + afterAll(async () => { + await fs.rm(TMP_ROOT, { recursive: true, force: true }); + }); + + beforeEach(() => { + openedTempPaths.length = 0; + }); + + afterEach(async () => { + await cleanupAllExportStreams(); + await Promise.allSettled( + openedTempPaths.map((tempPath) => fs.rm(tempPath, { force: true })), + ); + }); + + it("persists multiple chunks in order and reports the highest watermark on close", async () => { + const { streamId, tempPath } = await openExportStream(); + openedTempPaths.push(tempPath); + expect(hasExportStream(streamId)).toBe(true); + + await writeToExportStream(streamId, 0, new Uint8Array([1, 2, 3, 4])); + await writeToExportStream(streamId, 4, new Uint8Array([5, 6, 7, 8])); + + const result = await closeExportStream(streamId); + expect(result.tempPath).toBe(tempPath); + expect(result.bytesWritten).toBe(8); + + const bytes = await readBytes(tempPath); + expect(Array.from(bytes)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + expect(hasExportStream(streamId)).toBe(false); + }); + + it("supports out-of-order writes and keeps the watermark at the highest offset", async () => { + const { streamId, tempPath } = await openExportStream(); + openedTempPaths.push(tempPath); + + await writeToExportStream(streamId, 16, new Uint8Array([0xff, 0xee])); + await writeToExportStream(streamId, 0, new Uint8Array([0x01, 0x02])); + + const result = await closeExportStream(streamId); + expect(result.bytesWritten).toBe(18); + + const bytes = await readBytes(tempPath); + expect(bytes.byteLength).toBe(18); + expect(bytes[0]).toBe(0x01); + expect(bytes[1]).toBe(0x02); + expect(bytes[16]).toBe(0xff); + expect(bytes[17]).toBe(0xee); + }); + + it("removes the temp file when closed with abort and returns a null tempPath", async () => { + const { streamId, tempPath } = await openExportStream(); + openedTempPaths.push(tempPath); + await writeToExportStream(streamId, 0, new Uint8Array([9, 9, 9])); + + const result = await closeExportStream(streamId, { abort: true }); + + await expect(fs.access(tempPath)).rejects.toThrow(); + expect(hasExportStream(streamId)).toBe(false); + expect(result.tempPath).toBeNull(); + expect(result.bytesWritten).toBe(0); + expect(isOwnedExportPath(tempPath)).toBe(false); + }); + + it("tracks open temp paths in the owned-path registry until close", async () => { + const { streamId, tempPath } = await openExportStream(); + openedTempPaths.push(tempPath); + + expect(isOwnedExportPath(tempPath)).toBe(true); + // Spoofed paths must not satisfy the registry check. + expect(isOwnedExportPath("/tmp/not-ours.mp4")).toBe(false); + + // A successful close keeps ownership so callers can still move or discard + // the file via the subsequent IPC call. + const result = await closeExportStream(streamId); + expect(result.tempPath).toBe(tempPath); + expect(isOwnedExportPath(tempPath)).toBe(true); + }); + + it("rejects writes after the stream has been aborted", async () => { + const { streamId, tempPath } = await openExportStream(); + openedTempPaths.push(tempPath); + await closeExportStream(streamId, { abort: true }); + + await expect(writeToExportStream(streamId, 0, new Uint8Array([1]))).rejects.toThrow( + /Export stream not found/, + ); + }); + + it("rejects an extension that would escape the temp directory", async () => { + await expect(openExportStream({ extension: "mp4/../etc/passwd" })).rejects.toThrow( + /Invalid export stream extension/, + ); + await expect(openExportStream({ extension: ".." })).rejects.toThrow( + /Invalid export stream extension/, + ); + await expect(openExportStream({ extension: "MP4" })).rejects.toThrow( + /Invalid export stream extension/, + ); + }); +}); diff --git a/electron/ipc/export/exportStream.ts b/electron/ipc/export/exportStream.ts new file mode 100644 index 000000000..d0aac4067 --- /dev/null +++ b/electron/ipc/export/exportStream.ts @@ -0,0 +1,204 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import path from "node:path"; +import { app } from "electron"; + +type ExportStreamSession = { + streamId: string; + sessionDir: string; + tempPath: string; + fileHandle: fs.promises.FileHandle; + bytesWritten: number; + highestWatermark: number; + writeQueue: Promise; + aborted: boolean; +}; + +const exportStreamSessions = new Map(); + +const EXTENSION_ALLOWLIST = /^[a-z0-9]{1,8}$/; +const SESSION_DIR_PREFIX = "recordly-export-"; + +// Paths that the export pipeline itself produced (stream temp files plus any +// successor temp files returned by main-process helpers such as +// muxNativeVideoExportAudio). Every renderer-facing handler that moves or +// deletes a path must assert membership here before touching disk, so a +// compromised renderer can never route arbitrary file paths into the IPC. +const ownedExportPaths = new Set(); + +function normalizeOwnedPath(candidate: string): string { + return path.resolve(candidate); +} + +export function registerOwnedExportPath(candidate: string): void { + ownedExportPaths.add(normalizeOwnedPath(candidate)); +} + +export function releaseOwnedExportPath(candidate: string): void { + ownedExportPaths.delete(normalizeOwnedPath(candidate)); +} + +export function isOwnedExportPath(candidate: string): boolean { + return ownedExportPaths.has(normalizeOwnedPath(candidate)); +} + +function generateStreamId() { + return `recordly-export-stream-${randomUUID()}`; +} + +export async function openExportStream(options?: { extension?: string }): Promise<{ + streamId: string; + tempPath: string; +}> { + const extension = options?.extension ?? "mp4"; + if (!EXTENSION_ALLOWLIST.test(extension)) { + throw new Error(`Invalid export stream extension: ${extension}`); + } + const streamId = generateStreamId(); + + // Per-session 0700 directory defeats TOCTOU/symlink races on shared + // tempdirs (e.g. /tmp on Linux): only the current user can enter the dir, + // so an adversary cannot pre-plant a symlink at the file path. + const sessionDir = await fsp.mkdtemp(path.join(app.getPath("temp"), SESSION_DIR_PREFIX)); + try { + await fsp.chmod(sessionDir, 0o700); + } catch { + // chmod is a defense-in-depth on platforms where mkdtemp already sets + // a safe mode. Non-Linux filesystems may ignore mode bits entirely. + } + const tempPath = path.join(sessionDir, `${streamId}.${extension}`); + const fileHandle = await fsp.open( + tempPath, + fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL, + 0o600, + ); + + exportStreamSessions.set(streamId, { + streamId, + sessionDir, + tempPath, + fileHandle, + bytesWritten: 0, + highestWatermark: 0, + writeQueue: Promise.resolve(), + aborted: false, + }); + registerOwnedExportPath(tempPath); + + return { streamId, tempPath }; +} + +export async function writeToExportStream( + streamId: string, + position: number, + chunk: Uint8Array, +): Promise { + const session = exportStreamSessions.get(streamId); + if (!session) { + throw new Error(`Export stream not found: ${streamId}`); + } + + if (session.aborted) { + throw new Error("Export stream was aborted"); + } + + // Serialize writes against the session to keep byte counters consistent when + // the renderer issues concurrent chunks. + const previous = session.writeQueue; + const next = previous.then(async () => { + if (session.aborted) { + return; + } + const buffer = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength); + await session.fileHandle.write(buffer, 0, buffer.byteLength, position); + session.bytesWritten += buffer.byteLength; + const end = position + buffer.byteLength; + if (end > session.highestWatermark) { + session.highestWatermark = end; + } + }); + session.writeQueue = next.catch(() => undefined); + await next; +} + +export async function closeExportStream( + streamId: string, + options?: { abort?: boolean }, +): Promise<{ tempPath: string | null; bytesWritten: number }> { + const session = exportStreamSessions.get(streamId); + if (!session) { + throw new Error(`Export stream not found: ${streamId}`); + } + + const abort = options?.abort === true; + if (abort) { + session.aborted = true; + } + + try { + await session.writeQueue; + } catch { + // Propagated to the in-flight write promise; closure proceeds regardless. + } + + try { + await session.fileHandle.close(); + } catch { + // File handle may already be closed; ignore so abort paths stay best-effort. + } + + exportStreamSessions.delete(streamId); + + if (abort) { + releaseOwnedExportPath(session.tempPath); + try { + await fsp.rm(session.tempPath, { force: true }); + } catch { + // Temp file may be gone already. + } + try { + await fsp.rm(session.sessionDir, { recursive: true, force: true }); + } catch { + // ignore + } + // Aborted streams return `tempPath: null` so callers cannot accidentally + // reuse a path that no longer references a file on disk (or, worse, a + // path a later session may recycle). + return { tempPath: null, bytesWritten: 0 }; + } + + return { + tempPath: session.tempPath, + bytesWritten: session.highestWatermark, + }; +} + +export function hasExportStream(streamId: string): boolean { + return exportStreamSessions.has(streamId); +} + +export async function cleanupAllExportStreams(): Promise { + const sessions = Array.from(exportStreamSessions.values()); + exportStreamSessions.clear(); + ownedExportPaths.clear(); + await Promise.allSettled( + sessions.map(async (session) => { + try { + await session.fileHandle.close(); + } catch { + // ignore + } + try { + await fsp.rm(session.tempPath, { force: true }); + } catch { + // ignore + } + try { + await fsp.rm(session.sessionDir, { recursive: true, force: true }); + } catch { + // ignore + } + }), + ); +} diff --git a/electron/ipc/export/native-video.test.ts b/electron/ipc/export/native-video.test.ts new file mode 100644 index 000000000..894485395 --- /dev/null +++ b/electron/ipc/export/native-video.test.ts @@ -0,0 +1,1515 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("electron", () => ({ + app: { + getAppPath: vi.fn(() => process.cwd()), + getGPUFeatureStatus: vi.fn(() => ({ + video_decode: "enabled", + video_encode: "enabled", + webgl: "enabled", + webgpu: "enabled", + })), + getGPUInfo: vi.fn(async () => ({ gpuDevice: [] })), + getPath: vi.fn(() => process.env.TEMP ?? process.cwd()), + isPackaged: false, + }, +})); + +vi.mock("../ffmpeg/binary", () => ({ + getFfmpegBinaryPath: vi.fn(() => "ffmpeg"), + getFfprobeBinaryPath: vi.fn(() => "ffprobe"), +})); + +vi.mock("../state", () => ({ + cachedNativeVideoEncoder: null, + setCachedNativeVideoEncoder: vi.fn(), +})); + +const fsMocks = vi.hoisted(() => ({ + access: vi.fn(async () => { + throw new Error("missing"); + }), + writeFile: vi.fn(async () => undefined), + readFile: vi.fn(), + stat: vi.fn(async () => ({ size: 5_000_000_000 })), + unlink: vi.fn(async () => undefined), +})); + +vi.mock("node:fs/promises", () => ({ + default: fsMocks, + ...fsMocks, +})); + +const execFileMock = vi.hoisted(() => + vi.fn((_cmd: string, _args: string[], _opts: unknown, cb: (err: Error | null) => void) => { + cb(null); + return { stdout: "", stderr: "" } as unknown; + }), +); + +vi.mock("node:child_process", () => ({ + execFile: execFileMock, + spawn: vi.fn(), +})); + +import { app } from "electron"; +import { + buildExperimentalNvidiaCudaStaticLayoutArgs, + buildExperimentalWindowsGpuStaticLayoutArgs, + buildNativeStaticLayoutSourceProxyArgs, + buildNativeStaticLayoutTimelineSegments, + buildNativeVideoAudioMuxArgs, + canCopyAudioCodecIntoMp4, + getExperimentalNvidiaCudaExportSkipReason, + getExportHardwareInfo, + getNativeExportCapabilities, + getNativeGpuCompositorStallTimeoutMs, + getNativeStaticLayoutSourceProxyBitrate, + getNvidiaCudaAudioExportSkipReason, + getNvidiaCudaAutoStallTimeoutMs, + hasNativeStaticLayoutProgressAdvanced, + hasNvidiaGpuDeviceInGpuInfo, + mapNvidiaCudaWrapperProgressPercentage, + muxExportedVideoAudioBuffer, + type NativeStaticLayoutExportOptions, + normalizeNativeStaticLayoutBackground, + parseFfmpegDurationSeconds, + parseFfmpegFrameRate, + parseFfmpegProgressLineSeconds, + parseNativeVideoMetadataProbeOutput, + parseNativeVideoStreamStatsProbeOutput, + parseNvidiaCudaExportSummary, + parseWindowsGpuExportProgressLine, + parseWindowsGpuExportSummary, + resolveExperimentalNvidiaCudaExportScriptPath, + sanitizeExportGpuInfo, + shouldCreateNativeStaticLayoutSourceProxy, + validateNativeStaticLayoutSourceProxyMetadata, + validateNativeVideoStreamStats, + validateNvidiaCudaExportSummary, + validateWindowsGpuExportSummary, +} from "./native-video"; + +const electronAppMock = app as unknown as { + getAppPath: ReturnType; + getGPUFeatureStatus: ReturnType; + getGPUInfo: ReturnType; + isPackaged: boolean; +}; + +function withNvidiaCudaAudioOverride(value: string | undefined, callback: () => T) { + const envName = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT"; + const originalValue = process.env[envName]; + if (value === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = value; + } + + try { + return callback(); + } finally { + if (originalValue === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = originalValue; + } + } +} + +function resetFsAccessMock() { + fsMocks.access.mockImplementation(async () => { + throw new Error("missing"); + }); +} + +function createNvidiaCudaSkipOptions( + overrides: Partial = {}, +): NativeStaticLayoutExportOptions { + return { + inputPath: "input.mp4", + width: 1920, + height: 1080, + frameRate: 30, + bitrate: 8_000_000, + encodingMode: "quality", + durationSec: 10, + contentWidth: 1600, + contentHeight: 900, + offsetX: 160, + offsetY: 90, + backgroundColor: "#101010", + experimentalWindowsGpuCompositor: true, + ...overrides, + }; +} + +async function withPackagedCudaCandidate(gpuInfo: unknown, callback: () => Promise) { + const envName = "RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT"; + const originalEnv = process.env[envName]; + const originalIsPackaged = electronAppMock.isPackaged; + electronAppMock.isPackaged = true; + electronAppMock.getGPUInfo.mockResolvedValue(gpuInfo); + fsMocks.access.mockResolvedValue(undefined); + delete process.env[envName]; + + try { + return await callback(); + } finally { + if (originalEnv === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = originalEnv; + } + electronAppMock.isPackaged = originalIsPackaged; + electronAppMock.getGPUInfo.mockReset(); + electronAppMock.getGPUInfo.mockResolvedValue({ gpuDevice: [] }); + resetFsAccessMock(); + } +} + +describe("normalizeNativeStaticLayoutBackground", () => { + it("falls back to a solid background when the configured image file is missing", async () => { + const normalized = await normalizeNativeStaticLayoutBackground({ + inputPath: "input.mp4", + width: 1920, + height: 1080, + frameRate: 30, + bitrate: 8_000_000, + encodingMode: "quality", + durationSec: 10, + contentWidth: 1600, + contentHeight: 900, + offsetX: 160, + offsetY: 90, + backgroundColor: "#101010", + backgroundImagePath: "Z:\\recordly-missing-wallpaper\\midnight-8.jpg", + }); + + expect(normalized.backgroundImagePath).toBeNull(); + expect(normalized.backgroundColor).toBe("#ffffff"); + }); +}); + +describe("native static-layout source proxy", () => { + const h264Metadata = { + width: 1920, + height: 1080, + duration: 45, + frameRate: 30, + codec: "h264 (High)", + hasAudio: true, + audioCodec: "aac", + }; + + it("proxies non-H.264 codecs and non-MP4-like containers", () => { + expect(shouldCreateNativeStaticLayoutSourceProxy(h264Metadata, "recording.mp4")).toBe( + false, + ); + expect(shouldCreateNativeStaticLayoutSourceProxy(h264Metadata, "recording.webm")).toBe( + true, + ); + expect( + shouldCreateNativeStaticLayoutSourceProxy( + { ...h264Metadata, codec: "vp9 (Profile 0)" }, + "recording.webm", + ), + ).toBe(true); + }); + + it("builds a CFR H.264 MP4 proxy at visually safe bitrate", () => { + const options = createNvidiaCudaSkipOptions({ + inputPath: "recording.webm", + bitrate: 22_000_000, + durationSec: 30, + }); + const metadata = { + ...h264Metadata, + codec: "vp9 (Profile 0)", + audioCodec: "opus", + duration: 45.036, + streamDuration: 45.036, + }; + const args = buildNativeStaticLayoutSourceProxyArgs(options, "source-proxy.mp4", metadata); + + expect(args).toEqual( + expect.arrayContaining([ + "-i", + "recording.webm", + "-an", + "-t", + "45.036", + "-c:v", + "libx264", + "-preset", + "veryfast", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + "source-proxy.mp4", + ]), + ); + expect(args).toEqual( + expect.arrayContaining([ + "-vf", + "fps=fps=30:start_time=0,scale=trunc(iw/2)*2:trunc(ih/2)*2,setsar=1,format=yuv420p,setpts=PTS-STARTPTS", + ]), + ); + expect(getNativeStaticLayoutSourceProxyBitrate(options, metadata)).toBeGreaterThan( + options.bitrate, + ); + }); + + it("validates proxy codec and duration before native composition", () => { + expect( + validateNativeStaticLayoutSourceProxyMetadata( + { ...h264Metadata, codec: "vp9", duration: 45 }, + { sourceDurationSec: 45 }, + ), + ).toEqual(["proxy codec is not H.264/AVC: vp9"]); + expect( + validateNativeStaticLayoutSourceProxyMetadata( + { ...h264Metadata, duration: 10 }, + { sourceDurationSec: 45 }, + ), + ).toEqual(["proxy duration 10.000s shorter than expected 45.000s"]); + }); +}); + +describe("getNvidiaCudaAudioExportSkipReason", () => { + it("allows video-only CUDA exports by default", () => { + withNvidiaCudaAudioOverride(undefined, () => { + expect(getNvidiaCudaAudioExportSkipReason(undefined)).toBeNull(); + expect(getNvidiaCudaAudioExportSkipReason("none")).toBeNull(); + }); + }); + + it("guards CUDA for audio exports unless explicitly overridden", () => { + withNvidiaCudaAudioOverride(undefined, () => { + expect(getNvidiaCudaAudioExportSkipReason("copy-source")).toBe( + "audio-mode:copy-source", + ); + expect(getNvidiaCudaAudioExportSkipReason("trim-source")).toBe( + "audio-mode:trim-source", + ); + expect(getNvidiaCudaAudioExportSkipReason("edited-track")).toBe( + "audio-mode:edited-track", + ); + }); + }); + + it("allows CUDA audio exports only for explicit lab overrides", () => { + withNvidiaCudaAudioOverride("1", () => { + expect(getNvidiaCudaAudioExportSkipReason("copy-source")).toBeNull(); + }); + }); + + it("allows validated fallback candidates to try CUDA audio exports", () => { + withNvidiaCudaAudioOverride(undefined, () => { + expect( + getNvidiaCudaAudioExportSkipReason("copy-source", { + allowValidatedFallbackCandidate: true, + }), + ).toBeNull(); + }); + }); +}); + +describe("getNvidiaCudaAutoStallTimeoutMs", () => { + it("only applies the stall guard to active validated CUDA candidates by default", () => { + expect(getNvidiaCudaAutoStallTimeoutMs(false)).toBeNull(); + expect(getNvidiaCudaAutoStallTimeoutMs(true)).toBe(120_000); + }); + + it("allows the CUDA auto stall guard to be disabled or tuned", () => { + const envName = "RECORDLY_NVIDIA_CUDA_AUTO_STALL_TIMEOUT_MS"; + const originalValue = process.env[envName]; + + try { + process.env[envName] = "0"; + expect(getNvidiaCudaAutoStallTimeoutMs(true)).toBeNull(); + + process.env[envName] = "5000"; + expect(getNvidiaCudaAutoStallTimeoutMs(true)).toBe(10_000); + + process.env[envName] = "45000"; + expect(getNvidiaCudaAutoStallTimeoutMs(true)).toBe(45_000); + } finally { + if (originalValue === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = originalValue; + } + } + }); +}); + +describe("getNativeGpuCompositorStallTimeoutMs", () => { + it("guards Windows GPU compositor stalls by default", () => { + expect(getNativeGpuCompositorStallTimeoutMs()).toBe(120_000); + }); + + it("allows the Windows GPU stall guard to be disabled or tuned", () => { + const envName = "RECORDLY_NATIVE_GPU_STALL_TIMEOUT_MS"; + const originalValue = process.env[envName]; + + try { + process.env[envName] = "0"; + expect(getNativeGpuCompositorStallTimeoutMs()).toBeNull(); + + process.env[envName] = "5000"; + expect(getNativeGpuCompositorStallTimeoutMs()).toBe(10_000); + + process.env[envName] = "45000"; + expect(getNativeGpuCompositorStallTimeoutMs()).toBe(45_000); + } finally { + if (originalValue === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = originalValue; + } + } + }); +}); + +describe("hasNvidiaGpuDeviceInGpuInfo", () => { + it("detects NVIDIA GPUs by vendor id or device strings", () => { + expect( + hasNvidiaGpuDeviceInGpuInfo({ + gpuDevice: [{ active: false, vendorId: 0x10de, deviceId: 0x1f91 }], + }), + ).toBe(true); + expect( + hasNvidiaGpuDeviceInGpuInfo({ + gpuDevice: [{ vendorString: "NVIDIA Corporation" }], + }), + ).toBe(true); + expect( + hasNvidiaGpuDeviceInGpuInfo({ + gpuDevice: [{ deviceString: "NVIDIA GeForce GTX 1650" }], + }), + ).toBe(true); + }); + + it("rejects non-NVIDIA and malformed GPU info", () => { + expect( + hasNvidiaGpuDeviceInGpuInfo({ + gpuDevice: [ + { vendorId: 0x1002, deviceString: "AMD Radeon" }, + { vendorId: 0x8086, deviceString: "Intel UHD Graphics" }, + ], + }), + ).toBe(false); + expect(hasNvidiaGpuDeviceInGpuInfo({})).toBe(false); + expect(hasNvidiaGpuDeviceInGpuInfo(null)).toBe(false); + }); +}); + +describe("getNativeExportCapabilities", () => { + it("reports NVIDIA CUDA availability when the wrapper and an NVIDIA GPU are present", async () => { + const capabilities = await withPackagedCudaCandidate( + { gpuDevice: [{ vendorId: 0x10de, deviceString: "NVIDIA GeForce GTX 1650" }] }, + () => getNativeExportCapabilities(), + ); + + expect(capabilities.nvidiaCuda.available).toBe(process.platform === "win32"); + expect(capabilities.nvidiaCuda.hasWrapper).toBe(process.platform === "win32"); + expect(capabilities.nvidiaCuda.hasNvidiaGpu).toBe( + process.platform === "win32" ? true : null, + ); + }); +}); + +describe("export hardware diagnostics", () => { + it("sanitizes machine and GPU details without exposing raw device identifiers", () => { + const hardware = sanitizeExportGpuInfo({ + machineModelName: "MacBookPro", + machineModelVersion: "18,2", + gpuDevice: [ + { + active: true, + vendorId: "0x10de", + deviceId: 9999, + deviceString: "NVIDIA GeForce RTX 4070", + }, + ], + }); + + expect(hardware).toEqual({ + machineModel: "MacBookPro 18,2", + gpus: [ + { + name: "NVIDIA GeForce RTX 4070", + vendor: "NVIDIA", + active: true, + }, + ], + }); + expect(JSON.stringify(hardware)).not.toContain("deviceId"); + expect(JSON.stringify(hardware)).not.toContain("vendorId"); + }); + + it("returns system capacity and GPU acceleration status", async () => { + electronAppMock.getGPUInfo.mockResolvedValueOnce({ + gpuDevice: [{ active: true, vendorId: 0x8086 }], + }); + + const hardware = await getExportHardwareInfo(); + + expect(electronAppMock.getGPUInfo).toHaveBeenCalledWith("complete"); + expect(hardware).toMatchObject({ + platform: process.platform, + arch: process.arch, + gpus: [{ name: "Intel", vendor: "Intel", active: true }], + gpuFeatures: { + videoDecode: "enabled", + videoEncode: "enabled", + webgl: "enabled", + webgpu: "enabled", + }, + }); + expect(hardware.logicalProcessors).toBeGreaterThan(0); + expect(hardware.totalMemoryGb).toBeGreaterThan(0); + }); +}); + +describe("getExperimentalNvidiaCudaExportSkipReason", () => { + it("requires user opt-in before packaged CUDA candidates run", async () => { + const reason = await withPackagedCudaCandidate( + { gpuDevice: [{ vendorId: 0x10de, deviceString: "NVIDIA GeForce GTX 1650" }] }, + () => + getExperimentalNvidiaCudaExportSkipReason( + createNvidiaCudaSkipOptions({ + audioOptions: { audioMode: "copy-source", audioSourcePath: "input.mp4" }, + }), + ), + ); + + expect(reason).toBe(process.platform === "win32" ? "env-disabled" : "not-windows"); + }); + + it("allows user opt-in CUDA candidates when the helper and an NVIDIA GPU are present", async () => { + const reason = await withPackagedCudaCandidate( + { gpuDevice: [{ vendorId: 0x10de, deviceString: "NVIDIA GeForce GTX 1650" }] }, + () => + getExperimentalNvidiaCudaExportSkipReason( + createNvidiaCudaSkipOptions({ + experimentalNvidiaCudaExport: true, + audioOptions: { audioMode: "copy-source", audioSourcePath: "input.mp4" }, + }), + ), + ); + + expect(reason).toBe(process.platform === "win32" ? null : "not-windows"); + }); + + it("allows explicit lab CUDA audio exports when forced onto the shared mux path", async () => { + const exportEnvName = "RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT"; + const forceEnvName = "RECORDLY_NVIDIA_CUDA_FORCE_VIDEO_ONLY"; + const allowAudioEnvName = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT"; + const originalExportEnv = process.env[exportEnvName]; + const originalForceEnv = process.env[forceEnvName]; + const originalAllowAudioEnv = process.env[allowAudioEnvName]; + process.env[exportEnvName] = "1"; + process.env[forceEnvName] = "1"; + delete process.env[allowAudioEnvName]; + + try { + const reason = await getExperimentalNvidiaCudaExportSkipReason( + createNvidiaCudaSkipOptions({ + audioOptions: { audioMode: "copy-source", audioSourcePath: "input.mp4" }, + }), + ); + + expect(reason).toBe(process.platform === "win32" ? null : "not-windows"); + } finally { + if (originalExportEnv === undefined) { + delete process.env[exportEnvName]; + } else { + process.env[exportEnvName] = originalExportEnv; + } + if (originalForceEnv === undefined) { + delete process.env[forceEnvName]; + } else { + process.env[forceEnvName] = originalForceEnv; + } + if (originalAllowAudioEnv === undefined) { + delete process.env[allowAudioEnvName]; + } else { + process.env[allowAudioEnvName] = originalAllowAudioEnv; + } + } + }); + + it("does not use packaged auto-candidate audio bypass when CUDA is explicitly enabled", async () => { + const exportEnvName = "RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT"; + const forceEnvName = "RECORDLY_NVIDIA_CUDA_FORCE_VIDEO_ONLY"; + const allowAudioEnvName = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT"; + const originalExportEnv = process.env[exportEnvName]; + const originalForceEnv = process.env[forceEnvName]; + const originalAllowAudioEnv = process.env[allowAudioEnvName]; + const originalIsPackaged = electronAppMock.isPackaged; + electronAppMock.isPackaged = true; + process.env[exportEnvName] = "1"; + delete process.env[forceEnvName]; + delete process.env[allowAudioEnvName]; + + try { + const reason = await getExperimentalNvidiaCudaExportSkipReason( + createNvidiaCudaSkipOptions({ + audioOptions: { audioMode: "copy-source", audioSourcePath: "input.mp4" }, + }), + ); + + expect(reason).toBe( + process.platform === "win32" ? "audio-mode:copy-source" : "not-windows", + ); + } finally { + if (originalExportEnv === undefined) { + delete process.env[exportEnvName]; + } else { + process.env[exportEnvName] = originalExportEnv; + } + if (originalForceEnv === undefined) { + delete process.env[forceEnvName]; + } else { + process.env[forceEnvName] = originalForceEnv; + } + if (originalAllowAudioEnv === undefined) { + delete process.env[allowAudioEnvName]; + } else { + process.env[allowAudioEnvName] = originalAllowAudioEnv; + } + electronAppMock.isPackaged = originalIsPackaged; + } + }); + + it("skips user opt-in CUDA candidates when Electron reports no NVIDIA GPU", async () => { + const reason = await withPackagedCudaCandidate( + { gpuDevice: [{ vendorId: 0x8086, deviceString: "Intel UHD Graphics" }] }, + () => + getExperimentalNvidiaCudaExportSkipReason( + createNvidiaCudaSkipOptions({ experimentalNvidiaCudaExport: true }), + ), + ); + + expect(reason).toBe( + process.platform === "win32" ? "nvidia-gpu-unavailable" : "not-windows", + ); + }); + + it("lets the user opt-in candidate be explicitly disabled", async () => { + const reason = await withPackagedCudaCandidate( + { gpuDevice: [{ vendorId: 0x10de, deviceString: "NVIDIA GeForce GTX 1650" }] }, + async () => { + process.env.RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT = "0"; + return getExperimentalNvidiaCudaExportSkipReason( + createNvidiaCudaSkipOptions({ experimentalNvidiaCudaExport: true }), + ); + }, + ); + + expect(reason).toBe(process.platform === "win32" ? "env-disabled" : "not-windows"); + }); +}); + +describe("resolveExperimentalNvidiaCudaExportScriptPath", () => { + it("prefers the packaged app.asar.unpacked CUDA wrapper over the virtual app.asar copy", async () => { + const envName = "RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT"; + const originalEnv = process.env[envName]; + const originalResourcesPath = Object.getOwnPropertyDescriptor(process, "resourcesPath"); + delete process.env[envName]; + + try { + if (process.platform !== "win32") { + expect(await resolveExperimentalNvidiaCudaExportScriptPath()).toBeNull(); + return; + } + + const resourcesPath = "C:\\Recordly\\resources"; + const unpackedScriptPath = + "C:\\Recordly\\resources\\app.asar.unpacked\\electron\\native\\nvidia-cuda-compositor\\run-mp4-pipeline.mjs"; + const asarScriptPath = + "C:\\Recordly\\resources\\app.asar\\electron\\native\\nvidia-cuda-compositor\\run-mp4-pipeline.mjs"; + Object.defineProperty(process, "resourcesPath", { + configurable: true, + value: resourcesPath, + }); + electronAppMock.getAppPath.mockReturnValue("C:\\Recordly\\resources\\app.asar"); + fsMocks.access.mockImplementation(async (candidate: string) => { + if (candidate === unpackedScriptPath || candidate === asarScriptPath) { + return; + } + throw new Error(`missing ${candidate}`); + }); + + expect(await resolveExperimentalNvidiaCudaExportScriptPath()).toBe(unpackedScriptPath); + } finally { + if (originalEnv === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = originalEnv; + } + if (originalResourcesPath) { + Object.defineProperty(process, "resourcesPath", originalResourcesPath); + } else { + delete (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath; + } + electronAppMock.getAppPath.mockReset(); + electronAppMock.getAppPath.mockReturnValue(process.cwd()); + resetFsAccessMock(); + } + }); +}); + +describe("buildExperimentalNvidiaCudaStaticLayoutArgs", () => { + it("passes output canvas dimensions to the CUDA wrapper", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ width: 1020, height: 572 }), + "output.mp4", + "work", + ); + + expect(args).toEqual(expect.arrayContaining(["--width", "1020", "--height", "572"])); + }); + + it("keeps explicit copy-source CUDA audio inline by default", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + audioOptions: { audioMode: "copy-source", audioSourcePath: "input.mp4" }, + }), + "output.mp4", + "work", + ); + + expect(args).not.toContain("--video-only"); + }); + + it("forces packaged auto CUDA candidates onto the shared audio mux path", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + audioOptions: { audioMode: "copy-source", audioSourcePath: "input.mp4" }, + nvidiaCudaForceVideoOnly: true, + }), + "output.mp4", + "work", + ); + + expect(args).toContain("--video-only"); + }); + + it("passes native timeline maps to the CUDA wrapper", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + timelineMapPath: "timeline-map.csv", + timelineSegments: [ + { + sourceStartMs: 0, + sourceEndMs: 6_000, + outputStartMs: 0, + outputEndMs: 4_000, + speed: 1.5, + }, + ], + }), + "output.mp4", + "work", + ); + + expect(args).toEqual(expect.arrayContaining(["--timeline-map", "timeline-map.csv"])); + }); + + it("passes background blur to the CUDA wrapper", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + backgroundImagePath: "wallpaper.jpg", + backgroundBlurPx: 36, + }), + "output.mp4", + "work", + ); + + expect(args).toEqual(expect.arrayContaining(["--background-blur", "36"])); + }); + + it("passes the export encoding mode to the CUDA wrapper", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + encodingMode: "quality", + }), + "output.mp4", + "work", + ); + + expect(args).toEqual(expect.arrayContaining(["--encoding-mode", "quality"])); + }); + + it("passes webcam source-time controls to the CUDA wrapper", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + webcamInputPath: "webcam.mp4", + webcamLeft: 32, + webcamTop: 48, + webcamSize: 240, + webcamRadius: 18, + webcamTimeOffsetMs: -125.5, + }), + "output.mp4", + "work", + ); + + expect(args).toEqual( + expect.arrayContaining([ + "--webcam-input", + "webcam.mp4", + "--webcam-time-offset-ms", + "-125.5", + "--webcam-stream", + ]), + ); + }); + + it("passes cursor telemetry and atlas assets to the CUDA wrapper", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + cursorTelemetryPath: "cursor-telemetry.json", + cursorSize: 96, + cursorAtlasPath: "cursor-atlas.png", + cursorAtlasMetadataPath: "cursor-atlas.tsv", + }), + "output.mp4", + "work", + ); + + expect(args).toEqual( + expect.arrayContaining([ + "--cursor-json", + "cursor-telemetry.json", + "--cursor-height", + "96", + "--cursor-style", + "external", + "--cursor-atlas-png", + "cursor-atlas.png", + "--cursor-atlas-metadata", + "cursor-atlas.tsv", + ]), + ); + }); + + it("passes source crop coordinates to the CUDA wrapper", () => { + const args = buildExperimentalNvidiaCudaStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + sourceCropX: 192, + sourceCropY: 108, + sourceCropWidth: 1536, + sourceCropHeight: 864, + }), + "output.mp4", + "work", + ); + + expect(args).toEqual( + expect.arrayContaining([ + "--source-crop-x", + "192", + "--source-crop-y", + "108", + "--source-crop-width", + "1536", + "--source-crop-height", + "864", + ]), + ); + }); +}); + +describe("buildExperimentalWindowsGpuStaticLayoutArgs", () => { + it("passes background blur to the D3D11 compositor", () => { + const args = buildExperimentalWindowsGpuStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + backgroundImagePath: "wallpaper.jpg", + backgroundBlurPx: 36, + }), + "output.mp4", + ); + + expect(args).toEqual(expect.arrayContaining(["--background-blur", "36"])); + }); + + it("passes source crop coordinates to the D3D11 compositor", () => { + const args = buildExperimentalWindowsGpuStaticLayoutArgs( + createNvidiaCudaSkipOptions({ + sourceCropX: 192, + sourceCropY: 108, + sourceCropWidth: 1536, + sourceCropHeight: 864, + }), + "output.mp4", + ); + + expect(args).toEqual( + expect.arrayContaining([ + "--source-crop-x", + "192", + "--source-crop-y", + "108", + "--source-crop-width", + "1536", + "--source-crop-height", + "864", + ]), + ); + }); +}); + +describe("buildNativeStaticLayoutTimelineSegments", () => { + it("derives contiguous output timeline ranges from edited-track source segments", () => { + expect( + buildNativeStaticLayoutTimelineSegments([ + { startMs: 0, endMs: 2_000, speed: 1 }, + { startMs: 2_000, endMs: 8_000, speed: 1.5 }, + { startMs: 8_000, endMs: 10_000, speed: 0.5 }, + ]), + ).toEqual([ + { + sourceStartMs: 0, + sourceEndMs: 2_000, + outputStartMs: 0, + outputEndMs: 2_000, + speed: 1, + }, + { + sourceStartMs: 2_000, + sourceEndMs: 8_000, + outputStartMs: 2_000, + outputEndMs: 6_000, + speed: 1.5, + }, + { + sourceStartMs: 8_000, + sourceEndMs: 10_000, + outputStartMs: 6_000, + outputEndMs: 10_000, + speed: 0.5, + }, + ]); + }); +}); + +describe("muxExportedVideoAudioBuffer", () => { + it("returns the muxed output path without reading the muxed file into memory", async () => { + const videoData = new ArrayBuffer(64); + const result = await muxExportedVideoAudioBuffer(videoData, { audioMode: "none" }); + + expect(typeof result.outputPath).toBe("string"); + expect(result.outputPath.length).toBeGreaterThan(0); + // The >2 GiB fix relies on stat-only metric collection; readFile must stay unused. + expect(fsMocks.readFile).not.toHaveBeenCalled(); + expect(result.metrics.muxedVideoBytes).toBe(5_000_000_000); + }); + + it("preserves the input temp path when audioMode='none' (no re-mux)", async () => { + const videoData = new ArrayBuffer(32); + const result = await muxExportedVideoAudioBuffer(videoData, { audioMode: "none" }); + + expect(result.outputPath).toMatch(/recordly-export-video-/); + }); +}); + +describe("buildNativeVideoAudioMuxArgs", () => { + it("stream-copies source audio and preserves the requested video duration", () => { + const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.mp4", "out.mp4", { + audioMode: "copy-source", + audioSourceCodec: "aac (LC) (mp4a / 0x6134706D)", + outputDurationSec: 60, + }); + + expect(args).toEqual( + expect.arrayContaining([ + "-map", + "0:v:0", + "-map", + "1:a:0", + "-c:v", + "copy", + "-c:a", + "copy", + "-t", + "60.000", + ]), + ); + expect(args).not.toContain("-shortest"); + }); + + it("does not shorten copy-source muxes when no explicit duration is available", () => { + const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.mp4", "out.mp4", { + audioMode: "copy-source", + audioSourceCodec: "aac", + }); + + expect(args).toEqual(expect.arrayContaining(["-c:a", "copy"])); + expect(args).not.toContain("-shortest"); + }); + + it("transcodes WebM/Opus source audio when muxing into MP4", () => { + const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.webm", "out.mp4", { + audioMode: "copy-source", + audioSourceCodec: "opus", + outputDurationSec: 60, + }); + + expect(args).toEqual(expect.arrayContaining(["-c:a", "aac", "-b:a", "192k"])); + expect(args.join(";")).not.toContain("-c:a;copy"); + }); + + it("transcodes unknown source audio instead of copying unsafe codecs into MP4", () => { + const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.wav", "out.mp4", { + audioMode: "copy-source", + outputDurationSec: 60, + }); + + expect(args).toEqual(expect.arrayContaining(["-c:a", "aac", "-b:a", "192k"])); + expect(args.join(";")).not.toContain("-c:a;copy"); + }); + + it("keeps filtered audio on the AAC encode path", () => { + const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.mp4", "out.mp4", { + audioMode: "trim-source", + trimSegments: [{ startMs: 0, endMs: 1_000 }], + outputDurationSec: 1, + }); + + expect(args).toEqual(expect.arrayContaining(["-filter_complex"])); + expect(args).toEqual(expect.arrayContaining(["-c:a", "aac", "-b:a", "192k"])); + }); + + it("pads and trims edited-track filtergraph audio to the expected duration", () => { + const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.mp4", "out.mp4", { + audioMode: "edited-track", + editedTrackStrategy: "filtergraph-fast-path", + audioSourceSampleRate: 48_000, + editedTrackSegments: [{ startMs: 0, endMs: 4_000, speed: 0.5 }], + outputDurationSec: 8, + }); + + expect(args).toEqual(expect.arrayContaining(["-map", "[aout_sync]"])); + expect(args.join(";")).toContain( + "[aout]apad,atrim=duration=8.000,asetpts=PTS-STARTPTS[aout_sync]", + ); + }); + + it("can enable machine-readable FFmpeg mux progress", () => { + const args = buildNativeVideoAudioMuxArgs( + "video.mp4", + "source.mp4", + "out.mp4", + { audioMode: "copy-source", audioSourceCodec: "aac", outputDurationSec: 60 }, + { progressPipe: 2 }, + ); + + expect(args).toEqual( + expect.arrayContaining(["-stats_period", "0.5", "-progress", "pipe:2", "-nostats"]), + ); + }); +}); + +describe("canCopyAudioCodecIntoMp4", () => { + it("allows common MP4-compatible audio codecs", () => { + expect(canCopyAudioCodecIntoMp4("aac (LC) (mp4a / 0x6134706D)")).toBe(true); + expect(canCopyAudioCodecIntoMp4("mp3")).toBe(true); + }); + + it("blocks Opus so native exports transcode it to AAC for MP4", () => { + expect(canCopyAudioCodecIntoMp4("opus")).toBe(false); + }); + + it("blocks unknown codecs so sidecar WAV/PCM audio is encoded for MP4", () => { + expect(canCopyAudioCodecIntoMp4(undefined)).toBe(false); + expect(canCopyAudioCodecIntoMp4("")).toBe(false); + }); +}); + +describe("validateNativeVideoStreamStats", () => { + it("accepts a complete video stream", () => { + expect( + validateNativeVideoStreamStats( + { durationSec: 45, frameCount: 1350, frameRate: 30 }, + { durationSec: 45, targetFrames: 1350 }, + ), + ).toEqual([]); + }); + + it("rejects files with container duration but too few video frames", () => { + expect( + validateNativeVideoStreamStats( + { durationSec: 0.067, frameCount: 2, frameRate: 30 }, + { durationSec: 45, targetFrames: 1350 }, + ), + ).toEqual([ + "video frames 2 below expected minimum 1282", + "video stream duration 0.067s differs from expected 45.000s", + ]); + }); +}); + +describe("parseNativeVideoStreamStatsProbeOutput", () => { + it("parses FFprobe count-frame JSON output", () => { + expect( + parseNativeVideoStreamStatsProbeOutput( + JSON.stringify({ + streams: [ + { + duration: "44.999000", + nb_read_frames: "1349", + avg_frame_rate: "30000/1001", + }, + ], + }), + ), + ).toEqual({ + durationSec: 44.999, + frameCount: 1349, + frameRate: 30000 / 1001, + }); + }); +}); + +describe("parseFfmpegProgressLineSeconds", () => { + it("parses FFmpeg progress timestamps into seconds", () => { + expect(parseFfmpegProgressLineSeconds("out_time_us=1500000")).toBe(1.5); + expect(parseFfmpegProgressLineSeconds("out_time_ms=2500000")).toBe(2.5); + expect(parseFfmpegProgressLineSeconds("out_time=00:01:02.500000")).toBe(62.5); + expect(parseFfmpegProgressLineSeconds("progress=continue")).toBeNull(); + }); +}); + +describe("parseWindowsGpuExportSummary", () => { + it("returns the last JSON summary from helper stdout", () => { + const summary = parseWindowsGpuExportSummary( + [ + "initializing", + '{"success":true,"frames":30,"totalMs":1000,"realtimeMultiplier":2}', + "cleanup", + '{"success":true,"frames":60,"surfacePoolSize":12,"readMs":12.5,"videoProcessMs":30,"writeSampleMs":40,"finalizeMs":5,"realtimeMultiplier":4}', + ].join("\n"), + ); + + expect(summary).toEqual({ + success: true, + frames: 60, + surfacePoolSize: 12, + readMs: 12.5, + videoProcessMs: 30, + writeSampleMs: 40, + finalizeMs: 5, + realtimeMultiplier: 4, + }); + }); + + it("returns null when helper stdout has no valid JSON summary", () => { + expect(parseWindowsGpuExportSummary("initializing\nnot-json")).toBeNull(); + expect(parseWindowsGpuExportSummary("")).toBeNull(); + }); +}); + +describe("validateWindowsGpuExportSummary", () => { + it("rejects short Windows GPU outputs before muxing audio", () => { + expect( + validateWindowsGpuExportSummary( + { success: true, frames: 2, seconds: 0.067 }, + { durationSec: 45, targetFrames: 1350 }, + ), + ).toEqual([ + "Windows GPU frames 2 below expected minimum 1282", + "Windows GPU duration 0.067s differs from expected 45.000s", + ]); + }); + + it("accepts complete Windows GPU outputs within tolerance", () => { + expect( + validateWindowsGpuExportSummary( + { success: true, frames: 1345, seconds: 44.9 }, + { durationSec: 45, targetFrames: 1350 }, + ), + ).toEqual([]); + }); +}); + +describe("parseNvidiaCudaExportSummary", () => { + it("parses the pretty JSON summary emitted by the CUDA wrapper", () => { + const summary = parseNvidiaCudaExportSummary( + [ + "preflight", + JSON.stringify( + { + success: true, + fps: 30, + durationSec: 10, + targetFrames: 300, + timingsMs: { nativeEncode: 920, mux: 45, endToEnd: 1400 }, + nativeSummary: { success: true, frames: 300, fps: 326.1 }, + }, + null, + 2, + ), + ].join("\n"), + ); + + expect(summary?.success).toBe(true); + expect(summary?.targetFrames).toBe(300); + expect(summary?.timingsMs?.nativeEncode).toBe(920); + expect(summary?.nativeSummary?.fps).toBe(326.1); + }); + + it("returns null when the wrapper output has no JSON object", () => { + expect(parseNvidiaCudaExportSummary("native helper failed before summary")).toBeNull(); + }); +}); + +describe("validateNvidiaCudaExportSummary", () => { + it("accepts CUDA output when frames and stream durations match the export target", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { success: true, frames: 300 }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300 }, + ); + + expect(issues).toEqual([]); + }); + + it("rejects CUDA output that reports too few frames or a short video stream", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { success: true, frames: 144 }, + outputVideo: { duration: "4.799952", nb_frames: "144" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300 }, + ); + + expect(issues).toEqual([ + "native frames 144 below expected minimum 285", + "output video frames 144 below expected minimum 285", + "output video duration 4.800s differs from expected 10.000s", + ]); + }); + + it("rejects audio CUDA output unless the helper reports timestamp-aligned frame selection", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + selectionStage: "decoder-policy-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: true }, + ); + + expect(issues).toEqual(["CUDA timeline mode is not timestamp-aligned for audio export"]); + }); + + it("accepts video-only CUDA output when audio is muxed by the shared export path", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + selectionStage: "decoder-policy-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: false }, + ); + + expect(issues).toEqual([]); + }); + + it("rejects inline-audio CUDA output when the helper does not produce audio", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + sourceTimestampMode: "pts", + selectionStage: "timestamp-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: true }, + ); + + expect(issues).toEqual(["missing output audio stream"]); + }); + + it("rejects inline-audio CUDA output when the probed audio stream is empty", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + sourceTimestampMode: "pts", + selectionStage: "timestamp-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: { duration: "0.000000" }, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: true }, + ); + + expect(issues).toEqual(["output audio duration is not positive"]); + }); + + it("rejects inline-audio CUDA output when the audio duration is missing", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + sourceTimestampMode: "pts", + selectionStage: "timestamp-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: {}, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: true }, + ); + + expect(issues).toEqual(["missing output audio duration"]); + }); + + it("accepts audio CUDA output when the helper reports PTS-aligned selection", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + sourceTimestampMode: "pts", + selectionStage: "timestamp-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: true }, + ); + + expect(issues).toEqual([]); + }); +}); + +describe("parseWindowsGpuExportProgressLine", () => { + it("parses bounded helper progress lines", () => { + expect( + parseWindowsGpuExportProgressLine( + 'PROGRESS {"currentFrame":30,"totalFrames":60,"percentage":50,"averageFps":240.5,"instantFps":180.25,"intervalMs":166.4,"intervalFrames":30,"intervalEncodeMs":120.2,"intervalPipelineWaitMs":46.2,"intervalMonolithicCompositeFrames":0,"stage":"finalizing"}', + ), + ).toEqual({ + currentFrame: 30, + totalFrames: 60, + percentage: 50, + stage: "finalizing", + averageFps: 240.5, + instantFps: 180.25, + intervalMs: 166.4, + intervalFrames: 30, + intervalEncodeMs: 120.2, + intervalPipelineWaitMs: 46.2, + intervalMonolithicCompositeFrames: 0, + }); + }); + + it("preserves CUDA preparation progress as a non-rendering stage", () => { + expect( + parseWindowsGpuExportProgressLine( + 'PROGRESS {"currentFrame":0,"totalFrames":100,"percentage":2.5,"stage":"preparing"}', + ), + ).toEqual({ + currentFrame: 0, + totalFrames: 100, + percentage: 2.5, + stage: "preparing", + }); + }); + + it("ignores non-progress or malformed helper stderr", () => { + expect(parseWindowsGpuExportProgressLine("warning: encoder selected")).toBeNull(); + expect(parseWindowsGpuExportProgressLine("PROGRESS not-json")).toBeNull(); + expect( + parseWindowsGpuExportProgressLine( + 'PROGRESS {"currentFrame":1,"totalFrames":0,"percentage":999}', + ), + ).toBeNull(); + }); +}); + +describe("mapNvidiaCudaWrapperProgressPercentage", () => { + it("keeps preflight progress and maps native encode into the main export span", () => { + expect( + mapNvidiaCudaWrapperProgressPercentage({ + currentFrame: 0, + totalFrames: 100, + percentage: 2.5, + }), + ).toBe(2.5); + expect( + mapNvidiaCudaWrapperProgressPercentage({ + currentFrame: 0, + totalFrames: 100, + percentage: 0, + }), + ).toBe(3); + expect( + mapNvidiaCudaWrapperProgressPercentage({ + currentFrame: 50, + totalFrames: 100, + percentage: 50, + }), + ).toBe(50.5); + expect( + mapNvidiaCudaWrapperProgressPercentage({ + currentFrame: 100, + totalFrames: 100, + percentage: 100, + }), + ).toBe(98); + expect( + mapNvidiaCudaWrapperProgressPercentage({ + currentFrame: 100, + totalFrames: 100, + percentage: 97.25, + stage: "finalizing", + }), + ).toBe(97.25); + }); +}); + +describe("hasNativeStaticLayoutProgressAdvanced", () => { + it("treats repeated preparation heartbeats as stalled until real progress arrives", () => { + const previous = { currentFrame: 0, percentage: 2.5 }; + + expect( + hasNativeStaticLayoutProgressAdvanced( + { currentFrame: 0, totalFrames: 100, percentage: 2.5, stage: "preparing" }, + previous, + ), + ).toBe(false); + expect( + hasNativeStaticLayoutProgressAdvanced( + { currentFrame: 0, totalFrames: 100, percentage: 2.7, stage: "preparing" }, + previous, + ), + ).toBe(true); + expect( + hasNativeStaticLayoutProgressAdvanced( + { currentFrame: 1, totalFrames: 100, percentage: 2.5 }, + previous, + ), + ).toBe(true); + expect( + hasNativeStaticLayoutProgressAdvanced( + { currentFrame: 100, totalFrames: 100, percentage: 97.25, stage: "finalizing" }, + { currentFrame: 100, percentage: 97.25 }, + ), + ).toBe(true); + }); +}); + +describe("parseNativeVideoMetadataProbeOutput", () => { + it("parses FFmpeg input metadata with video and audio streams", () => { + const metadata = parseNativeVideoMetadataProbeOutput(` +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'recording.mp4': + Metadata: + major_brand : isom + Duration: 00:06:04.25, start: 0.000000, bitrate: 3938 kb/s + Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 1920x1080, 3720 kb/s, 46.05 fps, 60 tbr, 90k tbn (default) + Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 192 kb/s (default) +`); + + expect(metadata).toEqual({ + width: 1920, + height: 1080, + duration: 364.25, + mediaStartTime: 0, + streamStartTime: 0, + streamDuration: 364.25, + frameRate: 46.05, + codec: "h264 (High) (avc1 / 0x31637661)", + hasAudio: true, + audioCodec: "aac (LC) (mp4a / 0x6134706D)", + audioSampleRate: 48000, + }); + }); + + it("parses video-only metadata and falls back to tbr when fps is absent", () => { + const metadata = parseNativeVideoMetadataProbeOutput(` +Input #0, matroska,webm, from 'recording.webm': + Duration: 00:00:10.50, start: 0.023000, bitrate: 1000 kb/s + Stream #0:0: Video: vp9, yuv420p, 1280x720, 30 tbr, 1k tbn +`); + + expect(metadata).toEqual({ + width: 1280, + height: 720, + duration: 10.5, + mediaStartTime: 0.023, + streamStartTime: 0.023, + streamDuration: 10.5, + frameRate: 30, + codec: "vp9", + hasAudio: false, + audioCodec: undefined, + audioSampleRate: undefined, + }); + }); + + it("rejects output without usable video metadata", () => { + expect(parseNativeVideoMetadataProbeOutput("Duration: N/A")).toBeNull(); + expect(parseNativeVideoMetadataProbeOutput("not a media file")).toBeNull(); + }); +}); + +describe("parseFfmpegDurationSeconds", () => { + it("parses HH:MM:SS timestamps", () => { + expect(parseFfmpegDurationSeconds("01:02:03.5")).toBe(3723.5); + expect(parseFfmpegDurationSeconds("bad")).toBeNull(); + }); +}); + +describe("parseFfmpegFrameRate", () => { + it("prefers fps and falls back to tbr", () => { + expect(parseFfmpegFrameRate("Video: h264, 1920x1080, 59.94 fps, 60 tbr")).toBe(59.94); + expect(parseFfmpegFrameRate("Video: h264, 1920x1080, 30 tbr")).toBe(30); + expect(parseFfmpegFrameRate("Video: h264")).toBeNull(); + }); +}); diff --git a/electron/ipc/export/native-video.ts b/electron/ipc/export/native-video.ts new file mode 100644 index 000000000..1aa5fc4e2 --- /dev/null +++ b/electron/ipc/export/native-video.ts @@ -0,0 +1,4195 @@ +import { probeNativeVideoMetadata, type NativeVideoMetadataProbe } from "../ffmpeg/metadata"; +export { + probeNativeVideoMetadata, + parseNativeVideoMetadataProbeOutput, + parseFfmpegDurationSeconds, + parseFfmpegFrameRate, + type NativeVideoMetadataProbe, +} from "../ffmpeg/metadata"; +import type { ChildProcessByStdio } from "node:child_process"; +import { execFile, spawn } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { performance } from "node:perf_hooks"; +import type { Readable, Writable } from "node:stream"; +import { promisify } from "node:util"; +import type { WebContents } from "electron"; +import { app, powerSaveBlocker } from "electron"; +import { getFfmpegBinaryPath, getFfprobeBinaryPath } from "../ffmpeg/binary"; +import type { + NativeExportEncodingMode, + NativeStaticLayoutBackend, + NativeStaticLayoutExportArgsConfig, + NativeVideoAudioMuxMetrics, + NativeVideoExportAudioMode, + NativeVideoExportEditedTrackSegment, + NativeVideoExportFinishOptions, +} from "../nativeVideoExport"; +import { + buildEditedTrackSourceAudioFilter, + buildNativeConcatArgs, + buildNativeCudaOverlayStaticLayoutArgs, + buildNativeCudaScaleCpuPadStaticLayoutArgs, + buildNativePrecompositedStaticLayoutArgs, + buildNativeStaticBackgroundRenderArgs, + buildNativeStaticLayoutChunks, + buildNativeVideoExportArgs, + buildTrimmedSourceAudioFilter, + createNativeSquircleMaskPgmBuffer, + getEditedAudioExtension, + getNativeVideoInputByteSize, + getPreferredNativeVideoEncoders, + isNativeCudaOutOfMemory, + parseAvailableFfmpegEncoders, +} from "../nativeVideoExport"; +import { cachedNativeVideoEncoder, setCachedNativeVideoEncoder } from "../state"; + +const execFileAsync = promisify(execFile); +const getNowMs = () => performance.now(); +const formatFfmpegSeconds = (milliseconds: number) => (milliseconds / 1000).toFixed(3); +const MISSING_NATIVE_STATIC_BACKGROUND_COLOR = "#ffffff"; +const NATIVE_EXPORT_HIGH_PRIORITY = os.constants.priority.PRIORITY_HIGH; +const NVIDIA_PCI_VENDOR_ID = 0x10de; +const NVIDIA_CUDA_EXPORT_ENV = "RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT"; +const NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT"; +const NVIDIA_CUDA_FORCE_VIDEO_ONLY_ENV = "RECORDLY_NVIDIA_CUDA_FORCE_VIDEO_ONLY"; +const NVIDIA_CUDA_AUTO_STALL_TIMEOUT_ENV = "RECORDLY_NVIDIA_CUDA_AUTO_STALL_TIMEOUT_MS"; +const DEFAULT_NVIDIA_CUDA_AUTO_STALL_TIMEOUT_MS = 120_000; +const NATIVE_GPU_STALL_TIMEOUT_ENV = "RECORDLY_NATIVE_GPU_STALL_TIMEOUT_MS"; +const DEFAULT_NATIVE_GPU_STALL_TIMEOUT_MS = 120_000; +const NATIVE_STATIC_LAYOUT_SOURCE_PROXY_REFERENCE_PIXEL_RATE = 1920 * 1080 * 30; +const NATIVE_STATIC_LAYOUT_SOURCE_PROXY_1080P30_BITRATE = 24_000_000; +const NATIVE_STATIC_LAYOUT_SOURCE_PROXY_MAX_BITRATE = 80_000_000; +const NATIVE_STATIC_LAYOUT_SOURCE_PROXY_CONTAINERS = new Set([".mp4", ".m4v", ".mov"]); + +type ElectronGpuDeviceLike = { + active?: boolean; + vendorId?: number | string; + vendorString?: string; + deviceString?: string; +}; + +type ElectronGpuInfoLike = { + gpuDevice?: ElectronGpuDeviceLike[]; + machineModelName?: string; + machineModelVersion?: string; +}; + +export interface ExportHardwareInfo { + platform: NodeJS.Platform; + release: string; + arch: string; + cpuModel: string | null; + logicalProcessors: number; + totalMemoryGb: number; + machineModel: string | null; + gpus: Array<{ + name: string; + vendor: string | null; + active: boolean | null; + }>; + gpuFeatures: { + videoDecode: string | null; + videoEncode: string | null; + webgl: string | null; + webgpu: string | null; + }; +} + +export type NativeVideoExportSession = { + ffmpegProcess: ChildProcessByStdio; + outputPath: string; + inputByteSize: number; + inputMode: "rawvideo" | "h264-stream"; + maxQueuedWriteBytes: number; + stderrOutput: string; + encoderName: string; + processError: Error | null; + stdinError: Error | null; + terminating: boolean; + writeSequence: Promise; + completionPromise: Promise; + sender: WebContents | null; + pendingWriteRequestIds: Set; +}; + +export const nativeVideoExportSessions = new Map(); + +export interface NativeStaticLayoutTimelineSegment { + sourceStartMs: number; + sourceEndMs: number; + outputStartMs: number; + outputEndMs: number; + speed: number; +} + +export interface NativeStaticLayoutExportOptions { + sessionId?: string; + inputPath: string; + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: NativeExportEncodingMode; + durationSec: number; + contentWidth: number; + contentHeight: number; + offsetX: number; + offsetY: number; + sourceCropX?: number; + sourceCropY?: number; + sourceCropWidth?: number; + sourceCropHeight?: number; + backgroundColor: string; + backgroundImagePath?: string | null; + backgroundBlurPx?: number; + borderRadius?: number; + shadowIntensity?: number; + webcamInputPath?: string | null; + webcamLeft?: number; + webcamTop?: number; + webcamSize?: number; + webcamRadius?: number; + webcamShadowIntensity?: number; + webcamMirror?: boolean; + webcamTimeOffsetMs?: number; + cursorTelemetry?: Array<{ + timeMs: number; + cx: number; + cy: number; + cursorTypeIndex?: number; + bounceScale?: number; + visible?: boolean; + }>; + cursorTelemetryPath?: string | null; + cursorSize?: number; + cursorAtlasPngDataUrl?: string | null; + cursorAtlasPath?: string | null; + cursorAtlasEntries?: Array<{ + index: number; + x: number; + y: number; + width: number; + height: number; + anchorX: number; + anchorY: number; + aspectRatio: number; + }>; + cursorAtlasMetadataPath?: string | null; + zoomTelemetry?: Array<{ timeMs: number; scale: number; x: number; y: number }>; + zoomTelemetryPath?: string | null; + timelineSegments?: NativeStaticLayoutTimelineSegment[]; + timelineMapPath?: string | null; + chunkDurationSec?: number; + experimentalWindowsGpuCompositor?: boolean; + experimentalNvidiaCudaExport?: boolean; + audioOptions?: NativeVideoExportFinishOptions; + nvidiaCudaForceVideoOnly?: boolean; +} + +export interface NativeStaticLayoutExportProgress { + sessionId?: string; + backend?: NativeStaticLayoutBackend; + stage?: "preparing" | "finalizing"; + elapsedMs?: number; + averageFps?: number; + instantFps?: number; + intervalMs?: number; + intervalFrames?: number; + intervalDecodeWallMs?: number; + intervalEncodeMs?: number; + intervalPipelineWaitMs?: number; + intervalCompositeMs?: number; + intervalNvencMs?: number; + intervalPacketWriteMs?: number; + intervalWebcamDecodeMs?: number; + intervalWebcamCopyMs?: number; + intervalRoiCompositeFrames?: number; + intervalMonolithicCompositeFrames?: number; + intervalCopyCompositeFrames?: number; + currentFrame: number; + totalFrames: number; + percentage: number; +} + +type NativeVideoAudioMuxProgress = { + ratio: number; + processedSec?: number; + totalSec?: number; +}; + +type NativeVideoAudioMuxArgsOptions = { + progressPipe?: 1 | 2; +}; + +export interface WindowsGpuExportSummary { + success?: boolean; + width?: number; + height?: number; + fps?: number; + seconds?: number; + mediaMs?: number; + frames?: number; + gpuDecodeSurface?: boolean; + webcamOverlay?: boolean; + cursorOverlay?: boolean; + cursorAtlas?: boolean; + zoomOverlay?: boolean; + surfacePoolSize?: number; + adapterIndex?: number; + adapterVendorId?: number; + adapterDeviceId?: number; + adapterDedicatedVideoMemoryMB?: number; + encoderBackend?: string; + encoderTuningApplied?: boolean; + nvencOutputBytes?: number; + initializeMs?: number; + initCoInitializeMs?: number; + initMfStartupMs?: number; + initD3DDeviceMs?: number; + initSourceReaderMs?: number; + initWebcamReaderMs?: number; + initVideoProcessorMs?: number; + initTexturesMs?: number; + initShaderPipelineMs?: number; + initSinkWriterMs?: number; + totalMs?: number; + readMs?: number; + clearMs?: number; + videoProcessMs?: number; + writeSampleMs?: number; + finalizeMs?: number; + realtimeMultiplier?: number; +} + +export interface NvidiaCudaExportSummary { + success?: boolean; + inputPath?: string; + outputPath?: string; + fps?: number; + bitrateMbps?: number; + durationSec?: number; + targetFrames?: number; + sourcePtsFrames?: number; + sourcePtsSource?: string; + timingsMs?: { + demux?: number; + backgroundConvert?: number; + cursorAtlas?: number; + webcamConvert?: number; + webcamDemux?: number; + sourcePtsProbe?: number; + nativeEncode?: number; + mux?: number; + endToEnd?: number; + }; + nativeSummary?: { + success?: boolean; + selectionStage?: string; + sourceTimestampMode?: string; + timelineMode?: string; + frames?: number; + totalMs?: number; + fps?: number; + measuredFps?: number; + mappedDisplayFrames?: number; + selectedDisplayFrames?: number; + skippedDisplayFrames?: number; + roiCompositeFrames?: number; + monolithicCompositeFrames?: number; + copyCompositeFrames?: number; + cursorAtlas?: boolean; + webcamOverlay?: boolean; + zoomOverlay?: boolean; + zoomSamples?: number; + }; + nativeProcessPriorityBoosted?: boolean; + appRuntimeGuard?: { + powerGuardStarted?: boolean; + wrapperProcessPriorityBoosted?: boolean; + nativeProcessPriorityBoosted?: boolean; + }; + gpuSamples?: unknown[]; + gpuSummary?: unknown; + outputVideo?: unknown; + outputAudio?: unknown; +} + +export interface NativeVideoStreamStatsProbe { + durationSec: number | null; + frameCount: number | null; + frameRate: number | null; +} + +export interface NativeStaticLayoutChunkMetric { + index: number; + startSec: number; + durationSec: number; + backend: NativeStaticLayoutBackend; + elapsedMs: number; + outputBytes: number; + fallbackReason?: string; + windowsGpuSummary?: WindowsGpuExportSummary; + nvidiaCudaSummary?: NvidiaCudaExportSummary; +} + +export interface NativeStaticLayoutExportMetrics extends NativeVideoAudioMuxMetrics { + chunkCount: number; + chunkDurationSec: number; + chunkExecMs: number; + concatExecMs?: number; + staticAssetExecMs?: number; + fallbackChunkCount: number; + videoOnlyBytes?: number; + chunks: NativeStaticLayoutChunkMetric[]; +} + +export interface NativeStaticLayoutExportSession { + terminating: boolean; + currentProcess: ReturnType | null; +} + +export function cleanupNativeVideoExportSessions() { + for (const [sessionId, session] of nativeVideoExportSessions) { + session.terminating = true; + try { + if (!session.ffmpegProcess.stdin.destroyed) { + session.ffmpegProcess.stdin.destroy(); + } + } catch { + /* stream may already be closed */ + } + try { + session.ffmpegProcess.kill("SIGKILL"); + } catch { + /* process may already be exited */ + } + nativeVideoExportSessions.delete(sessionId); + } + + for (const [sessionId, session] of nativeStaticLayoutExportSessions) { + session.terminating = true; + try { + session.currentProcess?.kill("SIGKILL"); + } catch { + /* process may already be exited */ + } + nativeStaticLayoutExportSessions.delete(sessionId); + } +} + +export function parseWindowsGpuExportSummary(stdout: string): WindowsGpuExportSummary | null { + const summaryLine = stdout + .trim() + .split(/\r?\n/) + .reverse() + .find((line) => line.trim().startsWith("{")); + if (!summaryLine) { + return null; + } + + try { + const parsed = JSON.parse(summaryLine) as WindowsGpuExportSummary; + return parsed && typeof parsed === "object" ? parsed : null; + } catch { + return null; + } +} + +export function parseNvidiaCudaExportSummary(stdout: string): NvidiaCudaExportSummary | null { + const trimmed = stdout.trim(); + const startIndex = trimmed.indexOf("{"); + const endIndex = trimmed.lastIndexOf("}"); + if (startIndex === -1 || endIndex <= startIndex) { + return null; + } + + try { + const parsed = JSON.parse( + trimmed.slice(startIndex, endIndex + 1), + ) as NvidiaCudaExportSummary; + return parsed && typeof parsed === "object" ? parsed : null; + } catch { + return null; + } +} + +function getFiniteNumber(value: unknown) { + const numberValue = typeof value === "string" ? Number(value) : value; + return typeof numberValue === "number" && Number.isFinite(numberValue) ? numberValue : null; +} + +function getNvidiaCudaOutputStreamNumber(stream: unknown, property: string) { + if (!stream || typeof stream !== "object") { + return null; + } + + return getFiniteNumber((stream as Record)[property]); +} + +export function validateNvidiaCudaExportSummary( + summary: NvidiaCudaExportSummary, + expected: { + durationSec: number; + targetFrames: number; + requiresTimelineSync?: boolean; + }, +) { + const issues: string[] = []; + const expectedFrames = Math.max(1, Math.round(expected.targetFrames)); + const minimumFrames = Math.max(1, Math.floor(expectedFrames * 0.95)); + const expectedDurationSec = Math.max(0, expected.durationSec); + const durationToleranceSec = Math.min(2, Math.max(0.5, expectedDurationSec * 0.02)); + const nativeFrames = getFiniteNumber(summary.nativeSummary?.frames); + const outputVideoFrames = getNvidiaCudaOutputStreamNumber(summary.outputVideo, "nb_frames"); + const outputVideoDurationSec = getNvidiaCudaOutputStreamNumber(summary.outputVideo, "duration"); + const outputAudioDurationSec = getNvidiaCudaOutputStreamNumber(summary.outputAudio, "duration"); + + if (!summary.outputVideo) { + issues.push("missing output video probe"); + } + if (expected.requiresTimelineSync && !isNvidiaCudaTimestampAlignedSummary(summary)) { + issues.push("CUDA timeline mode is not timestamp-aligned for audio export"); + } + if (expected.requiresTimelineSync) { + if (!summary.outputAudio) { + issues.push("missing output audio stream"); + } else if (outputAudioDurationSec === null) { + issues.push("missing output audio duration"); + } else if (outputAudioDurationSec <= 0) { + issues.push("output audio duration is not positive"); + } + } + if (nativeFrames === null) { + issues.push("missing native frame count"); + } else if (nativeFrames < minimumFrames) { + issues.push(`native frames ${nativeFrames} below expected minimum ${minimumFrames}`); + } + if (outputVideoFrames !== null && outputVideoFrames < minimumFrames) { + issues.push( + `output video frames ${outputVideoFrames} below expected minimum ${minimumFrames}`, + ); + } + if ( + outputVideoDurationSec !== null && + Math.abs(outputVideoDurationSec - expectedDurationSec) > durationToleranceSec + ) { + issues.push( + `output video duration ${outputVideoDurationSec.toFixed( + 3, + )}s differs from expected ${expectedDurationSec.toFixed(3)}s`, + ); + } + if ( + outputAudioDurationSec !== null && + outputAudioDurationSec > 0 && + Math.abs(outputAudioDurationSec - expectedDurationSec) > durationToleranceSec + ) { + issues.push( + `output audio duration ${outputAudioDurationSec.toFixed( + 3, + )}s differs from expected ${expectedDurationSec.toFixed(3)}s`, + ); + } + + return issues; +} + +export function validateWindowsGpuExportSummary( + summary: WindowsGpuExportSummary, + expected: { + durationSec: number; + targetFrames: number; + }, +) { + const issues: string[] = []; + const expectedFrames = Math.max(1, Math.round(expected.targetFrames)); + const minimumFrames = Math.max(1, Math.floor(expectedFrames * 0.95)); + const expectedDurationSec = Math.max(0, expected.durationSec); + const durationToleranceSec = Math.min(2, Math.max(0.5, expectedDurationSec * 0.02)); + const frames = getFiniteNumber(summary.frames); + const mediaMs = getFiniteNumber(summary.mediaMs); + const seconds = getFiniteNumber(summary.seconds) ?? (mediaMs !== null ? mediaMs / 1000 : null); + + if (frames === null) { + issues.push("missing Windows GPU frame count"); + } else if (frames < minimumFrames) { + issues.push(`Windows GPU frames ${frames} below expected minimum ${minimumFrames}`); + } + if ( + seconds !== null && + Number.isFinite(seconds) && + Math.abs(seconds - expectedDurationSec) > durationToleranceSec + ) { + issues.push( + `Windows GPU duration ${seconds.toFixed(3)}s differs from expected ${expectedDurationSec.toFixed(3)}s`, + ); + } + + return issues; +} + +function parseRationalFrameRate(value: unknown) { + if (typeof value !== "string") { + return null; + } + const [numeratorRaw, denominatorRaw] = value.split("/"); + const numerator = Number(numeratorRaw); + const denominator = Number(denominatorRaw); + if ( + !Number.isFinite(numerator) || + !Number.isFinite(denominator) || + numerator <= 0 || + denominator <= 0 + ) { + return null; + } + + return numerator / denominator; +} + +function parseOptionalPositiveNumber(value: unknown) { + const parsed = + typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; +} + +function parseOptionalPositiveInteger(value: unknown) { + const parsed = parseOptionalPositiveNumber(value); + return parsed === null ? null : Math.max(0, Math.round(parsed)); +} + +export function parseNativeVideoStreamStatsProbeOutput( + output: string, +): NativeVideoStreamStatsProbe | null { + const parsed = JSON.parse(output) as { + streams?: Array<{ + duration?: unknown; + nb_frames?: unknown; + nb_read_frames?: unknown; + avg_frame_rate?: unknown; + r_frame_rate?: unknown; + }>; + }; + const stream = parsed.streams?.[0]; + if (!stream) { + return null; + } + + return { + durationSec: parseOptionalPositiveNumber(stream.duration), + frameCount: + parseOptionalPositiveInteger(stream.nb_read_frames) ?? + parseOptionalPositiveInteger(stream.nb_frames), + frameRate: + parseRationalFrameRate(stream.avg_frame_rate) ?? + parseRationalFrameRate(stream.r_frame_rate), + }; +} + +export function validateNativeVideoStreamStats( + stats: NativeVideoStreamStatsProbe, + expected: { + durationSec: number; + targetFrames: number; + }, +) { + const issues: string[] = []; + const expectedFrames = Math.max(1, Math.round(expected.targetFrames)); + const minimumFrames = Math.max(1, Math.floor(expectedFrames * 0.95)); + const expectedDurationSec = Math.max(0, expected.durationSec); + const durationToleranceSec = Math.min(2, Math.max(0.5, expectedDurationSec * 0.02)); + + if (stats.frameCount === null) { + issues.push("missing video frame count"); + } else if (stats.frameCount < minimumFrames) { + issues.push(`video frames ${stats.frameCount} below expected minimum ${minimumFrames}`); + } + + if (stats.durationSec === null) { + issues.push("missing video stream duration"); + } else if (Math.abs(stats.durationSec - expectedDurationSec) > durationToleranceSec) { + issues.push( + `video stream duration ${stats.durationSec.toFixed( + 3, + )}s differs from expected ${expectedDurationSec.toFixed(3)}s`, + ); + } + + return issues; +} + +function isNvidiaCudaTimestampAlignedSummary(summary: NvidiaCudaExportSummary) { + const nativeSummary = summary.nativeSummary; + const timelineFields = [ + nativeSummary?.sourceTimestampMode, + nativeSummary?.timelineMode, + nativeSummary?.selectionStage, + ]; + return timelineFields.some((value) => { + const normalized = String(value ?? "").toLowerCase(); + return normalized.includes("pts") || normalized.includes("timestamp"); + }); +} + +function shouldPersistNativeExportDiagnostics() { + const rawValue = process.env.RECORDLY_NATIVE_EXPORT_DIAGNOSTICS?.trim().toLowerCase(); + return rawValue !== "0" && rawValue !== "off" && rawValue !== "false"; +} + +function shouldPersistNvidiaCudaExportDiagnostics() { + return ( + shouldPersistNativeExportDiagnostics() || + process.env.RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS === "1" + ); +} + +function getSafeDiagnosticsFileSegment(value: string | undefined) { + const safe = (value || "session").replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 96); + return safe || "session"; +} + +function getCliArgValue(args: string[], name: string) { + const index = args.indexOf(name); + if (index === -1 || index + 1 >= args.length) { + return null; + } + return args[index + 1] || null; +} + +async function persistNvidiaCudaExportDiagnostics(params: { + args: string[]; + code: number | null; + elapsedMs: number; + outputPath: string; + sessionId?: string; + signal: NodeJS.Signals | null; + startedAtIso: string; + stderr: string; + stdout: string; + summary: NvidiaCudaExportSummary | null; + timedOut: boolean; +}) { + if (!shouldPersistNvidiaCudaExportDiagnostics()) { + return; + } + + const diagnosticsDirectory = path.join(app.getPath("userData"), "native-export-diagnostics"); + const filePrefix = `${Date.now()}-${getSafeDiagnosticsFileSegment(params.sessionId)}`; + const manifest = { + backend: "nvidia-cuda-compositor", + startedAt: params.startedAtIso, + completedAt: new Date().toISOString(), + elapsedMs: Number(params.elapsedMs.toFixed(2)), + sessionId: params.sessionId ?? null, + outputPath: params.outputPath, + exitCode: params.code, + signal: params.signal, + timedOut: params.timedOut, + args: params.args, + summary: params.summary, + }; + + try { + await fs.mkdir(diagnosticsDirectory, { recursive: true }); + const artifactsDirectory = path.join(diagnosticsDirectory, `${filePrefix}.artifacts`); + const artifactArgs = [ + "--cursor-json", + "--cursor-atlas-png", + "--cursor-atlas-metadata", + "--zoom-telemetry", + ]; + const artifactCopies = artifactArgs + .map((argName) => { + const sourcePath = getCliArgValue(params.args, argName); + return sourcePath + ? { + argName, + sourcePath, + outputPath: path.join( + artifactsDirectory, + `${getSafeDiagnosticsFileSegment(argName.replace(/^--/, ""))}-${path.basename(sourcePath)}`, + ), + } + : null; + }) + .filter((artifact): artifact is NonNullable => Boolean(artifact)); + await fs.mkdir(artifactsDirectory, { recursive: true }).catch(() => undefined); + await Promise.allSettled([ + fs.writeFile( + path.join(diagnosticsDirectory, `${filePrefix}.manifest.json`), + `${JSON.stringify(manifest, null, 2)}\n`, + ), + fs.writeFile( + path.join(diagnosticsDirectory, `${filePrefix}.stdout.json`), + params.stdout, + ), + fs.writeFile( + path.join(diagnosticsDirectory, `${filePrefix}.stderr.log`), + params.stderr, + ), + ...artifactCopies.map((artifact) => + fs.copyFile(artifact.sourcePath, artifact.outputPath).catch(() => undefined), + ), + ]); + } catch (error) { + console.warn( + "[native-static-layout-export] Failed to persist NVIDIA CUDA diagnostics", + error, + ); + } +} + +async function persistWindowsGpuExportDiagnostics(params: { + args: string[]; + code: number | null; + elapsedMs: number; + outputPath: string; + signal: NodeJS.Signals | null; + startedAtIso: string; + stderr: string; + stdout: string; + summary: WindowsGpuExportSummary | null; + timedOut: boolean; +}) { + if (!shouldPersistNativeExportDiagnostics()) { + return; + } + + const diagnosticsDirectory = path.join(app.getPath("userData"), "native-export-diagnostics"); + const filePrefix = `${Date.now()}-windows-d3d11-compositor`; + const manifest = { + backend: "windows-d3d11-compositor", + startedAt: params.startedAtIso, + completedAt: new Date().toISOString(), + elapsedMs: Number(params.elapsedMs.toFixed(2)), + outputPath: params.outputPath, + exitCode: params.code, + signal: params.signal, + timedOut: params.timedOut, + args: params.args, + summary: params.summary, + }; + + try { + await fs.mkdir(diagnosticsDirectory, { recursive: true }); + await Promise.allSettled([ + fs.writeFile( + path.join(diagnosticsDirectory, `${filePrefix}.manifest.json`), + `${JSON.stringify(manifest, null, 2)}\n`, + ), + fs.writeFile( + path.join(diagnosticsDirectory, `${filePrefix}.stdout.log`), + params.stdout, + ), + fs.writeFile( + path.join(diagnosticsDirectory, `${filePrefix}.stderr.log`), + params.stderr, + ), + ]); + } catch (error) { + console.warn( + "[native-static-layout-export] Failed to persist Windows GPU diagnostics", + error, + ); + } +} + +export function parseWindowsGpuExportProgressLine( + line: string, +): NativeStaticLayoutExportProgress | null { + const trimmed = line.trim(); + const prefix = "PROGRESS "; + if (!trimmed.startsWith(prefix)) { + return null; + } + + try { + const parsed = JSON.parse(trimmed.slice(prefix.length)) as { + currentFrame?: unknown; + totalFrames?: unknown; + percentage?: unknown; + averageFps?: unknown; + instantFps?: unknown; + intervalMs?: unknown; + intervalFrames?: unknown; + intervalDecodeWallMs?: unknown; + intervalEncodeMs?: unknown; + intervalPipelineWaitMs?: unknown; + intervalCompositeMs?: unknown; + intervalNvencMs?: unknown; + intervalPacketWriteMs?: unknown; + intervalWebcamDecodeMs?: unknown; + intervalWebcamCopyMs?: unknown; + intervalRoiCompositeFrames?: unknown; + intervalMonolithicCompositeFrames?: unknown; + intervalCopyCompositeFrames?: unknown; + stage?: unknown; + }; + const currentFrame = Number(parsed.currentFrame); + const totalFrames = Number(parsed.totalFrames); + const percentage = Number(parsed.percentage); + if ( + !Number.isFinite(currentFrame) || + !Number.isFinite(totalFrames) || + !Number.isFinite(percentage) || + totalFrames <= 0 + ) { + return null; + } + + const progress: NativeStaticLayoutExportProgress = { + currentFrame: Math.max(0, Math.floor(currentFrame)), + totalFrames: Math.max(1, Math.floor(totalFrames)), + percentage: Math.min(100, Math.max(0, percentage)), + }; + if (parsed.stage === "preparing" || parsed.stage === "finalizing") { + progress.stage = parsed.stage; + } + const optionalNumberFields = [ + "averageFps", + "instantFps", + "intervalMs", + "intervalFrames", + "intervalDecodeWallMs", + "intervalEncodeMs", + "intervalPipelineWaitMs", + "intervalCompositeMs", + "intervalNvencMs", + "intervalPacketWriteMs", + "intervalWebcamDecodeMs", + "intervalWebcamCopyMs", + "intervalRoiCompositeFrames", + "intervalMonolithicCompositeFrames", + "intervalCopyCompositeFrames", + ] as const; + for (const field of optionalNumberFields) { + const value = Number(parsed[field]); + if (Number.isFinite(value) && value >= 0) { + progress[field] = value; + } + } + return progress; + } catch { + return null; + } +} + +export function mapNvidiaCudaWrapperProgressPercentage(progress: NativeStaticLayoutExportProgress) { + if (progress.stage === "finalizing") { + return progress.percentage; + } + + if (progress.currentFrame > 0 || progress.percentage === 0) { + return Math.min(98, 3 + progress.percentage * 0.95); + } + + return progress.percentage; +} + +export function hasNativeStaticLayoutProgressAdvanced( + progress: { currentFrame: number; percentage: number; stage?: string }, + previous: { currentFrame: number; percentage: number; stage?: string }, +) { + const currentFrame = Math.max(0, Math.floor(progress.currentFrame)); + const percentage = + typeof progress.percentage === "number" && Number.isFinite(progress.percentage) + ? progress.percentage + : 0; + if (currentFrame > previous.currentFrame) { + return true; + } + if (percentage > previous.percentage + 0.1) { + return true; + } + return progress.stage === "finalizing" && previous.stage !== "finalizing"; +} + +function startNativeStaticLayoutExportPowerGuard() { + try { + const blockerId = powerSaveBlocker.start("prevent-app-suspension"); + return { + started: true, + release: () => { + if (powerSaveBlocker.isStarted(blockerId)) { + powerSaveBlocker.stop(blockerId); + } + }, + }; + } catch (error) { + console.warn("[native-static-layout-export] Failed to start power guard", error); + return { + started: false, + release: () => undefined, + }; + } +} + +function setNativeStaticLayoutExportProcessPriority(pid: number | undefined, label: string) { + if (!pid) { + return false; + } + + try { + os.setPriority(pid, NATIVE_EXPORT_HIGH_PRIORITY); + return true; + } catch (error) { + console.warn(`[native-static-layout-export] Failed to raise ${label} priority`, error); + return false; + } +} + +export const nativeStaticLayoutExportSessions = new Map(); + +export interface NativeExportCapabilities { + platform: NodeJS.Platform; + nvidiaCuda: { + available: boolean; + skipReason: string | null; + hasNvidiaGpu: boolean | null; + hasWrapper: boolean; + explicitEnabled: boolean; + explicitDisabled: boolean; + userOptInRequired: boolean; + }; +} + +export async function probeNativeVideoStreamStats( + ffprobePath: string, + inputPath: string, +): Promise { + const result = await execFileAsync( + ffprobePath, + [ + "-v", + "error", + "-select_streams", + "v:0", + "-count_frames", + "-show_entries", + "stream=duration,nb_frames,nb_read_frames,avg_frame_rate,r_frame_rate", + "-of", + "json", + inputPath, + ], + { timeout: 120_000, maxBuffer: 2 * 1024 * 1024 }, + ); + const stats = parseNativeVideoStreamStatsProbeOutput(result.stdout); + if (!stats) { + throw new Error("Unable to parse native video stream stats from FFprobe output"); + } + + return stats; +} + +async function validateNativeVideoOutputFile( + ffprobePath: string, + videoPath: string, + expected: { + durationSec: number; + targetFrames: number; + }, +) { + const stats = await probeNativeVideoStreamStats(ffprobePath, videoPath); + const issues = validateNativeVideoStreamStats(stats, expected); + if (issues.length > 0) { + throw new Error(`Native video output is invalid: ${issues.join("; ")}`); + } +} + +export function isNativeStaticLayoutH264SourceCodec(codec: string | undefined) { + const normalized = (codec ?? "").trim().toLowerCase(); + return ( + /\bh\.?264\b/.test(normalized) || + normalized.includes("avc1") || + normalized.includes("avc3") || + normalized.includes("mpeg-4 avc") + ); +} + +export function shouldCreateNativeStaticLayoutSourceProxy( + metadata: Pick, + inputPath: string, +) { + const extension = path.extname(inputPath).toLowerCase(); + return ( + !isNativeStaticLayoutH264SourceCodec(metadata.codec) || + !NATIVE_STATIC_LAYOUT_SOURCE_PROXY_CONTAINERS.has(extension) + ); +} + +export function getNativeStaticLayoutSourceProxyBitrate( + options: Pick, + metadata?: Pick, +) { + const frameRate = Math.max(1, Math.round(options.frameRate)); + const width = Math.max(1, Math.round(metadata?.width ?? options.width)); + const height = Math.max(1, Math.round(metadata?.height ?? options.height)); + const pixelRateScale = Math.sqrt( + (width * height * frameRate) / NATIVE_STATIC_LAYOUT_SOURCE_PROXY_REFERENCE_PIXEL_RATE, + ); + const qualityFloor = Math.round( + NATIVE_STATIC_LAYOUT_SOURCE_PROXY_1080P30_BITRATE * pixelRateScale, + ); + return Math.min( + NATIVE_STATIC_LAYOUT_SOURCE_PROXY_MAX_BITRATE, + Math.max(Math.round(options.bitrate * 1.2), qualityFloor), + ); +} + +export function buildNativeStaticLayoutSourceProxyArgs( + options: NativeStaticLayoutExportOptions, + outputPath: string, + metadata: NativeVideoMetadataProbe, +) { + const frameRate = Math.max(1, Math.round(options.frameRate)); + const sourceDurationSec = Math.max( + 0.001, + metadata.streamDuration ?? metadata.duration ?? options.durationSec, + ); + const proxyBitrate = getNativeStaticLayoutSourceProxyBitrate(options, metadata); + return [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + options.inputPath, + "-map", + "0:v:0", + "-an", + "-t", + formatCliNumber(sourceDurationSec), + "-vf", + `fps=fps=${frameRate}:start_time=0,scale=trunc(iw/2)*2:trunc(ih/2)*2,setsar=1,format=yuv420p,setpts=PTS-STARTPTS`, + "-c:v", + "libx264", + "-preset", + "veryfast", + "-tune", + "zerolatency", + "-g", + String(Math.max(1, Math.round(frameRate * 2))), + "-b:v", + String(proxyBitrate), + "-maxrate", + String(Math.round(proxyBitrate * 1.25)), + "-bufsize", + String(proxyBitrate * 2), + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + outputPath, + ]; +} + +export function validateNativeStaticLayoutSourceProxyMetadata( + metadata: NativeVideoMetadataProbe, + expected: { sourceDurationSec: number }, +) { + const issues: string[] = []; + const expectedDurationSec = Math.max(0, expected.sourceDurationSec); + const durationToleranceSec = Math.min(2, Math.max(0.5, expectedDurationSec * 0.03)); + if (!isNativeStaticLayoutH264SourceCodec(metadata.codec)) { + issues.push(`proxy codec is not H.264/AVC: ${metadata.codec || "unknown"}`); + } + if (!Number.isFinite(metadata.duration) || metadata.duration <= 0) { + issues.push("proxy duration is unavailable"); + } else if (metadata.duration + durationToleranceSec < expectedDurationSec) { + issues.push( + `proxy duration ${metadata.duration.toFixed( + 3, + )}s shorter than expected ${expectedDurationSec.toFixed(3)}s`, + ); + } + return issues; +} + +export function getNativeVideoExportMaxQueuedWriteBytes(inputByteSize: number) { + if (inputByteSize === 0) return 8 * 1024 * 1024; + return Math.min(64 * 1024 * 1024, Math.max(16 * 1024 * 1024, inputByteSize * 4)); +} + +async function runFfmpegWithMetrics( + ffmpegPath: string, + args: string[], + timeoutMs: number, + session?: NativeStaticLayoutExportSession, +): Promise<{ + success: boolean; + elapsedMs: number; + stderr: string; + code: number | null; + signal: NodeJS.Signals | null; +}> { + const startedAt = getNowMs(); + return await new Promise((resolve) => { + const child = spawn(ffmpegPath, args, { + stdio: ["ignore", "ignore", "pipe"], + }); + if (session) { + session.currentProcess = child; + if (session.terminating) { + child.kill("SIGKILL"); + } + } + let stderr = ""; + let settled = false; + const timeout = setTimeout(() => { + if (settled) return; + try { + child.kill("SIGKILL"); + } catch { + // ignore + } + }, timeoutMs); + + child.stderr.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); + child.once("error", (error) => { + if (settled) return; + settled = true; + if (session?.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + resolve({ + success: false, + elapsedMs: getNowMs() - startedAt, + stderr: error instanceof Error ? error.message : String(error), + code: null, + signal: null, + }); + }); + child.once("close", (code, signal) => { + if (settled) return; + settled = true; + if (session?.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + resolve({ + success: code === 0, + elapsedMs: getNowMs() - startedAt, + stderr, + code, + signal, + }); + }); + }); +} + +function parseFfmpegProgressClockSeconds(value: string) { + const match = value.match(/^(\d+):(\d+):(\d+(?:\.\d+)?)$/); + if (!match) { + return null; + } + + const hours = Number(match[1]); + const minutes = Number(match[2]); + const seconds = Number(match[3]); + if (![hours, minutes, seconds].every(Number.isFinite)) { + return null; + } + + return hours * 3600 + minutes * 60 + seconds; +} + +export function parseFfmpegProgressLineSeconds(line: string) { + const separatorIndex = line.indexOf("="); + if (separatorIndex <= 0) { + return null; + } + + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim(); + if (key === "out_time_us" || key === "out_time_ms") { + const microseconds = Number(value); + return Number.isFinite(microseconds) && microseconds >= 0 ? microseconds / 1_000_000 : null; + } + if (key === "out_time") { + return parseFfmpegProgressClockSeconds(value); + } + + return null; +} + +function createFfmpegMuxProgressHandler( + totalSec: number | undefined, + onProgress?: (progress: NativeVideoAudioMuxProgress) => void, +) { + if (!onProgress || !Number.isFinite(totalSec) || !totalSec || totalSec <= 0) { + return () => undefined; + } + + let lineBuffer = ""; + let lastRatio = 0; + return (chunk: Buffer | string) => { + lineBuffer += chunk.toString(); + const lines = lineBuffer.split(/\r?\n/); + lineBuffer = lines.pop() ?? ""; + for (const line of lines) { + const processedSec = parseFfmpegProgressLineSeconds(line); + if (processedSec === null) { + continue; + } + const ratio = Math.max(0, Math.min(1, processedSec / totalSec)); + if (ratio < lastRatio + 0.0025 && ratio < 1) { + continue; + } + lastRatio = Math.max(lastRatio, ratio); + onProgress({ + ratio: lastRatio, + processedSec, + totalSec, + }); + } + }; +} + +async function runFfmpegAudioMux( + ffmpegPath: string, + args: string[], + timeoutMs: number, + options: NativeVideoExportFinishOptions, + onProgress?: (progress: NativeVideoAudioMuxProgress) => void, + session?: NativeStaticLayoutExportSession, +) { + if (!onProgress && !session) { + await execFileAsync(ffmpegPath, args, { + timeout: timeoutMs, + maxBuffer: 20 * 1024 * 1024, + }); + return; + } + + const handleProgressChunk = createFfmpegMuxProgressHandler( + options.outputDurationSec, + onProgress, + ); + await new Promise((resolve, reject) => { + const child = spawn(ffmpegPath, args, { + stdio: ["ignore", "ignore", "pipe"], + }); + if (session) { + session.currentProcess = child; + if (session.terminating) { + child.kill("SIGKILL"); + } + } + + let stderr = ""; + let settled = false; + const timeout = setTimeout(() => { + if (settled) return; + child.kill("SIGKILL"); + }, timeoutMs); + + child.stderr.on("data", (chunk: Buffer) => { + const text = chunk.toString(); + stderr += text; + handleProgressChunk(text); + }); + child.once("error", (error) => { + if (settled) return; + settled = true; + if (session?.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + reject(error); + }); + child.once("close", (code, signal) => { + if (settled) return; + settled = true; + if (session?.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + if (session?.terminating) { + reject(new Error("Native static layout export was cancelled")); + return; + } + if (code !== 0) { + const suffix = signal ? ` (signal ${signal})` : ""; + reject( + new Error( + `FFmpeg audio mux exited with code ${code ?? "unknown"}${suffix}` + + (stderr.trim() ? `\nSTDERR:\n${stderr.trim()}` : ""), + ), + ); + return; + } + if (onProgress) { + const completeProgress: NativeVideoAudioMuxProgress = { ratio: 1 }; + if ( + typeof options.outputDurationSec === "number" && + Number.isFinite(options.outputDurationSec) + ) { + completeProgress.processedSec = options.outputDurationSec; + completeProgress.totalSec = options.outputDurationSec; + } + onProgress(completeProgress); + } + resolve(); + }); + }); +} + +export function isHardwareAcceleratedVideoEncoder(encoderName: string) { + return /(videotoolbox|nvenc|qsv|amf|mf)/i.test(encoderName); +} + +export async function removeTemporaryExportFile(filePath: string | null | undefined) { + if (!filePath) { + return; + } + + try { + await fs.rm(filePath, { force: true }); + } catch { + // Ignore cleanup failures for temp export artifacts. + } +} + +export function getNativeVideoExportSessionError( + session: NativeVideoExportSession, + fallback: string, +) { + return ( + session.stdinError?.message || + session.processError?.message || + session.stderrOutput.trim() || + fallback + ); +} + +export function sendNativeVideoExportWriteFrameResult( + sender: WebContents | null | undefined, + sessionId: string, + requestId: number, + result: { success: boolean; error?: string }, +) { + if (!sender || sender.isDestroyed()) { + return; + } + + sender.send("native-video-export-write-frame-result", { + sessionId, + requestId, + ...result, + }); +} + +export function settleNativeVideoExportWriteFrameRequest( + sessionId: string, + session: NativeVideoExportSession, + requestId: number, + result: { success: boolean; error?: string }, +) { + session.pendingWriteRequestIds.delete(requestId); + sendNativeVideoExportWriteFrameResult(session.sender, sessionId, requestId, result); +} + +export function flushNativeVideoExportPendingWriteRequests( + sessionId: string, + session: NativeVideoExportSession, + error: string, +) { + for (const requestId of session.pendingWriteRequestIds) { + sendNativeVideoExportWriteFrameResult(session.sender, sessionId, requestId, { + success: false, + error, + }); + } + + session.pendingWriteRequestIds.clear(); +} + +export function isIgnorableNativeVideoExportStreamError(error: Error | null | undefined): boolean { + if (!error) { + return false; + } + + const errno = error as NodeJS.ErrnoException; + return ( + errno.code === "EPIPE" || + errno.code === "ERR_STREAM_DESTROYED" || + /broken pipe|stream destroyed|eof/i.test(error.message) + ); +} + +export async function waitForNativeVideoExportDrain(session: NativeVideoExportSession) { + if ( + session.stdinError || + session.processError || + session.ffmpegProcess.stdin.destroyed || + session.ffmpegProcess.stdin.writableEnded || + !session.ffmpegProcess.stdin.writable || + session.ffmpegProcess.stdin.writableLength <= 0 + ) { + return; + } + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject( + new Error("Timed out while waiting for native export writer backpressure to clear"), + ); + }, 15000); + + const cleanup = () => { + clearTimeout(timeout); + session.ffmpegProcess.stdin.off("drain", handleDrain); + session.ffmpegProcess.stdin.off("error", handleError); + session.ffmpegProcess.off("close", handleClose); + }; + + const handleDrain = () => { + cleanup(); + resolve(); + }; + + const handleError = (error: Error) => { + cleanup(); + reject(error); + }; + + const handleClose = () => { + cleanup(); + reject( + new Error( + getNativeVideoExportSessionError( + session, + "Native video export writer closed before draining", + ), + ), + ); + }; + + session.ffmpegProcess.stdin.once("drain", handleDrain); + session.ffmpegProcess.stdin.once("error", handleError); + session.ffmpegProcess.once("close", handleClose); + }); +} + +export function getNativeVideoExportFrameLength(frameData: Uint8Array | ArrayBuffer) { + return frameData.byteLength; +} + +export async function writeNativeVideoExportFrame( + session: NativeVideoExportSession, + frameData: Uint8Array | ArrayBuffer, +) { + if ( + session.inputMode !== "h264-stream" && + getNativeVideoExportFrameLength(frameData) !== session.inputByteSize + ) { + throw new Error( + `Native video export expected ${session.inputByteSize} bytes per frame but received ${getNativeVideoExportFrameLength(frameData)}`, + ); + } + + if ( + session.stdinError || + session.processError || + session.ffmpegProcess.stdin.destroyed || + session.ffmpegProcess.stdin.writableEnded || + !session.ffmpegProcess.stdin.writable + ) { + throw new Error( + getNativeVideoExportSessionError( + session, + "Native video export encoder is not accepting frames", + ), + ); + } + + const frameBuffer = + frameData instanceof ArrayBuffer + ? Buffer.from(frameData) + : Buffer.from(frameData.buffer, frameData.byteOffset, frameData.byteLength); + + try { + session.ffmpegProcess.stdin.write(frameBuffer); + } catch (error) { + session.stdinError = error instanceof Error ? error : new Error(String(error)); + throw session.stdinError; + } + + if (session.ffmpegProcess.stdin.writableLength >= session.maxQueuedWriteBytes) { + try { + await waitForNativeVideoExportDrain(session); + } catch (error) { + session.stdinError = error instanceof Error ? error : new Error(String(error)); + throw session.stdinError; + } + } +} + +function toConcatFileLine(filePath: string) { + const normalized = filePath.replace(/\\/g, "/").replace(/'/g, "'\\''"); + return `file '${normalized}'`; +} + +function getStaticLayoutChunkOutputPath(directory: string, index: number) { + return path.join(directory, `chunk-${String(index).padStart(4, "0")}.mp4`); +} + +function shouldUsePrecompositedStaticLayout(options: NativeStaticLayoutExportOptions) { + return Boolean( + options.backgroundImagePath || + (options.borderRadius ?? 0) > 0.5 || + (options.shadowIntensity ?? 0) > 0, + ); +} + +export async function normalizeNativeStaticLayoutBackground( + options: NativeStaticLayoutExportOptions, +): Promise { + if (!options.backgroundImagePath || (await pathExists(options.backgroundImagePath))) { + return options; + } + + console.warn( + "[native-static-layout-export] Background image is missing; using solid fallback", + { + backgroundImagePath: options.backgroundImagePath, + }, + ); + return { + ...options, + backgroundColor: MISSING_NATIVE_STATIC_BACKGROUND_COLOR, + backgroundImagePath: null, + }; +} + +function getFfmpegFailureMessage(result: Awaited>) { + const suffix = result.signal ? ` (signal ${result.signal})` : ""; + const status = result.code === null ? "unknown" : String(result.code); + return result.stderr.trim() || `FFmpeg exited with code ${status}${suffix}`; +} + +function clampUnit(value: number) { + if (!Number.isFinite(value)) { + return 0; + } + + return Math.min(1, Math.max(0, value)); +} + +function formatCliNumber(value: number) { + return Number.isInteger(value) + ? String(value) + : value.toFixed(6).replace(/0+$/, "").replace(/\.$/, ""); +} + +function hasNativeStaticLayoutTimeline(options: NativeStaticLayoutExportOptions) { + return (options.timelineSegments?.length ?? 0) > 0; +} + +function hasNativeStaticLayoutSourceCrop(options: NativeStaticLayoutExportOptions) { + return Boolean( + Number.isFinite(options.sourceCropWidth) && + Number.isFinite(options.sourceCropHeight) && + (options.sourceCropWidth ?? 0) >= 2 && + (options.sourceCropHeight ?? 0) >= 2, + ); +} + +export function buildNativeStaticLayoutTimelineSegments( + segments: NativeVideoExportEditedTrackSegment[], +): NativeStaticLayoutTimelineSegment[] { + const timelineSegments: NativeStaticLayoutTimelineSegment[] = []; + let outputCursorMs = 0; + + for (const segment of segments) { + const sourceStartMs = Math.max(0, segment.startMs); + const sourceEndMs = Math.max(sourceStartMs, segment.endMs); + const speed = segment.speed; + if ( + !Number.isFinite(sourceStartMs) || + !Number.isFinite(sourceEndMs) || + !Number.isFinite(speed) || + sourceEndMs - sourceStartMs <= 0.5 || + speed <= 0 + ) { + return []; + } + + const outputDurationMs = (sourceEndMs - sourceStartMs) / speed; + if (!Number.isFinite(outputDurationMs) || outputDurationMs <= 0.5) { + return []; + } + + const outputStartMs = outputCursorMs; + const outputEndMs = outputStartMs + outputDurationMs; + timelineSegments.push({ + sourceStartMs, + sourceEndMs, + outputStartMs, + outputEndMs, + speed, + }); + outputCursorMs = outputEndMs; + } + + return timelineSegments; +} + +function validateNativeStaticLayoutTimelineSegments( + segments: NativeStaticLayoutTimelineSegment[] | undefined, + durationSec: number, +) { + if (!segments?.length) { + return []; + } + + const normalized: NativeStaticLayoutTimelineSegment[] = []; + let expectedOutputStartMs = 0; + for (const segment of segments) { + const sourceStartMs = Math.max(0, segment.sourceStartMs); + const sourceEndMs = Math.max(sourceStartMs, segment.sourceEndMs); + const outputStartMs = Math.max(0, segment.outputStartMs); + const outputEndMs = Math.max(outputStartMs, segment.outputEndMs); + const speed = segment.speed; + if ( + !Number.isFinite(sourceStartMs) || + !Number.isFinite(sourceEndMs) || + !Number.isFinite(outputStartMs) || + !Number.isFinite(outputEndMs) || + !Number.isFinite(speed) || + sourceEndMs - sourceStartMs <= 0.5 || + outputEndMs - outputStartMs <= 0.5 || + speed <= 0 + ) { + throw new Error("Native timeline map contains an invalid segment"); + } + + if (Math.abs(outputStartMs - expectedOutputStartMs) > 2) { + throw new Error("Native timeline map output ranges must be contiguous"); + } + + const expectedOutputDurationMs = (sourceEndMs - sourceStartMs) / speed; + if (Math.abs(expectedOutputDurationMs - (outputEndMs - outputStartMs)) > 2) { + throw new Error("Native timeline map segment speed does not match its output duration"); + } + + normalized.push({ + sourceStartMs, + sourceEndMs, + outputStartMs, + outputEndMs, + speed, + }); + expectedOutputStartMs = outputEndMs; + } + + if (Math.abs(expectedOutputStartMs / 1000 - durationSec) > 0.05) { + throw new Error("Native timeline map duration does not match the export duration"); + } + + return normalized; +} + +async function prepareNativeStaticLayoutTimelineMap( + options: NativeStaticLayoutExportOptions, + outputPath: string, +) { + const segments = validateNativeStaticLayoutTimelineSegments( + options.timelineSegments, + options.durationSec, + ); + if (segments.length === 0) { + return null; + } + + const lines = segments.map((segment) => + [ + segment.sourceStartMs, + segment.sourceEndMs, + segment.outputStartMs, + segment.outputEndMs, + segment.speed, + ] + .map(formatCliNumber) + .join(","), + ); + await fs.writeFile(outputPath, `${lines.join("\n")}\n`, "utf8"); + return outputPath; +} + +async function pathExists(filePath: string) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +function isNvidiaVendorId(value: unknown) { + if (typeof value === "number") { + return value === NVIDIA_PCI_VENDOR_ID; + } + if (typeof value !== "string") { + return false; + } + + const normalized = value.trim().toLowerCase(); + if (normalized.length === 0) { + return false; + } + const parsed = normalized.startsWith("0x") + ? Number.parseInt(normalized.slice(2), 16) + : Number.parseInt(normalized, 10); + + return Number.isFinite(parsed) && parsed === NVIDIA_PCI_VENDOR_ID; +} + +function includesNvidiaGpuName(value: unknown) { + return typeof value === "string" && /\bnvidia\b/i.test(value); +} + +function isNvidiaGpuDevice(device: unknown) { + if (!device || typeof device !== "object") { + return false; + } + + const gpuDevice = device as ElectronGpuDeviceLike; + return ( + isNvidiaVendorId(gpuDevice.vendorId) || + includesNvidiaGpuName(gpuDevice.vendorString) || + includesNvidiaGpuName(gpuDevice.deviceString) + ); +} + +export function hasNvidiaGpuDeviceInGpuInfo(gpuInfo: unknown) { + if (!gpuInfo || typeof gpuInfo !== "object") { + return false; + } + + const devices = (gpuInfo as ElectronGpuInfoLike).gpuDevice; + return Array.isArray(devices) && devices.some(isNvidiaGpuDevice); +} + +function getGpuVendorLabel(device: ElectronGpuDeviceLike): string | null { + if (device.vendorString?.trim()) { + return device.vendorString.trim(); + } + + const rawVendorId = device.vendorId; + const vendorId = + typeof rawVendorId === "number" + ? rawVendorId + : typeof rawVendorId === "string" + ? rawVendorId.toLowerCase().startsWith("0x") + ? Number.parseInt(rawVendorId.slice(2), 16) + : Number.parseInt(rawVendorId, 10) + : Number.NaN; + return ( + { + [0x1002]: "AMD", + [0x106b]: "Apple", + [0x10de]: "NVIDIA", + [0x8086]: "Intel", + }[vendorId] ?? null + ); +} + +/** Reduces Electron's GPU response to support-safe hardware fields. */ +export function sanitizeExportGpuInfo( + gpuInfo: unknown, +): Pick { + if (!gpuInfo || typeof gpuInfo !== "object") { + return { machineModel: null, gpus: [] }; + } + + const info = gpuInfo as ElectronGpuInfoLike; + const machineModel = + [info.machineModelName, info.machineModelVersion] + .filter((value): value is string => Boolean(value?.trim())) + .join(" ") || null; + const gpus = Array.isArray(info.gpuDevice) + ? info.gpuDevice.map((device) => { + const vendor = getGpuVendorLabel(device); + return { + name: device.deviceString?.trim() || vendor || "Unknown GPU", + vendor, + active: typeof device.active === "boolean" ? device.active : null, + }; + }) + : []; + + return { machineModel, gpus }; +} + +/** Captures sanitized hardware and GPU acceleration details for export support reports. */ +export async function getExportHardwareInfo(): Promise { + let sanitizedGpuInfo: Pick = { + machineModel: null, + gpus: [], + }; + try { + sanitizedGpuInfo = sanitizeExportGpuInfo(await app.getGPUInfo("complete")); + } catch { + // Hardware diagnostics are best effort and must not affect exporting. + } + + let gpuFeatureStatus: Record = {}; + try { + gpuFeatureStatus = app.getGPUFeatureStatus() as unknown as Record; + } catch { + // GPU feature status can be unavailable before Chromium finishes GPU initialization. + } + + const cpuModel = os.cpus()[0]?.model?.replace(/\s+/g, " ").trim() || null; + return { + platform: process.platform, + release: os.release(), + arch: process.arch, + cpuModel, + logicalProcessors: os.cpus().length, + totalMemoryGb: Math.round((os.totalmem() / 1024 ** 3) * 10) / 10, + machineModel: sanitizedGpuInfo.machineModel, + gpus: sanitizedGpuInfo.gpus, + gpuFeatures: { + videoDecode: gpuFeatureStatus.video_decode ?? null, + videoEncode: gpuFeatureStatus.video_encode ?? null, + webgl: gpuFeatureStatus.webgl ?? null, + webgpu: gpuFeatureStatus.webgpu ?? null, + }, + }; +} + +async function hasNvidiaGpuForCudaExportCandidate() { + const hasNvidiaGpu = await probeNvidiaGpuForCudaExportCandidate(); + return hasNvidiaGpu ?? true; +} + +async function probeNvidiaGpuForCudaExportCandidate(): Promise { + const getGPUInfo = ( + app as typeof app & { + getGPUInfo?: (infoType: "basic") => Promise; + } + ).getGPUInfo; + if (typeof getGPUInfo !== "function") { + return null; + } + + try { + return hasNvidiaGpuDeviceInGpuInfo(await getGPUInfo.call(app, "basic")); + } catch (error) { + console.warn( + "[native-static-layout-export] Unable to inspect GPU info before NVIDIA CUDA export; letting the helper decide", + error, + ); + return null; + } +} + +function getNativeBinPlatformArch() { + return process.arch === "arm64" ? "win32-arm64" : "win32-x64"; +} + +async function resolveExperimentalWindowsGpuExporterPath() { + if (process.platform !== "win32") { + return null; + } + + const executableNames = ["recordly-gpu-export.exe", "gpu-export-probe.exe"]; + const candidates: string[] = []; + const configuredPath = process.env.RECORDLY_WINDOWS_GPU_EXPORT_EXE; + if (configuredPath) { + candidates.push(configuredPath); + } + + const nativeBinDir = path.join("electron", "native", "bin", getNativeBinPlatformArch()); + const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath; + if (resourcesPath) { + for (const executableName of executableNames) { + candidates.push( + path.join(resourcesPath, "app.asar.unpacked", nativeBinDir, executableName), + path.join(resourcesPath, nativeBinDir, executableName), + ); + } + } + + for (const executableName of executableNames) { + candidates.push( + path.join(process.cwd(), nativeBinDir, executableName), + path.join( + app.getAppPath().replace(/app\.asar$/, "app.asar.unpacked"), + nativeBinDir, + executableName, + ), + path.join(process.cwd(), ".tmp", "gpu-export-probe-build", "Release", executableName), + path.join( + process.cwd(), + "electron", + "native", + "gpu-export-probe", + "build", + "Release", + executableName, + ), + ); + } + + for (const candidate of candidates) { + if (await pathExists(candidate)) { + return candidate; + } + } + + return null; +} + +export function getNvidiaCudaAudioExportSkipReason( + audioMode: NativeVideoExportAudioMode | undefined, + options: { allowValidatedFallbackCandidate?: boolean } = {}, +) { + const resolvedAudioMode = audioMode ?? "none"; + if (resolvedAudioMode === "none") { + return null; + } + if (process.env[NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV] === "1") { + return null; + } + if (options.allowValidatedFallbackCandidate) { + return null; + } + + return `audio-mode:${resolvedAudioMode}`; +} + +function isExplicitNvidiaCudaExportEnabled() { + return process.env[NVIDIA_CUDA_EXPORT_ENV] === "1"; +} + +function isExplicitNvidiaCudaExportDisabled() { + return process.env[NVIDIA_CUDA_EXPORT_ENV] === "0"; +} + +function isUserOptedInNvidiaCudaExport(options: NativeStaticLayoutExportOptions) { + return options.experimentalNvidiaCudaExport === true && !isExplicitNvidiaCudaExportDisabled(); +} + +function isValidatedNvidiaCudaFallbackCandidate(options: NativeStaticLayoutExportOptions) { + return isUserOptedInNvidiaCudaExport(options) && !isExplicitNvidiaCudaExportEnabled(); +} + +function isNvidiaCudaForceVideoOnlyEnabled() { + return process.env[NVIDIA_CUDA_FORCE_VIDEO_ONLY_ENV] === "1"; +} + +export function getNvidiaCudaAutoStallTimeoutMs(autoCandidateActive = false) { + if (!autoCandidateActive && !isExplicitNvidiaCudaExportEnabled()) { + return null; + } + + const rawValue = process.env[NVIDIA_CUDA_AUTO_STALL_TIMEOUT_ENV]?.trim(); + if (rawValue === "0" || rawValue?.toLowerCase() === "off") { + return null; + } + + const parsed = Number(rawValue); + if (Number.isFinite(parsed) && parsed > 0) { + return Math.max(10_000, Math.round(parsed)); + } + + return DEFAULT_NVIDIA_CUDA_AUTO_STALL_TIMEOUT_MS; +} + +export function getNativeGpuCompositorStallTimeoutMs() { + const rawValue = process.env[NATIVE_GPU_STALL_TIMEOUT_ENV]?.trim(); + if (rawValue === "0" || rawValue?.toLowerCase() === "off") { + return null; + } + + const parsed = Number(rawValue); + if (Number.isFinite(parsed) && parsed > 0) { + return Math.max(10_000, Math.round(parsed)); + } + + return DEFAULT_NATIVE_GPU_STALL_TIMEOUT_MS; +} + +export async function getExperimentalNvidiaCudaExportSkipReason( + options: NativeStaticLayoutExportOptions, +) { + if (process.platform !== "win32") { + return "not-windows"; + } + if (isExplicitNvidiaCudaExportDisabled()) { + return "env-disabled"; + } + const explicitCuda = isExplicitNvidiaCudaExportEnabled(); + const userOptIn = isUserOptedInNvidiaCudaExport(options); + if (!explicitCuda && !userOptIn) { + return "env-disabled"; + } + if (!options.experimentalWindowsGpuCompositor) { + return "windows-gpu-compositor-disabled"; + } + + if (userOptIn) { + if (!(await resolveExperimentalNvidiaCudaExportScriptPath())) { + return "cuda-wrapper-unavailable"; + } + if (!(await hasNvidiaGpuForCudaExportCandidate())) { + return "nvidia-gpu-unavailable"; + } + } + + return getNvidiaCudaAudioExportSkipReason(options.audioOptions?.audioMode, { + allowValidatedFallbackCandidate: + isValidatedNvidiaCudaFallbackCandidate(options) || isNvidiaCudaForceVideoOnlyEnabled(), + }); +} + +export async function getNativeExportCapabilities(): Promise { + if (process.platform !== "win32") { + return { + platform: process.platform, + nvidiaCuda: { + available: false, + skipReason: "not-windows", + hasNvidiaGpu: null, + hasWrapper: false, + explicitEnabled: isExplicitNvidiaCudaExportEnabled(), + explicitDisabled: isExplicitNvidiaCudaExportDisabled(), + userOptInRequired: true, + }, + }; + } + + const explicitEnabled = isExplicitNvidiaCudaExportEnabled(); + const explicitDisabled = isExplicitNvidiaCudaExportDisabled(); + const wrapperPath = await resolveExperimentalNvidiaCudaExportScriptPath(); + const hasNvidiaGpu = await probeNvidiaGpuForCudaExportCandidate(); + const skipReason = explicitDisabled + ? "env-disabled" + : !wrapperPath + ? "cuda-wrapper-unavailable" + : hasNvidiaGpu === false + ? "nvidia-gpu-unavailable" + : hasNvidiaGpu === null + ? "nvidia-gpu-probe-unavailable" + : null; + + return { + platform: process.platform, + nvidiaCuda: { + available: skipReason === null, + skipReason, + hasNvidiaGpu, + hasWrapper: Boolean(wrapperPath), + explicitEnabled, + explicitDisabled, + userOptInRequired: !explicitEnabled, + }, + }; +} + +export async function resolveExperimentalNvidiaCudaExportScriptPath() { + if (process.platform !== "win32") { + return null; + } + + const candidates: string[] = []; + const configuredPath = process.env.RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT; + if (configuredPath) { + candidates.push(configuredPath); + } + const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath; + if (resourcesPath) { + candidates.push( + path.join( + resourcesPath, + "app.asar.unpacked", + "electron", + "native", + "nvidia-cuda-compositor", + "run-mp4-pipeline.mjs", + ), + ); + } + candidates.push( + path.join( + process.cwd(), + "electron", + "native", + "nvidia-cuda-compositor", + "run-mp4-pipeline.mjs", + ), + path.join( + app.getAppPath().replace(/app\.asar$/, "app.asar.unpacked"), + "electron", + "native", + "nvidia-cuda-compositor", + "run-mp4-pipeline.mjs", + ), + path.join( + app.getAppPath(), + "electron", + "native", + "nvidia-cuda-compositor", + "run-mp4-pipeline.mjs", + ), + path.join(process.cwd(), ".tmp", "nvdec-nvenc-probe", "run-mp4-pipeline.mjs"), + path.join(app.getAppPath(), ".tmp", "nvdec-nvenc-probe", "run-mp4-pipeline.mjs"), + ); + + for (const candidate of candidates) { + if (await pathExists(candidate)) { + return candidate; + } + } + + return null; +} + +function resolveExperimentalNvidiaCudaNodeCommand() { + const configuredNodePath = process.env.RECORDLY_NVIDIA_CUDA_NODE_EXE; + if (configuredNodePath) { + return { + command: configuredNodePath, + env: {}, + }; + } + + return { + command: process.execPath, + env: { + ELECTRON_RUN_AS_NODE: "1", + }, + }; +} + +function convertHexColorToNv12(color: string) { + const hex = color.trim().match(/^#?([0-9a-f]{6})$/i)?.[1] ?? "101010"; + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.slice(2, 4), 16); + const b = Number.parseInt(hex.slice(4, 6), 16); + const clampByte = (value: number) => Math.max(0, Math.min(255, Math.round(value))); + + return { + y: clampByte(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16), + u: clampByte(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128), + v: clampByte(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128), + }; +} + +function getNvidiaCudaBitrateMbps(options: NativeStaticLayoutExportOptions) { + return Math.max(1, Math.round(options.bitrate / 1_000_000)); +} + +export function buildExperimentalWindowsGpuStaticLayoutArgs( + options: NativeStaticLayoutExportOptions, + outputPath: string, +) { + const shadowPixels = Math.round(clampUnit(options.shadowIntensity ?? 0) * 64); + const backgroundBlurPx = Math.max(0, options.backgroundBlurPx ?? 0); + const pixelCount = options.width * options.height; + const surfacePoolSize = pixelCount <= 1920 * 1080 ? 12 : 8; + const args = [ + "--input", + options.inputPath, + "--output", + outputPath, + "--width", + String(options.width), + "--height", + String(options.height), + "--fps", + String(options.frameRate), + "--seconds", + formatCliNumber(options.durationSec), + "--bitrate", + String(options.bitrate), + "--shader-composite", + "--radius", + formatCliNumber(Math.max(0, options.borderRadius ?? 0)), + "--shadow", + formatCliNumber(shadowPixels), + "--content-left", + String(Math.round(options.offsetX)), + "--content-top", + String(Math.round(options.offsetY)), + "--content-width", + String(Math.round(options.contentWidth)), + "--content-height", + String(Math.round(options.contentHeight)), + "--background-color", + options.backgroundColor, + "--surface-pool-size", + String(surfacePoolSize), + ]; + + if (options.backgroundImagePath) { + args.push("--background-image", options.backgroundImagePath); + } + if ( + Number.isFinite(options.sourceCropX) && + Number.isFinite(options.sourceCropY) && + Number.isFinite(options.sourceCropWidth) && + Number.isFinite(options.sourceCropHeight) && + (options.sourceCropWidth ?? 0) >= 2 && + (options.sourceCropHeight ?? 0) >= 2 + ) { + args.push( + "--source-crop-x", + String(Math.round(options.sourceCropX ?? 0)), + "--source-crop-y", + String(Math.round(options.sourceCropY ?? 0)), + "--source-crop-width", + String(Math.round(options.sourceCropWidth ?? 0)), + "--source-crop-height", + String(Math.round(options.sourceCropHeight ?? 0)), + ); + } + if (backgroundBlurPx > 0) { + args.push("--background-blur", formatCliNumber(backgroundBlurPx)); + } + if (options.webcamInputPath) { + const webcamShadowPixels = Math.round(clampUnit(options.webcamShadowIntensity ?? 0) * 64); + args.push( + "--webcam-input", + options.webcamInputPath, + "--webcam-left", + String(Math.round(options.webcamLeft ?? 0)), + "--webcam-top", + String(Math.round(options.webcamTop ?? 0)), + "--webcam-size", + String(Math.round(options.webcamSize ?? 0)), + "--webcam-radius", + formatCliNumber(Math.max(0, options.webcamRadius ?? 0)), + "--webcam-shadow", + formatCliNumber(webcamShadowPixels), + "--webcam-time-offset-ms", + formatCliNumber(options.webcamTimeOffsetMs ?? 0), + ); + if (options.webcamMirror !== false) { + args.push("--webcam-mirror"); + } + } + if (options.cursorTelemetryPath) { + args.push( + "--cursor-telemetry", + options.cursorTelemetryPath, + "--cursor-size", + formatCliNumber(Math.max(1, options.cursorSize ?? 84)), + ); + if (options.cursorAtlasPath && options.cursorAtlasMetadataPath) { + args.push( + "--cursor-atlas", + options.cursorAtlasPath, + "--cursor-atlas-metadata", + options.cursorAtlasMetadataPath, + ); + } + } + if (options.zoomTelemetryPath) { + args.push("--zoom-telemetry", options.zoomTelemetryPath); + } + if (options.timelineMapPath) { + args.push("--timeline-map", options.timelineMapPath); + } + + return args; +} + +async function prepareWindowsGpuCursorTelemetry( + options: NativeStaticLayoutExportOptions, + outputPath: string, +) { + const telemetry = options.cursorTelemetry; + if (!telemetry || telemetry.length === 0) { + return null; + } + + const lines = telemetry + .filter((sample) => { + return ( + Number.isFinite(sample.timeMs) && + Number.isFinite(sample.cx) && + Number.isFinite(sample.cy) + ); + }) + .map((sample) => { + const timeMs = Math.max(0, sample.timeMs); + const cx = Math.min(1, Math.max(0, sample.cx)); + const cy = Math.min(1, Math.max(0, sample.cy)); + const cursorTypeIndex = Math.max( + 0, + Math.min(8, Math.round(sample.cursorTypeIndex ?? 0)), + ); + const bounceScale = Math.min(2, Math.max(0.1, sample.bounceScale ?? 1)); + const visible = sample.visible !== false ? 1 : 0; + return [ + formatCliNumber(timeMs), + formatCliNumber(cx), + formatCliNumber(cy), + String(cursorTypeIndex), + formatCliNumber(bounceScale), + String(visible), + ].join(","); + }); + + if (lines.length === 0) { + return null; + } + + await fs.writeFile(outputPath, lines.join("\n"), "utf8"); + return outputPath; +} + +async function prepareWindowsGpuCursorAtlas( + options: NativeStaticLayoutExportOptions, + atlasPath: string, + metadataPath: string, +) { + const dataUrl = options.cursorAtlasPngDataUrl; + const entries = options.cursorAtlasEntries; + if (!dataUrl || !entries?.length) { + return null; + } + + const match = /^data:image\/png;base64,([A-Za-z0-9+/=]+)$/.exec(dataUrl); + if (!match) { + return null; + } + + const metadataLines = entries + .filter((entry) => { + return ( + Number.isInteger(entry.index) && + Number.isFinite(entry.x) && + Number.isFinite(entry.y) && + Number.isFinite(entry.width) && + Number.isFinite(entry.height) && + Number.isFinite(entry.anchorX) && + Number.isFinite(entry.anchorY) && + Number.isFinite(entry.aspectRatio) && + entry.width > 0 && + entry.height > 0 + ); + }) + .map((entry) => + [ + String(Math.max(0, Math.min(8, entry.index))), + formatCliNumber(Math.max(0, entry.x)), + formatCliNumber(Math.max(0, entry.y)), + formatCliNumber(Math.max(1, entry.width)), + formatCliNumber(Math.max(1, entry.height)), + formatCliNumber(Math.min(1, Math.max(0, entry.anchorX))), + formatCliNumber(Math.min(1, Math.max(0, entry.anchorY))), + formatCliNumber(Math.max(0.01, entry.aspectRatio)), + ].join(","), + ); + if (metadataLines.length === 0) { + return null; + } + + await fs.writeFile(atlasPath, Buffer.from(match[1], "base64")); + await fs.writeFile(metadataPath, metadataLines.join("\n"), "utf8"); + return { atlasPath, metadataPath }; +} + +async function prepareNvidiaCudaCursorTelemetry( + options: NativeStaticLayoutExportOptions, + outputPath: string, +) { + const telemetry = options.cursorTelemetry; + if (!telemetry || telemetry.length === 0) { + return null; + } + + const samples = telemetry + .filter((sample) => { + return ( + Number.isFinite(sample.timeMs) && + Number.isFinite(sample.cx) && + Number.isFinite(sample.cy) + ); + }) + .map((sample) => ({ + timeMs: Math.max(0, sample.timeMs), + cx: Math.min(1, Math.max(0, sample.cx)), + cy: Math.min(1, Math.max(0, sample.cy)), + cursorTypeIndex: Math.max(0, Math.min(8, Math.round(sample.cursorTypeIndex ?? 0))), + bounceScale: Math.min(2, Math.max(0.1, sample.bounceScale ?? 1)), + visible: sample.visible !== false, + })); + if (samples.length === 0) { + return null; + } + + await fs.writeFile(outputPath, JSON.stringify({ samples }), "utf8"); + return outputPath; +} + +async function prepareNvidiaCudaCursorAtlas( + options: NativeStaticLayoutExportOptions, + atlasPath: string, + metadataPath: string, +) { + const dataUrl = options.cursorAtlasPngDataUrl; + const entries = options.cursorAtlasEntries; + if (!dataUrl || !entries?.length) { + return null; + } + + const match = /^data:image\/png;base64,([A-Za-z0-9+/=]+)$/.exec(dataUrl); + if (!match) { + return null; + } + + const metadataLines = entries + .filter((entry) => { + return ( + Number.isInteger(entry.index) && + Number.isFinite(entry.x) && + Number.isFinite(entry.y) && + Number.isFinite(entry.width) && + Number.isFinite(entry.height) && + Number.isFinite(entry.anchorX) && + Number.isFinite(entry.anchorY) && + Number.isFinite(entry.aspectRatio) && + entry.width > 0 && + entry.height > 0 + ); + }) + .map((entry) => + [ + String(Math.max(0, Math.min(8, entry.index))), + formatCliNumber(Math.max(0, entry.x)), + formatCliNumber(Math.max(0, entry.y)), + formatCliNumber(Math.max(1, entry.width)), + formatCliNumber(Math.max(1, entry.height)), + formatCliNumber(Math.min(1, Math.max(0, entry.anchorX))), + formatCliNumber(Math.min(1, Math.max(0, entry.anchorY))), + formatCliNumber(Math.max(0.01, entry.aspectRatio)), + ].join("\t"), + ); + if (metadataLines.length === 0) { + return null; + } + + await fs.writeFile(atlasPath, Buffer.from(match[1], "base64")); + await fs.writeFile(metadataPath, `${metadataLines.join("\n")}\n`, "utf8"); + return { atlasPath, metadataPath }; +} + +async function prepareWindowsGpuZoomTelemetry( + options: NativeStaticLayoutExportOptions, + outputPath: string, +) { + const telemetry = options.zoomTelemetry; + if (!telemetry || telemetry.length === 0) { + return null; + } + + const lines = telemetry + .filter((sample) => { + return ( + Number.isFinite(sample.timeMs) && + Number.isFinite(sample.scale) && + Number.isFinite(sample.x) && + Number.isFinite(sample.y) + ); + }) + .map((sample) => { + const timeMs = Math.max(0, sample.timeMs); + const scale = Math.max(0.01, sample.scale); + return [ + formatCliNumber(timeMs), + formatCliNumber(scale), + formatCliNumber(sample.x), + formatCliNumber(sample.y), + ].join(","); + }); + + if (lines.length === 0) { + return null; + } + + await fs.writeFile(outputPath, lines.join("\n"), "utf8"); + return outputPath; +} + +function shouldCreateWindowsGpuWebcamProxy(filePath: string) { + const extension = path.extname(filePath).toLowerCase(); + return extension !== ".mp4" && extension !== ".m4v" && extension !== ".mov"; +} + +function buildWindowsGpuWebcamProxyArgs( + options: NativeStaticLayoutExportOptions, + outputPath: string, +) { + if (!options.webcamInputPath) { + throw new Error("Windows GPU webcam proxy requires an input path"); + } + + const frameRate = Math.max(1, Math.round(options.frameRate)); + const proxyBitrate = Math.max(2_000_000, Math.min(6_000_000, Math.round(options.bitrate / 3))); + return [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + options.webcamInputPath, + "-map", + "0:v:0", + "-an", + "-t", + formatCliNumber(options.durationSec), + "-vf", + `scale=trunc(iw/2)*2:trunc(ih/2)*2,setsar=1,fps=${frameRate}`, + "-c:v", + "libx264", + "-preset", + "ultrafast", + "-tune", + "zerolatency", + "-b:v", + String(proxyBitrate), + "-maxrate", + String(Math.round(proxyBitrate * 1.2)), + "-bufsize", + String(proxyBitrate * 2), + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + outputPath, + ]; +} + +async function prepareWindowsGpuWebcamInput( + ffmpegPath: string, + options: NativeStaticLayoutExportOptions, + outputPath: string, + session: NativeStaticLayoutExportSession, +) { + if (!options.webcamInputPath || !shouldCreateWindowsGpuWebcamProxy(options.webcamInputPath)) { + return { inputPath: options.webcamInputPath ?? null, elapsedMs: 0 }; + } + + const result = await runFfmpegWithMetrics( + ffmpegPath, + buildWindowsGpuWebcamProxyArgs(options, outputPath), + Math.max(5 * 60 * 1000, options.durationSec * 1000), + session, + ); + if (!result.success) { + throw new Error(getFfmpegFailureMessage(result)); + } + + return { inputPath: outputPath, elapsedMs: result.elapsedMs }; +} + +async function prepareNativeStaticLayoutSourceInput( + ffmpegPath: string, + options: NativeStaticLayoutExportOptions, + outputPath: string, + session: NativeStaticLayoutExportSession, +) { + const metadata = await probeNativeVideoMetadata(ffmpegPath, options.inputPath); + if (!shouldCreateNativeStaticLayoutSourceProxy(metadata, options.inputPath)) { + return { + inputPath: options.inputPath, + elapsedMs: 0, + sourceCodec: metadata.codec, + proxyCreated: false, + }; + } + + const sourceDurationSec = Math.max( + 0.001, + metadata.streamDuration ?? metadata.duration ?? options.durationSec, + ); + const result = await runFfmpegWithMetrics( + ffmpegPath, + buildNativeStaticLayoutSourceProxyArgs(options, outputPath, metadata), + Math.max(5 * 60 * 1000, sourceDurationSec * 2000), + session, + ); + if (!result.success) { + throw new Error(getFfmpegFailureMessage(result)); + } + + const proxyMetadata = await probeNativeVideoMetadata(ffmpegPath, outputPath); + const validationIssues = validateNativeStaticLayoutSourceProxyMetadata(proxyMetadata, { + sourceDurationSec, + }); + if (validationIssues.length > 0) { + throw new Error(`Native source proxy is invalid: ${validationIssues.join("; ")}`); + } + + return { + inputPath: outputPath, + elapsedMs: result.elapsedMs, + sourceCodec: metadata.codec, + proxyCodec: proxyMetadata.codec, + proxyCreated: true, + }; +} + +export function buildExperimentalNvidiaCudaStaticLayoutArgs( + options: NativeStaticLayoutExportOptions, + outputPath: string, + workDir: string, +) { + const background = convertHexColorToNv12(options.backgroundColor); + const backgroundBlurPx = Math.max(0, options.backgroundBlurPx ?? 0); + const shadowIntensityPct = Math.round(clampUnit(options.shadowIntensity ?? 0) * 100); + const shadowOffsetY = + shadowIntensityPct > 0 ? Math.max(1, Math.round(options.height * 0.012)) : 0; + const args = [ + "--input", + options.inputPath, + "--output", + outputPath, + "--work-dir", + workDir, + "--width", + String(options.width), + "--height", + String(options.height), + "--fps", + String(Math.max(1, Math.round(options.frameRate))), + "--bitrate-mbps", + String(getNvidiaCudaBitrateMbps(options)), + "--encoding-mode", + options.encodingMode, + "--duration-sec", + formatCliNumber(options.durationSec), + "--stream-sync", + "--prewarm-ms", + process.env.RECORDLY_NVIDIA_CUDA_PREWARM_MS || "500", + "--content-x", + String(Math.round(options.offsetX)), + "--content-y", + String(Math.round(options.offsetY)), + "--content-width", + String(Math.round(options.contentWidth)), + "--content-height", + String(Math.round(options.contentHeight)), + "--radius", + String(Math.round(Math.max(0, options.borderRadius ?? 0))), + "--background-y", + String(background.y), + "--background-u", + String(background.u), + "--background-v", + String(background.v), + ]; + if (!canMuxNvidiaCudaSourceAudioInline(options)) { + args.push("--video-only"); + } + + if (options.backgroundImagePath) { + args.push("--background-image", options.backgroundImagePath); + } + if ( + Number.isFinite(options.sourceCropX) && + Number.isFinite(options.sourceCropY) && + Number.isFinite(options.sourceCropWidth) && + Number.isFinite(options.sourceCropHeight) && + (options.sourceCropWidth ?? 0) >= 2 && + (options.sourceCropHeight ?? 0) >= 2 + ) { + args.push( + "--source-crop-x", + String(Math.round(options.sourceCropX ?? 0)), + "--source-crop-y", + String(Math.round(options.sourceCropY ?? 0)), + "--source-crop-width", + String(Math.round(options.sourceCropWidth ?? 0)), + "--source-crop-height", + String(Math.round(options.sourceCropHeight ?? 0)), + ); + } + if (backgroundBlurPx > 0) { + args.push("--background-blur", formatCliNumber(backgroundBlurPx)); + } + if (shadowOffsetY > 0 && shadowIntensityPct > 0) { + args.push( + "--shadow-offset-y", + String(shadowOffsetY), + "--shadow-intensity-pct", + String(shadowIntensityPct), + ); + } + if (options.webcamInputPath && (options.webcamSize ?? 0) > 0) { + args.push( + "--webcam-input", + options.webcamInputPath, + "--webcam-x", + String(Math.round(options.webcamLeft ?? 0)), + "--webcam-y", + String(Math.round(options.webcamTop ?? 0)), + "--webcam-size", + String(Math.round(options.webcamSize ?? 0)), + "--webcam-radius", + String(Math.round(Math.max(0, options.webcamRadius ?? 0))), + "--webcam-time-offset-ms", + formatCliNumber(options.webcamTimeOffsetMs ?? 0), + "--webcam-stream", + ); + if (options.webcamMirror !== false) { + args.push("--webcam-mirror"); + } + } + if (options.cursorTelemetryPath) { + args.push( + "--cursor-json", + options.cursorTelemetryPath, + "--cursor-height", + String(Math.round(Math.max(1, options.cursorSize ?? 84))), + "--cursor-style", + "external", + ); + if (options.cursorAtlasPath && options.cursorAtlasMetadataPath) { + args.push( + "--cursor-atlas-png", + options.cursorAtlasPath, + "--cursor-atlas-metadata", + options.cursorAtlasMetadataPath, + ); + } + } + if (options.zoomTelemetryPath) { + args.push("--zoom-telemetry", options.zoomTelemetryPath); + } + if (options.timelineMapPath) { + args.push("--timeline-map", options.timelineMapPath); + } + if (process.env.RECORDLY_NVIDIA_CUDA_SAMPLE_GPU === "1") { + args.push("--sample-gpu"); + } + + return args; +} + +function canMuxNvidiaCudaSourceAudioInline(options: NativeStaticLayoutExportOptions) { + if (options.nvidiaCudaForceVideoOnly) { + return false; + } + + const audioOptions = options.audioOptions; + if (audioOptions?.audioMode !== "copy-source" || !audioOptions.audioSourcePath) { + return false; + } + + return path.resolve(audioOptions.audioSourcePath) === path.resolve(options.inputPath); +} + +async function runExperimentalNvidiaCudaStaticLayoutExport( + ffmpegPath: string, + options: NativeStaticLayoutExportOptions, + outputPath: string, + chunkDirectory: string, + session: NativeStaticLayoutExportSession, + onProgress?: (progress: NativeStaticLayoutExportProgress) => void, +) { + const scriptPath = await resolveExperimentalNvidiaCudaExportScriptPath(); + if (!scriptPath) { + throw new Error("Experimental NVIDIA CUDA export script is not available"); + } + + const nodeCommand = resolveExperimentalNvidiaCudaNodeCommand(); + const workDir = path.join(chunkDirectory, "nvidia-cuda-work"); + const args = [ + scriptPath, + ...buildExperimentalNvidiaCudaStaticLayoutArgs(options, outputPath, workDir), + ]; + const startedAt = getNowMs(); + const startedAtIso = new Date().toISOString(); + const timeoutMs = Math.max(20 * 60 * 1000, options.durationSec * 2000); + const stallTimeoutMs = getNvidiaCudaAutoStallTimeoutMs( + isValidatedNvidiaCudaFallbackCandidate(options), + ); + const ffmpegDirectory = path.dirname(ffmpegPath); + const pathKey = process.platform === "win32" ? "Path" : "PATH"; + const env = { + ...process.env, + ...nodeCommand.env, + RECORDLY_NVIDIA_CUDA_EXPORT_HIGH_PRIORITY: + process.env.RECORDLY_NVIDIA_CUDA_EXPORT_HIGH_PRIORITY ?? "1", + RECORDLY_FFMPEG_EXE: ffmpegPath, + RECORDLY_FFPROBE_EXE: process.env.RECORDLY_FFPROBE_EXE ?? getFfprobeBinaryPath(), + [pathKey]: `${ffmpegDirectory}${path.delimiter}${process.env[pathKey] ?? ""}`, + }; + const powerGuard = startNativeStaticLayoutExportPowerGuard(); + + return await new Promise<{ + elapsedMs: number; + stdout: string; + stderr: string; + summary: NvidiaCudaExportSummary; + }>((resolve, reject) => { + const child = spawn(nodeCommand.command, args, { + env, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + const childPriorityApplied = setNativeStaticLayoutExportProcessPriority( + child.pid, + "NVIDIA CUDA export wrapper", + ); + console.info("[native-static-layout-export] NVIDIA CUDA runtime guard started", { + childPriorityApplied, + powerGuardStarted: powerGuard.started, + }); + session.currentProcess = child; + if (session.terminating) { + child.kill("SIGKILL"); + } + + let stdout = ""; + let stderr = ""; + let stderrLineBuffer = ""; + let lastProgressPercentage = 0; + let lastProgressForStallGuard: { + currentFrame: number; + percentage: number; + stage?: string; + } = { currentFrame: -1, percentage: -1 }; + let stallTimedOut = false; + let settled = false; + const timeout = setTimeout(() => { + if (settled) return; + child.kill("SIGKILL"); + }, timeoutMs); + let stallTimeout: ReturnType | null = null; + const clearStallTimeout = () => { + if (stallTimeout) { + clearTimeout(stallTimeout); + stallTimeout = null; + } + }; + const armStallTimeout = () => { + if (!stallTimeoutMs) { + return; + } + clearStallTimeout(); + stallTimeout = setTimeout(() => { + if (settled) return; + stallTimedOut = true; + child.kill("SIGKILL"); + }, stallTimeoutMs); + }; + armStallTimeout(); + + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk: Buffer) => { + const text = chunk.toString(); + stderr += text; + stderrLineBuffer += text; + const lines = stderrLineBuffer.split(/\r?\n/); + stderrLineBuffer = lines.pop() ?? ""; + for (const line of lines) { + const progress = parseWindowsGpuExportProgressLine(line); + if (!progress) { + continue; + } + const elapsedMs = Math.max(0, getNowMs() - startedAt); + const averageFps = + typeof progress.averageFps === "number" && + Number.isFinite(progress.averageFps) && + progress.averageFps > 0 + ? progress.averageFps + : elapsedMs > 0 && progress.currentFrame > 0 + ? (progress.currentFrame * 1000) / elapsedMs + : undefined; + const mappedPercentage = mapNvidiaCudaWrapperProgressPercentage(progress); + lastProgressPercentage = Math.max(lastProgressPercentage, mappedPercentage); + const progressForStallGuard = { + currentFrame: progress.currentFrame, + percentage: mappedPercentage, + stage: progress.stage, + }; + if ( + hasNativeStaticLayoutProgressAdvanced( + progressForStallGuard, + lastProgressForStallGuard, + ) + ) { + lastProgressForStallGuard = progressForStallGuard; + armStallTimeout(); + } + onProgress?.({ + ...progress, + percentage: lastProgressPercentage, + sessionId: options.sessionId, + backend: "nvidia-cuda-compositor", + elapsedMs, + averageFps, + }); + } + }); + child.once("error", (error) => { + if (settled) return; + settled = true; + if (session.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + clearStallTimeout(); + powerGuard.release(); + reject(error); + }); + child.once("close", async (code, signal) => { + if (settled) return; + settled = true; + if (session.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + clearStallTimeout(); + powerGuard.release(); + + if (session.terminating) { + reject(new Error("Native static layout export was cancelled")); + return; + } + + const elapsedMs = getNowMs() - startedAt; + const summary = parseNvidiaCudaExportSummary(stdout); + if (summary) { + summary.appRuntimeGuard = { + powerGuardStarted: powerGuard.started, + wrapperProcessPriorityBoosted: childPriorityApplied, + nativeProcessPriorityBoosted: summary.nativeProcessPriorityBoosted, + }; + } + await Promise.allSettled([ + fs.writeFile(path.join(chunkDirectory, "nvidia-cuda-export.stdout.json"), stdout), + fs.writeFile(path.join(chunkDirectory, "nvidia-cuda-export.stderr.log"), stderr), + summary + ? fs.writeFile( + path.join(chunkDirectory, "nvidia-cuda-export.summary.json"), + `${JSON.stringify(summary, null, 2)}\n`, + ) + : Promise.resolve(), + ]); + await persistNvidiaCudaExportDiagnostics({ + args, + code, + elapsedMs, + outputPath, + sessionId: options.sessionId, + signal, + startedAtIso, + stderr, + stdout, + summary, + timedOut: stallTimedOut, + }); + if (code !== 0 || !summary?.success) { + const suffix = signal ? ` (signal ${signal})` : ""; + reject( + new Error( + (stallTimedOut && stallTimeoutMs + ? `Experimental NVIDIA CUDA exporter stalled for ${stallTimeoutMs}ms without progress` + : stderr.trim()) || + stdout.trim() || + `Experimental NVIDIA CUDA exporter exited with code ${code ?? "unknown"}${suffix}`, + ), + ); + return; + } + + resolve({ + elapsedMs, + stdout, + stderr, + summary, + }); + }); + }); +} + +async function runExperimentalWindowsGpuStaticLayoutExport( + options: NativeStaticLayoutExportOptions, + outputPath: string, + session: NativeStaticLayoutExportSession, + onProgress?: (progress: NativeStaticLayoutExportProgress) => void, +) { + const executablePath = await resolveExperimentalWindowsGpuExporterPath(); + if (!executablePath) { + throw new Error("Experimental Windows GPU exporter is not built"); + } + + const args = buildExperimentalWindowsGpuStaticLayoutArgs(options, outputPath); + const startedAt = getNowMs(); + const startedAtIso = new Date().toISOString(); + const timeoutMs = Math.max(15 * 60 * 1000, options.durationSec * 1000); + const stallTimeoutMs = getNativeGpuCompositorStallTimeoutMs(); + + return await new Promise<{ + elapsedMs: number; + stdout: string; + stderr: string; + summary: WindowsGpuExportSummary; + }>((resolve, reject) => { + const child = spawn(executablePath, args, { + stdio: ["ignore", "pipe", "pipe"], + }); + session.currentProcess = child; + if (session.terminating) { + child.kill("SIGKILL"); + } + + let stdout = ""; + let stderr = ""; + let stderrLineBuffer = ""; + let lastProgressForStallGuard: { + currentFrame: number; + percentage: number; + stage?: string; + } = { currentFrame: -1, percentage: -1 }; + let stallTimedOut = false; + let settled = false; + const timeout = setTimeout(() => { + if (settled) return; + child.kill("SIGKILL"); + }, timeoutMs); + let stallTimeout: ReturnType | null = null; + const clearStallTimeout = () => { + if (stallTimeout) { + clearTimeout(stallTimeout); + stallTimeout = null; + } + }; + const armStallTimeout = () => { + if (!stallTimeoutMs) { + return; + } + clearStallTimeout(); + stallTimeout = setTimeout(() => { + if (settled) return; + stallTimedOut = true; + child.kill("SIGKILL"); + }, stallTimeoutMs); + }; + armStallTimeout(); + + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk: Buffer) => { + const text = chunk.toString(); + stderr += text; + stderrLineBuffer += text; + const lines = stderrLineBuffer.split(/\r?\n/); + stderrLineBuffer = lines.pop() ?? ""; + for (const line of lines) { + const progress = parseWindowsGpuExportProgressLine(line); + if (!progress) { + continue; + } + if (hasNativeStaticLayoutProgressAdvanced(progress, lastProgressForStallGuard)) { + lastProgressForStallGuard = { + currentFrame: progress.currentFrame, + percentage: progress.percentage, + stage: progress.stage, + }; + armStallTimeout(); + } + const elapsedMs = Math.max(0, getNowMs() - startedAt); + onProgress?.({ + ...progress, + sessionId: options.sessionId, + backend: "windows-d3d11-compositor", + elapsedMs, + averageFps: + elapsedMs > 0 && progress.currentFrame > 0 + ? (progress.currentFrame * 1000) / elapsedMs + : undefined, + }); + } + }); + child.once("error", (error) => { + if (settled) return; + settled = true; + if (session.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + clearStallTimeout(); + reject(error); + }); + child.once("close", async (code, signal) => { + if (settled) return; + settled = true; + if (session.currentProcess === child) { + session.currentProcess = null; + } + clearTimeout(timeout); + clearStallTimeout(); + + if (session.terminating) { + reject(new Error("Native static layout export was cancelled")); + return; + } + + const elapsedMs = getNowMs() - startedAt; + const summary = parseWindowsGpuExportSummary(stdout); + await persistWindowsGpuExportDiagnostics({ + args, + code, + elapsedMs, + outputPath, + signal, + startedAtIso, + stderr, + stdout, + summary, + timedOut: stallTimedOut, + }); + + if (code !== 0 || !summary?.success) { + const suffix = signal ? ` (signal ${signal})` : ""; + reject( + new Error( + (stallTimedOut && stallTimeoutMs + ? `Experimental Windows GPU exporter stalled for ${stallTimeoutMs}ms without progress` + : stderr.trim()) || + stdout.trim() || + `Experimental Windows GPU exporter exited with code ${code ?? "unknown"}${suffix}`, + ), + ); + return; + } + + resolve({ + elapsedMs, + stdout, + stderr, + summary, + }); + }); + }); +} + +export async function exportNativeStaticLayoutVideo( + ffmpegPath: string, + options: NativeStaticLayoutExportOptions, + onProgress?: (progress: NativeStaticLayoutExportProgress) => void, +) { + if (options.width % 2 !== 0 || options.height % 2 !== 0) { + throw new Error("Native static layout export requires even output dimensions"); + } + if (!Number.isFinite(options.durationSec) || options.durationSec <= 0) { + throw new Error("Native static layout export requires a positive duration"); + } + options = await normalizeNativeStaticLayoutBackground(options); + if ( + options.webcamInputPath && + !(options.experimentalWindowsGpuCompositor && process.platform === "win32") + ) { + throw new Error("Native webcam overlay requires the Windows GPU compositor"); + } + if ( + options.zoomTelemetry?.length && + !(options.experimentalWindowsGpuCompositor && process.platform === "win32") + ) { + throw new Error("Native zoom telemetry requires the Windows GPU compositor"); + } + + const chunkDurationSec = Math.max(1, Math.min(300, options.chunkDurationSec ?? 120)); + const chunks = buildNativeStaticLayoutChunks(options.durationSec, chunkDurationSec); + if (chunks.length === 0) { + throw new Error("Native static layout export produced no chunks"); + } + + const sessionId = + options.sessionId ?? + `recordly-static-layout-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const session: NativeStaticLayoutExportSession = { + terminating: false, + currentProcess: null, + }; + const chunkDirectory = path.join(app.getPath("temp"), sessionId); + const concatListPath = path.join(chunkDirectory, "chunks.txt"); + const videoOnlyPath = path.join(app.getPath("temp"), `${sessionId}.mp4`); + const ffprobePath = getFfprobeBinaryPath(); + let outputPathToKeep: string | null = null; + let videoOutputValidated = false; + const metrics: NativeStaticLayoutExportMetrics = { + chunkCount: chunks.length, + chunkDurationSec, + chunkExecMs: 0, + fallbackChunkCount: 0, + chunks: [], + }; + const validateRenderedVideoOutput = async () => { + await validateNativeVideoOutputFile(ffprobePath, videoOnlyPath, { + durationSec: options.durationSec, + targetFrames: Math.ceil(options.durationSec * options.frameRate), + }); + videoOutputValidated = true; + }; + + try { + nativeStaticLayoutExportSessions.set(sessionId, session); + await fs.mkdir(chunkDirectory, { recursive: true }); + const sourceInput = await prepareNativeStaticLayoutSourceInput( + ffmpegPath, + options, + path.join(chunkDirectory, "source-proxy.mp4"), + session, + ); + if (sourceInput.elapsedMs > 0) { + metrics.staticAssetExecMs = (metrics.staticAssetExecMs ?? 0) + sourceInput.elapsedMs; + } + if (sourceInput.inputPath !== options.inputPath) { + console.info("[native-static-layout-export] Prepared H.264 source proxy", { + sourceCodec: sourceInput.sourceCodec, + proxyCodec: sourceInput.proxyCodec, + elapsedMs: sourceInput.elapsedMs, + }); + options = { + ...options, + inputPath: sourceInput.inputPath, + }; + } + const timelineMapPath = await prepareNativeStaticLayoutTimelineMap( + options, + path.join(chunkDirectory, "timeline-map.csv"), + ); + if (timelineMapPath) { + if (!(options.experimentalWindowsGpuCompositor && process.platform === "win32")) { + throw new Error("Native timeline-map export requires the Windows GPU compositor"); + } + options = { + ...options, + timelineMapPath, + }; + } + const fullConfig: NativeStaticLayoutExportArgsConfig = { + inputPath: options.inputPath, + outputPath: videoOnlyPath, + width: options.width, + height: options.height, + frameRate: options.frameRate, + bitrate: options.bitrate, + encodingMode: options.encodingMode, + contentWidth: options.contentWidth, + contentHeight: options.contentHeight, + offsetX: options.offsetX, + offsetY: options.offsetY, + sourceCropX: options.sourceCropX, + sourceCropY: options.sourceCropY, + sourceCropWidth: options.sourceCropWidth, + sourceCropHeight: options.sourceCropHeight, + backgroundColor: options.backgroundColor, + backgroundImagePath: options.backgroundImagePath, + backgroundBlurPx: options.backgroundBlurPx, + borderRadius: options.borderRadius, + shadowIntensity: options.shadowIntensity, + durationSec: options.durationSec, + }; + const usePrecompositedLayout = shouldUsePrecompositedStaticLayout(options); + let didRenderVideo = false; + let didMuxAudioInline = false; + + if (options.experimentalWindowsGpuCompositor && process.platform === "win32") { + try { + if (session.terminating) { + throw new Error("Native static layout export was cancelled"); + } + let experimentalGpuOptions = options; + if (options.webcamInputPath) { + const webcamProxyPath = path.join(chunkDirectory, "webcam-proxy.mp4"); + const webcamInput = await prepareWindowsGpuWebcamInput( + ffmpegPath, + options, + webcamProxyPath, + session, + ); + if (webcamInput.elapsedMs > 0) { + metrics.staticAssetExecMs = + (metrics.staticAssetExecMs ?? 0) + webcamInput.elapsedMs; + } + if ( + webcamInput.inputPath && + webcamInput.inputPath !== options.webcamInputPath + ) { + experimentalGpuOptions = { + ...options, + webcamInputPath: webcamInput.inputPath, + }; + } + } + if (options.cursorTelemetry?.length) { + const cursorTelemetryPath = await prepareWindowsGpuCursorTelemetry( + options, + path.join(chunkDirectory, "cursor-telemetry.csv"), + ); + if (cursorTelemetryPath) { + experimentalGpuOptions = { + ...experimentalGpuOptions, + cursorTelemetryPath, + }; + } + const cursorAtlas = await prepareWindowsGpuCursorAtlas( + options, + path.join(chunkDirectory, "cursor-atlas.png"), + path.join(chunkDirectory, "cursor-atlas.csv"), + ); + if (cursorAtlas) { + experimentalGpuOptions = { + ...experimentalGpuOptions, + cursorAtlasPath: cursorAtlas.atlasPath, + cursorAtlasMetadataPath: cursorAtlas.metadataPath, + }; + } + } + if (options.zoomTelemetry?.length) { + const zoomTelemetryPath = await prepareWindowsGpuZoomTelemetry( + options, + path.join(chunkDirectory, "zoom-telemetry.csv"), + ); + if (zoomTelemetryPath) { + experimentalGpuOptions = { + ...experimentalGpuOptions, + zoomTelemetryPath, + }; + } + } + let experimentalNvidiaCudaOptions = experimentalGpuOptions; + const nvidiaCudaSkipReason = + await getExperimentalNvidiaCudaExportSkipReason(options); + let shouldTryNvidiaCuda = nvidiaCudaSkipReason === null; + const validatedCudaFallbackCandidate = + isValidatedNvidiaCudaFallbackCandidate(options); + if ( + shouldTryNvidiaCuda && + (validatedCudaFallbackCandidate || isNvidiaCudaForceVideoOnlyEnabled()) && + (experimentalNvidiaCudaOptions.audioOptions?.audioMode ?? "none") !== "none" + ) { + experimentalNvidiaCudaOptions = { + ...experimentalNvidiaCudaOptions, + nvidiaCudaForceVideoOnly: true, + }; + console.info( + "[native-static-layout-export] NVIDIA CUDA candidate will use shared audio mux validation", + { + audioMode: + experimentalNvidiaCudaOptions.audioOptions?.audioMode ?? "none", + forcedByEnv: isNvidiaCudaForceVideoOnlyEnabled(), + userOptIn: options.experimentalNvidiaCudaExport === true, + }, + ); + } + const shouldLogNvidiaCudaSkip = + isExplicitNvidiaCudaExportEnabled() || + (options.experimentalNvidiaCudaExport === true && + nvidiaCudaSkipReason !== "env-disabled"); + if ( + !shouldTryNvidiaCuda && + shouldLogNvidiaCudaSkip && + nvidiaCudaSkipReason !== "env-disabled" + ) { + console.warn( + "[native-static-layout-export] Skipping NVIDIA CUDA compositor; falling back to Windows GPU compositor", + { + reason: nvidiaCudaSkipReason, + audioMode: options.audioOptions?.audioMode ?? "none", + overrideEnv: NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV, + userOptIn: options.experimentalNvidiaCudaExport === true, + }, + ); + } + if (shouldTryNvidiaCuda && options.cursorTelemetry?.length) { + const cursorTelemetryPath = await prepareNvidiaCudaCursorTelemetry( + options, + path.join(chunkDirectory, "cursor-telemetry.json"), + ); + const cursorAtlas = await prepareNvidiaCudaCursorAtlas( + options, + path.join(chunkDirectory, "cursor-atlas-nvidia.png"), + path.join(chunkDirectory, "cursor-atlas-nvidia.tsv"), + ); + shouldTryNvidiaCuda = Boolean(cursorTelemetryPath && cursorAtlas); + if (cursorTelemetryPath && cursorAtlas) { + experimentalNvidiaCudaOptions = { + ...experimentalNvidiaCudaOptions, + cursorTelemetryPath, + cursorAtlasPath: cursorAtlas.atlasPath, + cursorAtlasMetadataPath: cursorAtlas.metadataPath, + }; + } + } + + if (shouldTryNvidiaCuda) { + try { + const shouldMuxAudioInline = canMuxNvidiaCudaSourceAudioInline( + experimentalNvidiaCudaOptions, + ); + const cudaResult = await runExperimentalNvidiaCudaStaticLayoutExport( + ffmpegPath, + experimentalNvidiaCudaOptions, + videoOnlyPath, + chunkDirectory, + session, + onProgress, + ); + const cudaValidationIssues = validateNvidiaCudaExportSummary( + cudaResult.summary, + { + durationSec: options.durationSec, + targetFrames: Math.ceil(options.durationSec * options.frameRate), + requiresTimelineSync: shouldMuxAudioInline, + }, + ); + if (cudaValidationIssues.length > 0) { + throw new Error( + `Experimental NVIDIA CUDA compositor produced an invalid output: ${cudaValidationIssues.join("; ")}`, + ); + } + const outputStat = await fs.stat(videoOnlyPath); + if (outputStat.size <= 0) { + throw new Error( + "Experimental NVIDIA CUDA compositor produced an empty output file", + ); + } + await validateRenderedVideoOutput(); + console.info( + "[native-static-layout-export] NVIDIA CUDA compositor completed", + { + elapsedMs: cudaResult.elapsedMs, + fps: cudaResult.summary.fps, + targetFrames: cudaResult.summary.targetFrames, + durationSec: cudaResult.summary.durationSec, + nativeEncodeMs: cudaResult.summary.timingsMs?.nativeEncode, + muxMs: cudaResult.summary.timingsMs?.mux, + endToEndMs: cudaResult.summary.timingsMs?.endToEnd, + nativeFps: + cudaResult.summary.nativeSummary?.measuredFps ?? + cudaResult.summary.nativeSummary?.fps, + mappedDisplayFrames: + cudaResult.summary.nativeSummary?.mappedDisplayFrames, + selectedDisplayFrames: + cudaResult.summary.nativeSummary?.selectedDisplayFrames, + skippedDisplayFrames: + cudaResult.summary.nativeSummary?.skippedDisplayFrames, + roiCompositeFrames: + cudaResult.summary.nativeSummary?.roiCompositeFrames, + monolithicCompositeFrames: + cudaResult.summary.nativeSummary?.monolithicCompositeFrames, + copyCompositeFrames: + cudaResult.summary.nativeSummary?.copyCompositeFrames, + webcamOverlay: cudaResult.summary.nativeSummary?.webcamOverlay, + cursorAtlas: cudaResult.summary.nativeSummary?.cursorAtlas, + zoomOverlay: cudaResult.summary.nativeSummary?.zoomOverlay, + zoomSamples: cudaResult.summary.nativeSummary?.zoomSamples, + }, + ); + metrics.chunkCount = 1; + metrics.chunkDurationSec = options.durationSec; + metrics.chunkExecMs += cudaResult.elapsedMs; + metrics.chunks.push({ + index: 0, + startSec: 0, + durationSec: options.durationSec, + backend: "nvidia-cuda-compositor", + elapsedMs: cudaResult.elapsedMs, + outputBytes: outputStat.size, + nvidiaCudaSummary: cudaResult.summary, + }); + didRenderVideo = true; + didMuxAudioInline = shouldMuxAudioInline; + } catch (error) { + if (session.terminating) { + throw error; + } + metrics.fallbackChunkCount++; + console.warn( + "[native-static-layout-export] Experimental NVIDIA CUDA compositor failed or produced invalid output; falling back to Windows GPU compositor:", + error, + ); + await removeTemporaryExportFile(videoOnlyPath); + } + } + + if (!didRenderVideo) { + const gpuResult = await runExperimentalWindowsGpuStaticLayoutExport( + experimentalGpuOptions, + videoOnlyPath, + session, + onProgress, + ); + const gpuValidationIssues = validateWindowsGpuExportSummary(gpuResult.summary, { + durationSec: options.durationSec, + targetFrames: Math.ceil(options.durationSec * options.frameRate), + }); + if (gpuValidationIssues.length > 0) { + throw new Error( + `Experimental Windows GPU compositor produced an invalid output: ${gpuValidationIssues.join("; ")}`, + ); + } + await validateRenderedVideoOutput(); + console.info("[native-static-layout-export] Windows GPU compositor completed", { + elapsedMs: gpuResult.elapsedMs, + width: gpuResult.summary.width, + height: gpuResult.summary.height, + fps: gpuResult.summary.fps, + frames: gpuResult.summary.frames, + realtimeMultiplier: gpuResult.summary.realtimeMultiplier, + surfacePoolSize: gpuResult.summary.surfacePoolSize, + gpuDecodeSurface: gpuResult.summary.gpuDecodeSurface, + adapterIndex: gpuResult.summary.adapterIndex, + encoderBackend: gpuResult.summary.encoderBackend, + encoderTuningApplied: gpuResult.summary.encoderTuningApplied, + readMs: gpuResult.summary.readMs, + videoProcessMs: gpuResult.summary.videoProcessMs, + writeSampleMs: gpuResult.summary.writeSampleMs, + finalizeMs: gpuResult.summary.finalizeMs, + webcamOverlay: gpuResult.summary.webcamOverlay, + cursorOverlay: gpuResult.summary.cursorOverlay, + cursorAtlas: gpuResult.summary.cursorAtlas, + zoomOverlay: gpuResult.summary.zoomOverlay, + }); + const outputStat = await fs.stat(videoOnlyPath); + metrics.chunkCount = 1; + metrics.chunkDurationSec = options.durationSec; + metrics.chunkExecMs += gpuResult.elapsedMs; + metrics.chunks.push({ + index: 0, + startSec: 0, + durationSec: options.durationSec, + backend: "windows-d3d11-compositor", + elapsedMs: gpuResult.elapsedMs, + outputBytes: outputStat.size, + windowsGpuSummary: gpuResult.summary, + }); + didRenderVideo = true; + } + } catch (error) { + if (session.terminating) { + throw error; + } + if (hasNativeStaticLayoutTimeline(options)) { + throw error; + } + metrics.fallbackChunkCount++; + console.warn( + "[native-static-layout-export] Experimental Windows GPU compositor unavailable; falling back to FFmpeg static layout:", + error, + ); + await removeTemporaryExportFile(videoOnlyPath); + if (options.webcamInputPath || options.zoomTelemetry?.length) { + throw error; + } + } + } + + if (!didRenderVideo && hasNativeStaticLayoutTimeline(options)) { + throw new Error("Native timeline-map export requires a GPU compositor backend"); + } + if (!didRenderVideo && hasNativeStaticLayoutSourceCrop(options)) { + throw new Error("Native crop export requires a GPU compositor backend"); + } + + if (!didRenderVideo && usePrecompositedLayout) { + const maskPath = path.join(chunkDirectory, "layout-mask.pgm"); + const staticBackgroundPath = path.join(chunkDirectory, "layout-background.png"); + await fs.writeFile( + maskPath, + createNativeSquircleMaskPgmBuffer( + options.contentWidth, + options.contentHeight, + options.borderRadius ?? 0, + ), + ); + + const backgroundResult = await runFfmpegWithMetrics( + ffmpegPath, + buildNativeStaticBackgroundRenderArgs({ + ...fullConfig, + inputPath: options.inputPath, + outputPath: staticBackgroundPath, + maskPath, + }), + 2 * 60 * 1000, + session, + ); + metrics.staticAssetExecMs = backgroundResult.elapsedMs; + if (!backgroundResult.success) { + throw new Error(getFfmpegFailureMessage(backgroundResult)); + } + + const fullResult = await runFfmpegWithMetrics( + ffmpegPath, + buildNativePrecompositedStaticLayoutArgs({ + ...fullConfig, + staticBackgroundPath, + maskPath, + }), + 15 * 60 * 1000, + session, + ); + metrics.chunkExecMs += fullResult.elapsedMs; + if (!fullResult.success) { + throw new Error(getFfmpegFailureMessage(fullResult)); + } + + const outputStat = await fs.stat(videoOnlyPath); + metrics.chunkCount = 1; + metrics.chunkDurationSec = options.durationSec; + metrics.chunks.push({ + index: 0, + startSec: 0, + durationSec: options.durationSec, + backend: "cuda-static-composite", + elapsedMs: fullResult.elapsedMs, + outputBytes: outputStat.size, + }); + } else if (!didRenderVideo) { + const primaryResult = await runFfmpegWithMetrics( + ffmpegPath, + buildNativeCudaOverlayStaticLayoutArgs(fullConfig), + 15 * 60 * 1000, + session, + ); + let fullResult = primaryResult; + let fullBackend: NativeStaticLayoutBackend = "cuda-overlay"; + let fallbackReason: string | undefined; + if (!primaryResult.success) { + fullBackend = "cuda-scale-cpu-pad"; + fallbackReason = isNativeCudaOutOfMemory(primaryResult.stderr) + ? "cuda-oom" + : "cuda-overlay-failed"; + metrics.fallbackChunkCount++; + fullResult = await runFfmpegWithMetrics( + ffmpegPath, + buildNativeCudaScaleCpuPadStaticLayoutArgs(fullConfig), + 15 * 60 * 1000, + session, + ); + } + metrics.chunkExecMs += fullResult.elapsedMs; + if (fullResult !== primaryResult) { + metrics.chunkExecMs += primaryResult.elapsedMs; + } + + if (fullResult.success) { + const outputStat = await fs.stat(videoOnlyPath); + metrics.chunkCount = 1; + metrics.chunkDurationSec = options.durationSec; + metrics.chunks.push({ + index: 0, + startSec: 0, + durationSec: options.durationSec, + backend: fullBackend, + elapsedMs: fullResult.elapsedMs, + outputBytes: outputStat.size, + fallbackReason, + }); + } else if (isNativeCudaOutOfMemory(fullResult.stderr)) { + const concatLines: string[] = []; + + for (const chunk of chunks) { + if (session.terminating) { + throw new Error("Native static layout export was cancelled"); + } + + const outputPath = getStaticLayoutChunkOutputPath(chunkDirectory, chunk.index); + const baseConfig: NativeStaticLayoutExportArgsConfig = { + inputPath: options.inputPath, + outputPath, + width: options.width, + height: options.height, + frameRate: options.frameRate, + bitrate: options.bitrate, + encodingMode: options.encodingMode, + contentWidth: options.contentWidth, + contentHeight: options.contentHeight, + offsetX: options.offsetX, + offsetY: options.offsetY, + backgroundColor: options.backgroundColor, + startSec: chunk.startSec, + durationSec: chunk.durationSec, + }; + const primary = await runFfmpegWithMetrics( + ffmpegPath, + buildNativeCudaOverlayStaticLayoutArgs(baseConfig), + 15 * 60 * 1000, + session, + ); + let backend: NativeStaticLayoutBackend = "cuda-overlay"; + let result = primary; + let fallbackReason: string | undefined; + + if (!primary.success && isNativeCudaOutOfMemory(primary.stderr)) { + backend = "cuda-scale-cpu-pad"; + fallbackReason = "cuda-oom"; + metrics.fallbackChunkCount++; + result = await runFfmpegWithMetrics( + ffmpegPath, + buildNativeCudaScaleCpuPadStaticLayoutArgs(baseConfig), + 15 * 60 * 1000, + session, + ); + } + + metrics.chunkExecMs += result.elapsedMs; + if (!result.success) { + throw new Error(getFfmpegFailureMessage(result)); + } + + const outputStat = await fs.stat(outputPath); + metrics.chunks.push({ + index: chunk.index, + startSec: chunk.startSec, + durationSec: chunk.durationSec, + backend, + elapsedMs: result.elapsedMs, + outputBytes: outputStat.size, + fallbackReason, + }); + concatLines.push(toConcatFileLine(outputPath)); + } + + metrics.chunkCount = chunks.length; + await fs.writeFile(concatListPath, `${concatLines.join("\n")}\n`, "utf8"); + if (session.terminating) { + throw new Error("Native static layout export was cancelled"); + } + const concatStartedAt = getNowMs(); + const concatResult = await runFfmpegWithMetrics( + ffmpegPath, + buildNativeConcatArgs({ listPath: concatListPath, outputPath: videoOnlyPath }), + 15 * 60 * 1000, + session, + ); + metrics.concatExecMs = getNowMs() - concatStartedAt; + if (!concatResult.success) { + throw new Error(getFfmpegFailureMessage(concatResult)); + } + } else { + throw new Error(getFfmpegFailureMessage(fullResult)); + } + } + + const videoOnlyStat = await fs.stat(videoOnlyPath); + metrics.videoOnlyBytes = videoOnlyStat.size; + if (!videoOutputValidated) { + await validateRenderedVideoOutput(); + } + if (didMuxAudioInline) { + outputPathToKeep = videoOnlyPath; + return { + outputPath: videoOnlyPath, + metrics, + }; + } + const audioMuxProgressStart = 97.25; + const audioMuxProgressEnd = 99; + const progressTotalFrames = Math.max(1, Math.ceil(options.durationSec * options.frameRate)); + const finalized = await muxNativeVideoExportAudio( + videoOnlyPath, + options.audioOptions ?? {}, + (progress) => { + const ratio = Math.max(0, Math.min(1, progress.ratio)); + const percentage = + audioMuxProgressStart + (audioMuxProgressEnd - audioMuxProgressStart) * ratio; + const progressPayload: NativeStaticLayoutExportProgress = { + sessionId, + stage: "finalizing", + currentFrame: progressTotalFrames, + totalFrames: progressTotalFrames, + percentage, + }; + const backend = metrics.chunks[0]?.backend; + if (backend) { + progressPayload.backend = backend; + } + onProgress?.(progressPayload); + }, + session, + ); + Object.assign(metrics, finalized.metrics); + outputPathToKeep = finalized.outputPath; + return { + outputPath: finalized.outputPath, + metrics, + }; + } catch (error) { + await removeTemporaryExportFile(videoOnlyPath); + await removeTemporaryExportFile(videoOnlyPath.replace(/\.mp4$/, "-final.mp4")); + throw error; + } finally { + nativeStaticLayoutExportSessions.delete(sessionId); + await fs.rm(chunkDirectory, { force: true, recursive: true }).catch(() => undefined); + if (outputPathToKeep !== videoOnlyPath) { + await removeTemporaryExportFile(videoOnlyPath); + } + } +} + +export async function enqueueNativeVideoExportFrameWrite( + session: NativeVideoExportSession, + frameData: Uint8Array | ArrayBuffer, +) { + const writePromise = session.writeSequence.then(async () => { + if (session.terminating) { + throw new Error("Native video export session was cancelled"); + } + + await writeNativeVideoExportFrame(session, frameData); + }); + + session.writeSequence = writePromise.catch(() => undefined); + await writePromise; +} + +export async function enqueueNativeVideoExportFrameWrites( + session: NativeVideoExportSession, + frameDataList: Array, +) { + const writePromise = session.writeSequence.then(async () => { + if (session.terminating) { + throw new Error("Native video export session was cancelled"); + } + + for (const frameData of frameDataList) { + await writeNativeVideoExportFrame(session, frameData); + } + }); + + session.writeSequence = writePromise.catch(() => undefined); + await writePromise; +} + +export async function getAvailableNativeVideoEncoders(ffmpegPath: string) { + const { stdout } = await execFileAsync(ffmpegPath, ["-hide_banner", "-encoders"], { + timeout: 15000, + maxBuffer: 20 * 1024 * 1024, + }); + + return parseAvailableFfmpegEncoders(stdout); +} + +export async function probeNativeVideoEncoder( + ffmpegPath: string, + encoderName: string, + encodingMode: NativeExportEncodingMode, +) { + const outputPath = path.join( + app.getPath("temp"), + `recordly-export-probe-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mp4`, + ); + const args = buildNativeVideoExportArgs( + encoderName, + { + width: 64, + height: 64, + frameRate: 1, + bitrate: 1_500_000, + encodingMode, + }, + outputPath, + ); + + return new Promise((resolve) => { + const process = spawn(ffmpegPath, args, { + stdio: ["pipe", "ignore", "pipe"], + }); + let stderrOutput = ""; + const timeout = setTimeout(() => { + try { + process.kill("SIGKILL"); + } catch { + // ignore + } + resolve(false); + }, 15000); + + process.stderr.on("data", (chunk: Buffer) => { + stderrOutput += chunk.toString(); + }); + + process.on("close", (code) => { + clearTimeout(timeout); + void removeTemporaryExportFile(outputPath); + if (code !== 0 && stderrOutput.trim().length > 0) { + console.warn( + `[native-export] Encoder probe failed for ${encoderName}:`, + stderrOutput.trim(), + ); + } + resolve(code === 0); + }); + + process.stdin.end(Buffer.alloc(getNativeVideoInputByteSize(64, 64), 0)); + }); +} + +export async function resolveNativeVideoEncoder( + ffmpegPath: string, + encodingMode: NativeExportEncodingMode, +) { + if ( + cachedNativeVideoEncoder?.ffmpegPath === ffmpegPath && + cachedNativeVideoEncoder?.encodingMode === encodingMode + ) { + return cachedNativeVideoEncoder.encoderName; + } + + const availableEncoders = await getAvailableNativeVideoEncoders(ffmpegPath); + const candidates = [ + ...new Set([...getPreferredNativeVideoEncoders(process.platform), "libx264"]), + ]; + + for (const encoderName of candidates) { + if (!availableEncoders.has(encoderName)) { + continue; + } + + if (await probeNativeVideoEncoder(ffmpegPath, encoderName, encodingMode)) { + setCachedNativeVideoEncoder({ ffmpegPath, encodingMode, encoderName }); + return encoderName; + } + } + + throw new Error("No usable FFmpeg encoder was available for native export"); +} + +export function canCopyAudioCodecIntoMp4(codec?: string | null) { + const normalized = (codec ?? "").trim().toLowerCase(); + if (!normalized) { + return false; + } + + return ( + normalized.includes("aac") || + normalized.includes("mp4a") || + normalized.includes("mpeg-4 audio") || + normalized.includes("mp3") || + normalized.includes("alac") + ); +} + +export function buildNativeVideoAudioMuxArgs( + videoPath: string, + audioInputPath: string, + outputPath: string, + options: NativeVideoExportFinishOptions, + argsOptions: NativeVideoAudioMuxArgsOptions = {}, +) { + const audioMode = options.audioMode ?? "none"; + const useEditedTrackFiltergraph = + audioMode === "edited-track" && options.editedTrackStrategy === "filtergraph-fast-path"; + const args = ["-y", "-hide_banner", "-loglevel", "error"]; + if (argsOptions.progressPipe) { + args.push( + "-stats_period", + "0.5", + "-progress", + `pipe:${argsOptions.progressPipe}`, + "-nostats", + ); + } + args.push("-i", videoPath, "-i", audioInputPath); + + if (audioMode === "trim-source") { + const filter = buildTrimmedSourceAudioFilter(options.trimSegments ?? []); + if (filter) { + args.push("-filter_complex", filter, "-map", "0:v:0", "-map", "[aout]"); + } else { + args.push("-map", "0:v:0", "-map", "1:a:0"); + } + } else if (useEditedTrackFiltergraph) { + const filter = buildEditedTrackSourceAudioFilter( + options.editedTrackSegments ?? [], + options.audioSourceSampleRate ?? 0, + ); + if (!filter) { + throw new Error("Edited-track filtergraph inputs are incomplete for native export"); + } + if ( + typeof options.outputDurationSec === "number" && + Number.isFinite(options.outputDurationSec) && + options.outputDurationSec > 0 + ) { + const duration = formatFfmpegSeconds(options.outputDurationSec * 1000); + args.push( + "-filter_complex", + `${filter};[aout]apad,atrim=duration=${duration},asetpts=PTS-STARTPTS[aout_sync]`, + "-map", + "0:v:0", + "-map", + "[aout_sync]", + ); + } else { + args.push("-filter_complex", filter, "-map", "0:v:0", "-map", "[aout]"); + } + } else { + args.push("-map", "0:v:0", "-map", "1:a:0"); + } + + args.push("-c:v", "copy"); + if (audioMode === "copy-source" && canCopyAudioCodecIntoMp4(options.audioSourceCodec)) { + args.push("-c:a", "copy"); + } else { + args.push("-c:a", "aac", "-b:a", "192k"); + } + if ( + typeof options.outputDurationSec === "number" && + Number.isFinite(options.outputDurationSec) && + options.outputDurationSec > 0 + ) { + args.push("-t", formatFfmpegSeconds(options.outputDurationSec * 1000)); + } else if (audioMode !== "copy-source") { + args.push("-shortest"); + } + args.push("-movflags", "+faststart", outputPath); + + return args; +} + +export async function muxNativeVideoExportAudio( + videoPath: string, + options: NativeVideoExportFinishOptions, + onProgress?: (progress: NativeVideoAudioMuxProgress) => void, + session?: NativeStaticLayoutExportSession, +) { + const audioMode = options.audioMode ?? "none"; + if (audioMode === "none") { + return { + outputPath: videoPath, + metrics: {} as NativeVideoAudioMuxMetrics, + }; + } + + const ffmpegPath = getFfmpegBinaryPath(); + const metrics: NativeVideoAudioMuxMetrics = {}; + const tempArtifacts: string[] = []; + let audioInputPath = options.audioSourcePath ?? null; + const useEditedTrackFiltergraph = + audioMode === "edited-track" && options.editedTrackStrategy === "filtergraph-fast-path"; + + if (audioMode === "edited-track" && !useEditedTrackFiltergraph) { + if (!options.editedAudioData) { + throw new Error("Edited audio data is missing for native export"); + } + + const extension = getEditedAudioExtension(options.editedAudioMimeType); + audioInputPath = path.join( + app.getPath("temp"), + `recordly-export-audio-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${extension}`, + ); + const tempAudioWriteStartedAt = getNowMs(); + await fs.writeFile(audioInputPath, Buffer.from(options.editedAudioData)); + metrics.tempEditedAudioWriteMs = getNowMs() - tempAudioWriteStartedAt; + metrics.tempEditedAudioBytes = options.editedAudioData.byteLength; + tempArtifacts.push(audioInputPath); + } + + if (!audioInputPath) { + return { + outputPath: videoPath, + metrics, + }; + } + + const outputPath = path.join( + path.dirname(videoPath), + `${path.basename(videoPath, path.extname(videoPath))}-final.mp4`, + ); + + const args = buildNativeVideoAudioMuxArgs( + videoPath, + audioInputPath, + outputPath, + options, + onProgress ? { progressPipe: 2 } : {}, + ); + + try { + const ffmpegExecStartedAt = getNowMs(); + await runFfmpegAudioMux(ffmpegPath, args, 15 * 60 * 1000, options, onProgress, session); + metrics.ffmpegExecMs = getNowMs() - ffmpegExecStartedAt; + console.info("[native-video-export] Audio mux completed", { + ffmpegExecMs: metrics.ffmpegExecMs, + audioMode: options.audioMode, + tempVideoBytes: metrics.tempVideoBytes, + muxedVideoBytes: metrics.muxedVideoBytes, + }); + await removeTemporaryExportFile(videoPath); + return { + outputPath, + metrics, + }; + } finally { + await Promise.allSettled( + tempArtifacts.map((artifactPath) => removeTemporaryExportFile(artifactPath)), + ); + } +} + +export async function muxExportedVideoAudioBuffer( + videoData: ArrayBuffer, + options: NativeVideoExportFinishOptions, +) { + const tempVideoPath = path.join( + app.getPath("temp"), + `recordly-export-video-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mp4`, + ); + const metrics: NativeVideoAudioMuxMetrics = {}; + let succeeded = false; + let outputPath = tempVideoPath; + + try { + const tempVideoWriteStartedAt = getNowMs(); + await fs.writeFile(tempVideoPath, Buffer.from(videoData)); + metrics.tempVideoWriteMs = getNowMs() - tempVideoWriteStartedAt; + metrics.tempVideoBytes = videoData.byteLength; + const finalized = await muxNativeVideoExportAudio(tempVideoPath, options); + Object.assign(metrics, finalized.metrics); + outputPath = finalized.outputPath; + // Record byte size via stat instead of reading the whole file into a + // Buffer — fs.readFile throws ERR_FS_FILE_TOO_LARGE on >2 GiB outputs. + try { + const stat = await fs.stat(outputPath); + metrics.muxedVideoBytes = stat.size; + } catch { + // Stat failures are non-fatal; size is purely metric data. + } + succeeded = true; + return { + outputPath, + metrics, + }; + } finally { + // Always remove the unmuxed intermediate when the muxer wrote a separate + // file. Only remove the muxed output on failure — on success the caller + // owns it and is responsible for moving/deleting it. + const cleanupTargets: string[] = []; + if (outputPath !== tempVideoPath) { + cleanupTargets.push(tempVideoPath); + } + if (!succeeded) { + cleanupTargets.push(outputPath); + } + if (cleanupTargets.length > 0) { + await Promise.allSettled( + cleanupTargets.map((target) => removeTemporaryExportFile(target)), + ); + } + } +} diff --git a/electron/ipc/export/nativeStaticLayoutRoutePlan.test.ts b/electron/ipc/export/nativeStaticLayoutRoutePlan.test.ts new file mode 100644 index 000000000..36bcaf064 --- /dev/null +++ b/electron/ipc/export/nativeStaticLayoutRoutePlan.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it } from "vitest"; +import { + type NativeStaticLayoutRouteSource, + type NvidiaCudaExportCapabilityProbe, + planNativeStaticLayoutRoutes, + type WindowsD3D11ExportCapabilityProbe, +} from "./nativeStaticLayoutRoutePlan"; + +const source: NativeStaticLayoutRouteSource = { + inputCodec: "h264", + proxyCodec: "h264", + proxyCreated: false, +}; + +const cudaProbe: NvidiaCudaExportCapabilityProbe = { + platform: "win32", + appPackaged: true, + explicitEnabled: false, + explicitDisabled: false, + packagedAutoCandidateEnabled: true, + packagedAutoCandidateActive: true, + windowsGpuCompositorEnabled: true, + wrapperPath: "C:\\Recordly\\run-mp4-pipeline.mjs", + hasNvidiaGpu: true, + audioMode: "copy-source", + audioSkipReason: null, + stallTimeoutMs: 120_000, + skipReason: null, +}; + +const d3d11Probe: WindowsD3D11ExportCapabilityProbe = { + platform: "win32", + windowsGpuCompositorEnabled: true, + helperPath: "C:\\Recordly\\recordly-gpu-export.exe", + adapterIndexOverride: null, + preferHighPerformanceAdapter: true, + nvencSdkRequested: false, + skipReason: null, +}; + +describe("planNativeStaticLayoutRoutes", () => { + it("selects CUDA first and records D3D11 plus FFmpeg fallbacks", () => { + expect( + planNativeStaticLayoutRoutes({ + cuda: cudaProbe, + d3d11: d3d11Probe, + source, + }), + ).toEqual({ + selectedRoute: "nvidia-cuda-compositor", + decisions: [ + { + route: "nvidia-cuda-compositor", + status: "selected", + reasons: ["cuda-wrapper-and-nvidia-gpu-available"], + }, + { + route: "windows-d3d11-compositor", + status: "fallback", + reasons: ["documented-fallback-if-cuda-runtime-fails"], + }, + { + route: "ffmpeg-static-layout", + status: "fallback", + reasons: ["native-gpu-runtime-fallback"], + }, + ], + cuda: cudaProbe, + d3d11: d3d11Probe, + source, + }); + }); + + it("selects D3D11 when CUDA is unavailable", () => { + const cuda = { + ...cudaProbe, + hasNvidiaGpu: false, + skipReason: "nvidia-gpu-unavailable", + } satisfies NvidiaCudaExportCapabilityProbe; + + expect( + planNativeStaticLayoutRoutes({ + cuda, + d3d11: d3d11Probe, + source, + }), + ).toMatchObject({ + selectedRoute: "windows-d3d11-compositor", + decisions: [ + { + route: "nvidia-cuda-compositor", + status: "rejected", + reasons: ["nvidia-gpu-unavailable"], + }, + { + route: "windows-d3d11-compositor", + status: "selected", + reasons: ["documented-fallback-after-cuda-skip:nvidia-gpu-unavailable"], + }, + { + route: "ffmpeg-static-layout", + status: "fallback", + reasons: ["windows-d3d11-runtime-fallback"], + }, + ], + }); + }); + + it("selects FFmpeg when both native GPU routes are unavailable", () => { + const cuda = { + ...cudaProbe, + wrapperPath: null, + skipReason: "cuda-wrapper-unavailable", + } satisfies NvidiaCudaExportCapabilityProbe; + const d3d11 = { + ...d3d11Probe, + helperPath: null, + skipReason: "windows-gpu-helper-unavailable", + } satisfies WindowsD3D11ExportCapabilityProbe; + + expect( + planNativeStaticLayoutRoutes({ + cuda, + d3d11, + source, + }), + ).toEqual({ + selectedRoute: "ffmpeg-static-layout", + decisions: [ + { + route: "nvidia-cuda-compositor", + status: "rejected", + reasons: ["cuda-wrapper-unavailable"], + }, + { + route: "windows-d3d11-compositor", + status: "rejected", + reasons: ["windows-gpu-helper-unavailable"], + }, + { + route: "ffmpeg-static-layout", + status: "selected", + reasons: ["native-gpu-routes-unavailable"], + }, + ], + cuda, + d3d11, + source, + }); + }); + + it("preserves proxy source metadata for route diagnostics", () => { + const proxiedSource = { + inputCodec: "vp9", + proxyCodec: "h264", + proxyCreated: true, + }; + + expect( + planNativeStaticLayoutRoutes({ + cuda: cudaProbe, + d3d11: d3d11Probe, + source: proxiedSource, + }).source, + ).toEqual(proxiedSource); + }); +}); diff --git a/electron/ipc/export/nativeStaticLayoutRoutePlan.ts b/electron/ipc/export/nativeStaticLayoutRoutePlan.ts new file mode 100644 index 000000000..3ee6c4e73 --- /dev/null +++ b/electron/ipc/export/nativeStaticLayoutRoutePlan.ts @@ -0,0 +1,132 @@ +import type { NativeVideoExportAudioMode } from "../nativeVideoExport"; + +export type NativeStaticLayoutRoute = + | "nvidia-cuda-compositor" + | "windows-d3d11-compositor" + | "ffmpeg-static-layout"; + +export interface NativeStaticLayoutRouteDecision { + route: NativeStaticLayoutRoute; + status: "selected" | "fallback" | "rejected"; + reasons: string[]; +} + +export interface NvidiaCudaExportCapabilityProbe { + platform: NodeJS.Platform; + appPackaged: boolean; + explicitEnabled: boolean; + explicitDisabled: boolean; + packagedAutoCandidateEnabled: boolean; + packagedAutoCandidateActive: boolean; + windowsGpuCompositorEnabled: boolean; + wrapperPath: string | null; + hasNvidiaGpu: boolean | null; + audioMode: NativeVideoExportAudioMode; + audioSkipReason: string | null; + stallTimeoutMs: number | null; + skipReason: string | null; +} + +export interface WindowsD3D11ExportCapabilityProbe { + platform: NodeJS.Platform; + windowsGpuCompositorEnabled: boolean; + helperPath: string | null; + adapterIndexOverride: number | null; + preferHighPerformanceAdapter: boolean; + nvencSdkRequested: boolean; + skipReason: string | null; +} + +export interface NativeStaticLayoutRouteSource { + inputCodec: string; + proxyCodec?: string; + proxyCreated: boolean; +} + +export interface NativeStaticLayoutRoutePlan { + selectedRoute: NativeStaticLayoutRoute; + decisions: NativeStaticLayoutRouteDecision[]; + cuda: NvidiaCudaExportCapabilityProbe; + d3d11: WindowsD3D11ExportCapabilityProbe; + source: NativeStaticLayoutRouteSource; +} + +export function planNativeStaticLayoutRoutes(options: { + cuda: NvidiaCudaExportCapabilityProbe; + d3d11: WindowsD3D11ExportCapabilityProbe; + source: NativeStaticLayoutRouteSource; +}): NativeStaticLayoutRoutePlan { + const { cuda, d3d11, source } = options; + const decisions: NativeStaticLayoutRouteDecision[] = []; + + if (!cuda.skipReason) { + decisions.push({ + route: "nvidia-cuda-compositor", + status: "selected", + reasons: ["cuda-wrapper-and-nvidia-gpu-available"], + }); + decisions.push({ + route: "windows-d3d11-compositor", + status: d3d11.skipReason ? "rejected" : "fallback", + reasons: d3d11.skipReason + ? [d3d11.skipReason] + : ["documented-fallback-if-cuda-runtime-fails"], + }); + decisions.push({ + route: "ffmpeg-static-layout", + status: "fallback", + reasons: ["native-gpu-runtime-fallback"], + }); + return { + selectedRoute: "nvidia-cuda-compositor", + decisions, + cuda, + d3d11, + source, + }; + } + + decisions.push({ + route: "nvidia-cuda-compositor", + status: "rejected", + reasons: [cuda.skipReason], + }); + + if (!d3d11.skipReason) { + decisions.push({ + route: "windows-d3d11-compositor", + status: "selected", + reasons: [`documented-fallback-after-cuda-skip:${cuda.skipReason}`], + }); + decisions.push({ + route: "ffmpeg-static-layout", + status: "fallback", + reasons: ["windows-d3d11-runtime-fallback"], + }); + return { + selectedRoute: "windows-d3d11-compositor", + decisions, + cuda, + d3d11, + source, + }; + } + + decisions.push({ + route: "windows-d3d11-compositor", + status: "rejected", + reasons: [d3d11.skipReason], + }); + decisions.push({ + route: "ffmpeg-static-layout", + status: "selected", + reasons: ["native-gpu-routes-unavailable"], + }); + return { + selectedRoute: "ffmpeg-static-layout", + decisions, + cuda, + d3d11, + source, + }; +} diff --git a/electron/ipc/ffmpeg/binary.ts b/electron/ipc/ffmpeg/binary.ts new file mode 100644 index 000000000..a0cf8468c --- /dev/null +++ b/electron/ipc/ffmpeg/binary.ts @@ -0,0 +1,161 @@ +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { app } from "electron"; + +const nodeRequire = createRequire(import.meta.url); + +export function loadFfmpegStatic(): string | null { + try { + const moduleExports = nodeRequire("ffmpeg-static"); + if (typeof moduleExports === "string") { + return moduleExports; + } + + if (typeof moduleExports?.default === "string") { + return moduleExports.default as string; + } + } catch { + // ffmpeg-static not available; fall through to system FFmpeg + } + + return null; +} + +export function loadFfprobeStatic(): string | null { + try { + const moduleExports = nodeRequire("ffprobe-static"); + if (typeof moduleExports === "string") { + return moduleExports; + } + + if (typeof moduleExports?.path === "string") { + return moduleExports.path as string; + } + + if (typeof moduleExports?.default === "string") { + return moduleExports.default as string; + } + + if (typeof moduleExports?.default?.path === "string") { + return moduleExports.default.path as string; + } + } catch { + // ffprobe-static not available; fall through to system FFprobe + } + + return null; +} + +export function resolveSystemFfmpegBinaryPath(): string | null { + const locator = process.platform === "win32" ? "where" : "which"; + const result = spawnSync(locator, ["ffmpeg"], { + encoding: "utf-8", + windowsHide: true, + }); + + if (result.status === 0) { + const candidate = result.stdout + .split(/\r?\n/) + .map((line: string) => line.trim()) + .find((line: string) => line.length > 0); + + if (candidate) { + return candidate; + } + } + + // Fallback: check common install paths directly (Electron's shell may lack full PATH) + if (process.platform !== "win32") { + const commonPaths = [ + "/opt/homebrew/bin/ffmpeg", + "/usr/local/bin/ffmpeg", + "/usr/bin/ffmpeg", + ]; + for (const p of commonPaths) { + if (existsSync(p)) { + return p; + } + } + } + + return null; +} + +export function resolveSystemFfprobeBinaryPath(): string | null { + const locator = process.platform === "win32" ? "where" : "which"; + const result = spawnSync(locator, ["ffprobe"], { + encoding: "utf-8", + windowsHide: true, + }); + + if (result.status === 0) { + const candidate = result.stdout + .split(/\r?\n/) + .map((line: string) => line.trim()) + .find((line: string) => line.length > 0); + + if (candidate) { + return candidate; + } + } + + if (process.platform !== "win32") { + const commonPaths = [ + "/opt/homebrew/bin/ffprobe", + "/usr/local/bin/ffprobe", + "/usr/bin/ffprobe", + ]; + for (const p of commonPaths) { + if (existsSync(p)) { + return p; + } + } + } + + return null; +} + +export function getFfmpegBinaryPath(): string { + const ffmpegStatic = loadFfmpegStatic(); + if (ffmpegStatic && typeof ffmpegStatic === "string") { + const bundledPath = app.isPackaged + ? ffmpegStatic.replace(/\.asar([/\\])/, ".asar.unpacked$1") + : ffmpegStatic; + + if (existsSync(bundledPath)) { + return bundledPath; + } + } + + const systemFfmpeg = resolveSystemFfmpegBinaryPath(); + if (systemFfmpeg) { + return systemFfmpeg; + } + + throw new Error( + "FFmpeg binary is unavailable. Install ffmpeg-static for this platform or make ffmpeg available on PATH.", + ); +} + +export function getFfprobeBinaryPath(): string { + const ffprobeStatic = loadFfprobeStatic(); + if (ffprobeStatic && typeof ffprobeStatic === "string") { + const bundledPath = app.isPackaged + ? ffprobeStatic.replace(/\.asar([/\\])/, ".asar.unpacked$1") + : ffprobeStatic; + + if (existsSync(bundledPath)) { + return bundledPath; + } + } + + const systemFfprobe = resolveSystemFfprobeBinaryPath(); + if (systemFfprobe) { + return systemFfprobe; + } + + throw new Error( + "FFprobe binary is unavailable. Install ffprobe-static for this platform or make ffprobe available on PATH.", + ); +} diff --git a/electron/ipc/ffmpeg/filters.test.ts b/electron/ipc/ffmpeg/filters.test.ts new file mode 100644 index 000000000..8b8ecfdb5 --- /dev/null +++ b/electron/ipc/ffmpeg/filters.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, it } from "vitest"; + +import { + appendSyncedAudioFilter, + applyRecordedAudioStartDelay, + getAudioSyncAdjustment, +} from "./filters"; + +describe("getAudioSyncAdjustment", () => { + it("does not speed up longer audio tracks that would advance speech", () => { + expect(getAudioSyncAdjustment(120, 122.5)).toEqual({ + mode: "none", + delayMs: 0, + tempoRatio: 1, + durationDeltaMs: -2500, + }); + }); + + it("still stretches slightly shorter audio tracks to match the video", () => { + expect(getAudioSyncAdjustment(120, 117)).toEqual({ + mode: "tempo", + delayMs: 0, + tempoRatio: 0.975, + durationDeltaMs: 3000, + }); + }); + + it("still delays much shorter audio tracks instead of extreme tempo correction", () => { + expect(getAudioSyncAdjustment(120, 110)).toEqual({ + mode: "delay", + delayMs: 10000, + tempoRatio: 1, + durationDeltaMs: 10000, + }); + }); + + it("pads trailing silence instead of prepending extreme delay for very short audio tracks", () => { + expect(getAudioSyncAdjustment(600, 480)).toEqual({ + mode: "pad", + delayMs: 0, + tempoRatio: 1, + durationDeltaMs: 120000, + }); + }); + + it("does not inject atempo when longer audio stays on the anchored path", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(120, 122.5)); + + expect(filterParts).toEqual([ + "[1:a]aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("still injects atempo for slightly shorter audio tracks", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(120, 117)); + + expect(filterParts).toEqual([ + "[1:a]atempo=0.975000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("pads the tail for very short audio tracks", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(600, 480)); + + expect(filterParts).toEqual([ + "[1:a]apad=pad_dur=120.000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("pads the remaining tail after a measured late start delay", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", { + mode: "delay", + delayMs: 18051, + tempoRatio: 1, + durationDeltaMs: 1631070, + }); + + expect(filterParts).toEqual([ + "[1:a]adelay=18051|18051,apad=pad_dur=1613.019,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("does not pad when a measured late start already explains the duration gap", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", { + mode: "delay", + delayMs: 10000, + tempoRatio: 1, + durationDeltaMs: 10000, + }); + + expect(filterParts).toEqual([ + "[1:a]adelay=10000|10000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("can add a small gain boost before resampling", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter( + filterParts, + "[1:a]", + "aout", + getAudioSyncAdjustment(120, 120), + 1.4, + ); + + expect(filterParts).toEqual([ + "[1:a]volume=1.400,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("can prepend mic normalization filters before sync handling", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(120, 120), { + preFilters: ["adeclip=threshold=1", "loudnorm=I=-16:TP=-1.5:LRA=11"], + }); + + expect(filterParts).toEqual([ + "[1:a]adeclip=threshold=1,loudnorm=I=-16:TP=-1.5:LRA=11,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); +}); + +describe("applyRecordedAudioStartDelay", () => { + it("pads the tail when recorded metadata says audio started on time", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 110), 0)).toEqual({ + mode: "pad", + delayMs: 0, + tempoRatio: 1, + durationDeltaMs: 10000, + }); + }); + + it("prefers a measured start delay over a tempo-only heuristic", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 119.8), 275)).toEqual({ + mode: "delay", + delayMs: 275, + tempoRatio: 1, + durationDeltaMs: 200, + }); + }); + + it("applies a measured start delay even when durations already match", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 120), 275)).toEqual({ + mode: "delay", + delayMs: 275, + tempoRatio: 1, + durationDeltaMs: 0, + }); + }); + + it("leaves tempo correction alone when recorded metadata says there was no late start", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 117), 0)).toEqual({ + mode: "tempo", + delayMs: 0, + tempoRatio: 0.975, + durationDeltaMs: 3000, + }); + }); +}); diff --git a/electron/ipc/ffmpeg/filters.ts b/electron/ipc/ffmpeg/filters.ts new file mode 100644 index 000000000..2d3370c7f --- /dev/null +++ b/electron/ipc/ffmpeg/filters.ts @@ -0,0 +1,258 @@ +import type { AudioSyncAdjustment, PauseSegment } from "../types"; + +const MAX_AUDIO_SYNC_DELAY_MS = 15000; +export const ATEMPO_FILTER_EPSILON = 0.0005; + +export function buildAtempoFilters(tempoRatio: number): string[] { + if (!Number.isFinite(tempoRatio) || tempoRatio <= 0) { + return []; + } + + const filters: string[] = []; + let remaining = tempoRatio; + + while (remaining < 0.5) { + filters.push("atempo=0.5"); + remaining /= 0.5; + } + + while (remaining > 2) { + filters.push("atempo=2.0"); + remaining /= 2.0; + } + + if (Math.abs(remaining - 1) > ATEMPO_FILTER_EPSILON) { + filters.push(`atempo=${remaining.toFixed(6)}`); + } + + return filters; +} + +export function getAudioSyncAdjustment( + videoDuration: number, + audioDuration: number, +): AudioSyncAdjustment { + if ( + !Number.isFinite(videoDuration) || + !Number.isFinite(audioDuration) || + videoDuration <= 0 || + audioDuration <= 0 + ) { + return { mode: "none", delayMs: 0, tempoRatio: 1, durationDeltaMs: 0 }; + } + + const durationDeltaMs = Math.round((videoDuration - audioDuration) * 1000); + const absDeltaMs = Math.abs(durationDeltaMs); + if (absDeltaMs <= 20) { + return { mode: "none", delayMs: 0, tempoRatio: 1, durationDeltaMs }; + } + + // When the recorded audio runs longer than the video, globally speeding it + // up can pull speech ahead of the picture. Keep the track anchored at the + // start instead and let the downstream mux path trim any trailing overrun. + if (durationDeltaMs < 0) { + return { mode: "none", delayMs: 0, tempoRatio: 1, durationDeltaMs }; + } + + const tempoRatio = Math.max(0.5, Math.min(2, audioDuration / videoDuration)); + const relativeDelta = absDeltaMs / Math.max(videoDuration * 1000, 1); + + if (relativeDelta <= 0.03 || absDeltaMs <= 1500) { + return { mode: "tempo", delayMs: 0, tempoRatio, durationDeltaMs }; + } + + if (durationDeltaMs > MAX_AUDIO_SYNC_DELAY_MS) { + return { mode: "pad", delayMs: 0, tempoRatio: 1, durationDeltaMs }; + } + + return { mode: "delay", delayMs: durationDeltaMs, tempoRatio: 1, durationDeltaMs }; +} + +export function applyRecordedAudioStartDelay( + adjustment: AudioSyncAdjustment, + recordedStartDelayMs?: number | null, +): AudioSyncAdjustment { + if (!Number.isFinite(recordedStartDelayMs) || (recordedStartDelayMs ?? 0) < 0) { + return adjustment; + } + + const delayMs = Math.max(0, Math.round(recordedStartDelayMs ?? 0)); + if (delayMs > 20) { + return { + mode: "delay", + delayMs, + tempoRatio: 1, + durationDeltaMs: adjustment.durationDeltaMs, + }; + } + + if (adjustment.mode !== "delay" && adjustment.mode !== "pad") { + return adjustment; + } + + return { + mode: "pad", + delayMs: 0, + tempoRatio: 1, + durationDeltaMs: adjustment.durationDeltaMs, + }; +} + +export function appendSyncedAudioFilter( + filterParts: string[], + inputLabel: string, + outputLabel: string, + adjustment: AudioSyncAdjustment, + options: number | { volumeMultiplier?: number; preFilters?: string[] } = 1, +) { + const volumeMultiplier = + typeof options === "number" ? options : (options.volumeMultiplier ?? 1); + const preFilters = typeof options === "number" ? [] : (options.preFilters ?? []); + const filters: string[] = [...preFilters]; + + if (adjustment.mode === "delay" && adjustment.delayMs > 0) { + filters.push(`adelay=${adjustment.delayMs}|${adjustment.delayMs}`); + } + + if (adjustment.mode === "delay" && adjustment.durationDeltaMs > adjustment.delayMs + 20) { + filters.push( + `apad=pad_dur=${formatFfmpegSeconds(adjustment.durationDeltaMs - adjustment.delayMs)}`, + ); + } + + if (adjustment.mode === "tempo") { + filters.push(...buildAtempoFilters(adjustment.tempoRatio)); + } + + if (adjustment.mode === "pad" && adjustment.durationDeltaMs > 0) { + filters.push(`apad=pad_dur=${formatFfmpegSeconds(adjustment.durationDeltaMs)}`); + } + + if ( + Number.isFinite(volumeMultiplier) && + volumeMultiplier > 0 && + Math.abs(volumeMultiplier - 1) > 0.0005 + ) { + filters.push(`volume=${volumeMultiplier.toFixed(3)}`); + } + + filters.push("aresample=async=1:first_pts=0", "asetpts=PTS-STARTPTS"); + filterParts.push(`${inputLabel}${filters.join(",")}[${outputLabel}]`); +} + +export function formatFfmpegSeconds(milliseconds: number): string { + return (milliseconds / 1000).toFixed(3); +} + +export function normalizePauseSegments(pauseSegments: PauseSegment[] | undefined): PauseSegment[] { + if (!Array.isArray(pauseSegments) || pauseSegments.length === 0) { + return []; + } + + const normalized = pauseSegments + .map((segment) => { + const startMs = Number(segment?.startMs); + const endMs = Number(segment?.endMs); + + if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) { + return null; + } + + const clampedStart = Math.max(0, Math.round(startMs)); + const clampedEnd = Math.max(0, Math.round(endMs)); + if (clampedEnd <= clampedStart) { + return null; + } + + return { startMs: clampedStart, endMs: clampedEnd }; + }) + .filter((segment): segment is PauseSegment => !!segment) + .sort((left, right) => left.startMs - right.startMs); + + if (normalized.length <= 1) { + return normalized; + } + + const merged: PauseSegment[] = [{ ...normalized[0] }]; + + for (const segment of normalized.slice(1)) { + const previous = merged[merged.length - 1]; + if (segment.startMs <= previous.endMs) { + previous.endMs = Math.max(previous.endMs, segment.endMs); + } else { + merged.push({ ...segment }); + } + } + + return merged; +} + +export function buildPausedAudioFilter( + inputLabel: string, + outputLabel: string, + pauseSegments: PauseSegment[], +): string | null { + if (pauseSegments.length === 0) { + return null; + } + + const activeSegments: Array<{ startMs: number; endMs?: number }> = []; + let cursorMs = 0; + + for (const pauseSegment of pauseSegments) { + if (pauseSegment.startMs > cursorMs) { + activeSegments.push({ startMs: cursorMs, endMs: pauseSegment.startMs }); + } + cursorMs = Math.max(cursorMs, pauseSegment.endMs); + } + + activeSegments.push({ startMs: cursorMs }); + + const filterParts: string[] = []; + const segmentLabels: string[] = []; + + activeSegments.forEach((segment, index) => { + if (typeof segment.endMs === "number" && segment.endMs <= segment.startMs) { + return; + } + + const segmentLabel = `${outputLabel}_part${index}`; + const trimArgs = + typeof segment.endMs === "number" + ? `start=${formatFfmpegSeconds(segment.startMs)}:end=${formatFfmpegSeconds(segment.endMs)}` + : `start=${formatFfmpegSeconds(segment.startMs)}`; + + filterParts.push(`[${inputLabel}]atrim=${trimArgs},asetpts=PTS-STARTPTS[${segmentLabel}]`); + segmentLabels.push(`[${segmentLabel}]`); + }); + + if (segmentLabels.length === 0) { + return null; + } + + if (segmentLabels.length === 1) { + filterParts.push(`${segmentLabels[0]}anull[${outputLabel}]`); + } else { + filterParts.push( + `${segmentLabels.join("")}concat=n=${segmentLabels.length}:v=0:a=1[${outputLabel}]`, + ); + } + + return filterParts.join(";"); +} + +export function parseFfmpegDurationSeconds(stderr: string): number | null { + const match = stderr.match(/Duration:\s+(\d+):(\d+):(\d+(?:\.\d+)?)/i); + if (!match) { + return null; + } + + const hours = Number(match[1]); + const minutes = Number(match[2]); + const seconds = Number(match[3]); + if (![hours, minutes, seconds].every(Number.isFinite)) { + return null; + } + + return hours * 3600 + minutes * 60 + seconds; +} diff --git a/electron/ipc/ffmpeg/metadata.ts b/electron/ipc/ffmpeg/metadata.ts new file mode 100644 index 000000000..50aaf731f --- /dev/null +++ b/electron/ipc/ffmpeg/metadata.ts @@ -0,0 +1,130 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +const execFileAsync = promisify(execFile); + +export interface NativeVideoMetadataProbe { + width: number; + height: number; + duration: number; + mediaStartTime?: number; + streamStartTime?: number; + streamDuration?: number; + frameRate: number; + codec: string; + hasAudio: boolean; + audioCodec?: string; + audioSampleRate?: number; +} + +export function parseFfmpegDurationSeconds(value: string): number | null { + const parts = value.trim().split(":"); + if (parts.length !== 3) { + return null; + } + + const [hours, minutes, seconds] = parts.map(Number); + if (![hours, minutes, seconds].every(Number.isFinite)) { + return null; + } + + return hours * 3600 + minutes * 60 + seconds; +} + +export function parseFfmpegFrameRate(line: string): number | null { + const fpsMatch = line.match(/,\s*([0-9]+(?:\.[0-9]+)?)\s*fps\b/i); + if (fpsMatch) { + const frameRate = Number(fpsMatch[1]); + return Number.isFinite(frameRate) && frameRate > 0 ? frameRate : null; + } + + const tbrMatch = line.match(/,\s*([0-9]+(?:\.[0-9]+)?)\s*tbr\b/i); + if (tbrMatch) { + const frameRate = Number(tbrMatch[1]); + return Number.isFinite(frameRate) && frameRate > 0 ? frameRate : null; + } + + return null; +} + +export function parseNativeVideoMetadataProbeOutput( + output: string, +): NativeVideoMetadataProbe | null { + const durationMatch = output.match( + /Duration:\s*([0-9:.]+),\s*start:\s*(-?[0-9]+(?:\.[0-9]+)?)/i, + ); + const duration = durationMatch ? parseFfmpegDurationSeconds(durationMatch[1]) : null; + if (!duration || duration <= 0) { + return null; + } + + const mediaStartTime = durationMatch ? Number(durationMatch[2]) : 0; + const lines = output.split(/\r?\n/); + const videoLine = lines.find((line) => /\bVideo:\s*/i.test(line)); + if (!videoLine) { + return null; + } + + const dimensionsMatch = videoLine.match(/,\s*([0-9]{2,5})x([0-9]{2,5})(?:[,\s]|$)/); + if (!dimensionsMatch) { + return null; + } + + const width = Number(dimensionsMatch[1]); + const height = Number(dimensionsMatch[2]); + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return null; + } + + const videoCodecMatch = videoLine.match(/Video:\s*([^,\r\n]+)/i); + const videoStartMatch = videoLine.match(/\bstart:\s*(-?[0-9]+(?:\.[0-9]+)?)/i); + const frameRate = parseFfmpegFrameRate(videoLine) ?? 60; + const audioLine = lines.find((line) => /\bAudio:\s*/i.test(line)); + const audioCodecMatch = audioLine?.match(/Audio:\s*([^,\r\n]+)/i); + const audioSampleRateMatch = audioLine?.match(/,\s*([0-9]+)\s*Hz\b/i); + + return { + width, + height, + duration, + mediaStartTime: Number.isFinite(mediaStartTime) ? mediaStartTime : 0, + streamStartTime: videoStartMatch ? Number(videoStartMatch[1]) : mediaStartTime, + streamDuration: duration, + frameRate, + codec: videoCodecMatch?.[1]?.trim() || "unknown", + hasAudio: Boolean(audioLine), + audioCodec: audioCodecMatch?.[1]?.trim(), + audioSampleRate: audioSampleRateMatch ? Number(audioSampleRateMatch[1]) : undefined, + }; +} + +export async function probeNativeVideoMetadata( + ffmpegPath: string, + inputPath: string, + signal?: AbortSignal, +): Promise { + let output = ""; + try { + const result = await execFileAsync(ffmpegPath, ["-hide_banner", "-i", inputPath], { + signal, + timeout: 30_000, + maxBuffer: 4 * 1024 * 1024, + }); + output = `${result.stdout}\n${result.stderr}`; + } catch (error) { + signal?.throwIfAborted(); + const processOutput = error as { stdout?: unknown; stderr?: unknown }; + output = [processOutput.stdout, processOutput.stderr] + .filter((value): value is string => typeof value === "string") + .join("\n"); + if (!output) { + throw error; + } + } + + const metadata = parseNativeVideoMetadataProbeOutput(output); + if (!metadata) { + throw new Error("Unable to parse native video metadata from FFmpeg output"); + } + + return metadata; +} diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 81e8af5ae..c5c8667e7 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -1,181 +1,78 @@ -import { ipcMain, desktopCapturer, BrowserWindow, shell, app } from 'electron' -import { startMouseTracking, stopMouseTracking, getTrackingData } from './mouseTracking' -import fs from 'node:fs/promises' -import path from 'node:path' -import { RECORDINGS_DIR } from '../main' +import { BrowserWindow } from "electron"; +import { registerAnnouncementHandlers } from "./register/announcements"; +import { registerCapturePickerHandlers } from "./register/capturePicker"; +import { registerAssetHandlers } from "./register/assets"; +import { registerCaptionHandlers } from "./register/captions"; +import { registerCloudShareHandlers } from "./register/cloudShare"; +import { registerExportHandlers } from "./register/export"; +import { registerPermissionHandlers } from "./register/permissions"; +import { registerProjectHandlers } from "./register/project"; +import { registerRecordingHandlers } from "./register/recording"; +import { registerSettingsHandlers } from "./register/settings"; +import { registerSourceHandlers } from "./register/sources"; +import { + selectedSource, + setNativeScreenRecordingActive, + setWindowsCapturePaused, + setWindowsCaptureProcess, + setWindowsCaptureStopRequested, + setWindowsCaptureTargetPath, + setWindowsMicAudioPath, + setWindowsNativeCaptureActive, + setWindowsOrphanedMicAudioPath, + setWindowsPendingVideoPath, + setWindowsSystemAudioPath, + windowsCaptureProcess, +} from "./state"; + +export { cleanupAllExportStreams } from "./export/exportStream"; +export { cleanupNativeVideoExportSessions } from "./export/native-video"; + +/** Returns the currently selected source ID for setDisplayMediaRequestHandler */ +export function getSelectedSourceId(): string | null { + return (selectedSource?.id as string | null) ?? null; +} -let selectedSource: any = null +export function killWindowsCaptureProcess() { + if (windowsCaptureProcess) { + try { + windowsCaptureProcess.kill(); + } catch { + /* ignore */ + } + setWindowsCaptureProcess(null); + setWindowsCaptureTargetPath(null); + setWindowsNativeCaptureActive(false); + setNativeScreenRecordingActive(false); + setWindowsCaptureStopRequested(false); + setWindowsCapturePaused(false); + setWindowsSystemAudioPath(null); + setWindowsMicAudioPath(null); + setWindowsOrphanedMicAudioPath(null); + setWindowsPendingVideoPath(null); + } +} export function registerIpcHandlers( - createEditorWindow: () => void, - createSourceSelectorWindow: () => BrowserWindow, - getMainWindow: () => BrowserWindow | null, - getSourceSelectorWindow: () => BrowserWindow | null, - onRecordingStateChange?: (recording: boolean, sourceName: string) => void + createEditorWindow: () => void, + createSourceSelectorWindow: () => BrowserWindow, + _getMainWindow: () => BrowserWindow | null, + getSourceSelectorWindow: () => BrowserWindow | null, + onRecordingStateChange?: (recording: boolean, sourceName: string) => void, ) { - ipcMain.handle('get-sources', async (_, opts) => { - const sources = await desktopCapturer.getSources(opts) - return sources.map(source => ({ - id: source.id, - name: source.name, - display_id: source.display_id, - thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, - appIcon: source.appIcon ? source.appIcon.toDataURL() : null - })) - }) - - ipcMain.handle('select-source', (_, source) => { - selectedSource = source - const sourceSelectorWin = getSourceSelectorWindow() - if (sourceSelectorWin) { - sourceSelectorWin.close() - } - return selectedSource - }) - - ipcMain.handle('get-selected-source', () => { - return selectedSource - }) - - ipcMain.handle('open-source-selector', () => { - const sourceSelectorWin = getSourceSelectorWindow() - if (sourceSelectorWin) { - sourceSelectorWin.focus() - return - } - createSourceSelectorWindow() - }) - - ipcMain.handle('switch-to-editor', () => { - const mainWin = getMainWindow() - if (mainWin) { - mainWin.close() - } - createEditorWindow() - }) - - ipcMain.handle('start-mouse-tracking', () => { - return startMouseTracking() - }) - - ipcMain.handle('stop-mouse-tracking', () => { - return stopMouseTracking() - }) - - ipcMain.handle('store-recorded-video', async (_, videoData: ArrayBuffer, fileName: string) => { - try { - const videoPath = path.join(RECORDINGS_DIR, fileName) - await fs.writeFile(videoPath, Buffer.from(videoData)) - - return { - success: true, - path: videoPath, - message: 'Video stored successfully' - } - } catch (error) { - console.error('Failed to store video:', error) - return { - success: false, - message: 'Failed to store video', - error: String(error) - } - } - }) - - ipcMain.handle('store-mouse-tracking-data', async (_, fileName: string) => { - try { - const data = getTrackingData() - - if (data.length === 0) { - return { success: false, message: 'No tracking data to save' } - } - - const trackingPath = path.join(RECORDINGS_DIR, fileName) - await fs.writeFile(trackingPath, JSON.stringify(data, null, 2), 'utf-8') - - return { - success: true, - path: trackingPath, - eventCount: data.length, - message: 'Mouse tracking data stored successfully' - } - } catch (error) { - console.error('Failed to store mouse tracking data:', error) - return { - success: false, - message: 'Failed to store mouse tracking data', - error: String(error) - } - } - }) - - ipcMain.handle('get-recorded-video-path', async () => { - try { - const files = await fs.readdir(RECORDINGS_DIR) - const videoFiles = files.filter(file => file.endsWith('.webm')) - - if (videoFiles.length === 0) { - return { success: false, message: 'No recorded video found' } - } - - const latestVideo = videoFiles.sort().reverse()[0] - const videoPath = path.join(RECORDINGS_DIR, latestVideo) - - return { success: true, path: videoPath } - } catch (error) { - console.error('Failed to get video path:', error) - return { success: false, message: 'Failed to get video path', error: String(error) } - } - }) - - ipcMain.handle('set-recording-state', (_, recording: boolean) => { - const source = selectedSource || { name: 'Screen' } - if (onRecordingStateChange) { - onRecordingStateChange(recording, source.name) - } - }) - - ipcMain.handle('open-external-url', async (_, url: string) => { - try { - await shell.openExternal(url) - return { success: true } - } catch (error) { - console.error('Failed to open URL:', error) - return { success: false, error: String(error) } - } - }) - - // Return base path for assets so renderer can resolve file:// paths in production - ipcMain.handle('get-asset-base-path', () => { - try { - if (app.isPackaged) { - return path.join(process.resourcesPath, 'assets') - } - return path.join(app.getAppPath(), 'public', 'assets') - } catch (err) { - console.error('Failed to resolve asset base path:', err) - return null - } - }) - - ipcMain.handle('save-exported-video', async (_, videoData: ArrayBuffer, fileName: string) => { - try { - const downloadsPath = app.getPath('downloads') - const videoPath = path.join(downloadsPath, fileName) - await fs.writeFile(videoPath, Buffer.from(videoData)) - - return { - success: true, - path: videoPath, - message: 'Video exported successfully' - } - } catch (error) { - console.error('Failed to save exported video:', error) - return { - success: false, - message: 'Failed to save exported video', - error: String(error) - } - } - }) + const { pickSourceList } = registerSourceHandlers({ + createEditorWindow, + createSourceSelectorWindow, + getSourceSelectorWindow, + }); + registerCapturePickerHandlers({ pickSourceList }); + registerRecordingHandlers(onRecordingStateChange); + registerPermissionHandlers(); + registerAnnouncementHandlers(); + registerAssetHandlers(); + registerExportHandlers(); + registerCaptionHandlers(); + registerCloudShareHandlers(); + registerProjectHandlers(); + registerSettingsHandlers(); } diff --git a/electron/ipc/monitorResolver.ts b/electron/ipc/monitorResolver.ts new file mode 100644 index 000000000..f14bbd38c --- /dev/null +++ b/electron/ipc/monitorResolver.ts @@ -0,0 +1,78 @@ +import { spawnSync } from "node:child_process"; + +/** + * Represents a Windows monitor handle and its physical desktop coordinates. + */ +export interface WinMonitorHandle { + handle: number; + x: number; + y: number; + width: number; + height: number; +} + +/** + * Retrieves raw HMONITOR handles from the Windows OS using a PowerShell bridge. + * This is necessary because Electron's display IDs are often internal hashes that + * cannot be used directly with native Windows APIs like Graphics Capture (WGC). + */ +export function getMonitorHandles(): WinMonitorHandle[] { + if (process.platform !== "win32") return []; + + // PowerShell snippet that uses P/Invoke to call EnumDisplayMonitors and return raw handles + bounds. + const psScript = ` +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +using System.Collections.Generic; + +public class MonitorHelper { + [DllImport("user32.dll")] + public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); + + public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); + + [StructLayout(LayoutKind.Sequential)] + public struct Rect { + public int left; + public int top; + public int right; + public int bottom; + } + + public static List GetMonitors() { + List result = new List(); + EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData) => { + result.Add(string.Format("{0}|{1}|{2}|{3}|{4}", hMonitor.ToInt64(), lprcMonitor.left, lprcMonitor.top, lprcMonitor.right - lprcMonitor.left, lprcMonitor.bottom - lprcMonitor.top)); + return true; + }, IntPtr.Zero); + return result; + } +} +"@ +[MonitorHelper]::GetMonitors() +`.trim(); + + const result = spawnSync( + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-Command", psScript], + { + encoding: "utf-8", + timeout: 5000, + }, + ); + + if (result.error || result.status !== 0) { + // Silent failure is preferred; the caller will fall back to coordinate-based matching. + return []; + } + + return result.stdout + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + const [handle, x, y, width, height] = line.split("|").map(Number); + return { handle, x, y, width, height }; + }); +} diff --git a/electron/ipc/mouseTracking.ts b/electron/ipc/mouseTracking.ts deleted file mode 100644 index 47d317174..000000000 --- a/electron/ipc/mouseTracking.ts +++ /dev/null @@ -1,155 +0,0 @@ -// uiohook-napi [WIP: scoping out mouse tracking with cross platform support for potential auto zoom/ post processing cursor effects] -// not currently being used. - -import { uIOhook } from 'uiohook-napi' - -let isMouseTrackingActive = false -let isHookStarted = false -let recordingStartTime: number = 0 -let mouseEventData: MouseEvent[] = [] - -export interface MouseEvent { - type: 'move' | 'down' | 'up' | 'click' - timestamp: number // milliseconds since recording started - x: number - y: number - button?: unknown - clicks?: number -} - -export interface MouseTrackingSession { - startTime: number - events: MouseEvent[] - duration: number -} - -export function startMouseTracking() { - if (isMouseTrackingActive) { - return { success: false, message: 'Already tracking' } - } - - isMouseTrackingActive = true - - // Reset data for new recording session - recordingStartTime = performance.now() - mouseEventData = [] - - // Only start the hook once - if (!isHookStarted) { - setupMouseEventListeners() - - try { - uIOhook.start() - isHookStarted = true - return { success: true, message: 'Mouse tracking started', startTime: recordingStartTime } - } catch (error) { - console.error('Failed to start mouse tracking:', error) - isMouseTrackingActive = false - return { success: false, message: 'Failed to start hook', error } - } - } else { - return { success: true, message: 'Mouse tracking resumed', startTime: recordingStartTime } - } -} - -export function stopMouseTracking(): { success: boolean; message: string; data?: MouseTrackingSession } { - if (!isMouseTrackingActive) { - return { success: false, message: 'Not currently tracking' } - } - - isMouseTrackingActive = false - - const duration = performance.now() - recordingStartTime - - const session: MouseTrackingSession = { - startTime: recordingStartTime, - events: mouseEventData, - duration: duration - } - - return { - success: true, - message: 'Mouse tracking stopped', - data: session - } -} - -function setupMouseEventListeners() { - // Track mouse movement - uIOhook.on('mousemove', (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime - const event: MouseEvent = { - type: 'move', - timestamp, - x: e.x, - y: e.y - } - mouseEventData.push(event) - } - }) - - // Track mouse button press - uIOhook.on('mousedown', (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime - const event: MouseEvent = { - type: 'down', - timestamp, - x: e.x, - y: e.y, - button: e.button, - clicks: e.clicks - } - mouseEventData.push(event) - } - }) - - // Track mouse button release - uIOhook.on('mouseup', (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime - const event: MouseEvent = { - type: 'up', - timestamp, - x: e.x, - y: e.y, - button: e.button - } - mouseEventData.push(event) - } - }) - - // Track complete click events - uIOhook.on('click', (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime - const event: MouseEvent = { - type: 'click', - timestamp, - x: e.x, - y: e.y, - button: e.button, - clicks: e.clicks - } - mouseEventData.push(event) - } - }) -} - -export function getTrackingData(): MouseEvent[] { - return [...mouseEventData] -} - -export function cleanupMouseTracking() { - if (isHookStarted) { - try { - uIOhook.stop() - isHookStarted = false - isMouseTrackingActive = false - mouseEventData = [] - } catch (error) { - console.error('Error cleaning up mouse tracking:', error) - } - } -} diff --git a/electron/ipc/nativeVideoExport.test.ts b/electron/ipc/nativeVideoExport.test.ts new file mode 100644 index 000000000..47a9a273e --- /dev/null +++ b/electron/ipc/nativeVideoExport.test.ts @@ -0,0 +1,325 @@ +import { describe, expect, it } from "vitest"; +import { ATEMPO_FILTER_EPSILON } from "./ffmpeg/filters"; +import { + buildEditedTrackSourceAudioFilter, + buildNativeConcatArgs, + buildNativeCudaOverlayStaticLayoutArgs, + buildNativeCudaScaleCpuPadStaticLayoutArgs, + buildNativePrecompositedStaticLayoutArgs, + buildNativeStaticBackgroundRenderArgs, + buildNativeStaticLayoutChunks, + buildNativeVideoExportArgs, + buildTrimmedSourceAudioFilter, + createNativeSquircleMaskPgmBuffer, + FFMPEG_BT709_VIDEO_COLOR_ARGS, + isNativeCudaOutOfMemory, +} from "./nativeVideoExport"; + +describe("buildTrimmedSourceAudioFilter", () => { + it("concatenates trimmed source segments into a single output label", () => { + expect( + buildTrimmedSourceAudioFilter([ + { startMs: 0, endMs: 2_000 }, + { startMs: 4_000, endMs: 6_000 }, + ]), + ).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[trimmed_audio_0];" + + "[1:a]atrim=start=4.000:end=6.000,asetpts=PTS-STARTPTS[trimmed_audio_1];" + + "[trimmed_audio_0][trimmed_audio_1]concat=n=2:v=0:a=1[aout]", + ); + }); +}); + +describe("buildEditedTrackSourceAudioFilter", () => { + it("builds a concat filtergraph that applies tempo filters for speed changes", () => { + const filter = buildEditedTrackSourceAudioFilter( + [ + { startMs: 0, endMs: 2_000, speed: 1 }, + { startMs: 2_000, endMs: 6_000, speed: 1.5 }, + ], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + + "[1:a]atrim=start=2.000:end=6.000,asetpts=PTS-STARTPTS,atempo=1.500000[edited_audio_1];" + + "[edited_audio_0][edited_audio_1]concat=n=2:v=0:a=1[aout]", + ); + }); + + it("builds a filtergraph for slowdown segments with a tempo filter", () => { + const filter = buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed: 0.5 }], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS,atempo=0.500000[edited_audio_0];" + + "[edited_audio_0]anull[aout]", + ); + }); + + it("treats near-unity speed changes as unchanged audio", () => { + const filter = buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed: 1.0002 }], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + + "[edited_audio_0]anull[aout]", + ); + }); + + it("treats exact epsilon speed changes as unchanged audio", () => { + for (const speed of [1 - ATEMPO_FILTER_EPSILON, 1 + ATEMPO_FILTER_EPSILON]) { + const filter = buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed }], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + + "[edited_audio_0]anull[aout]", + ); + } + }); + + it("returns null when the edited-track filtergraph inputs are incomplete", () => { + expect(buildEditedTrackSourceAudioFilter([], 44_100)).toBeNull(); + expect( + buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed: 1.5 }], + Number.NaN, + ), + ).toBeNull(); + }); + + it("returns null when the edited-track segments are malformed", () => { + expect( + buildEditedTrackSourceAudioFilter( + [{ startMs: Number.NaN, endMs: 2_000, speed: 1.5 }], + 44_100, + ), + ).toBeNull(); + expect( + buildEditedTrackSourceAudioFilter([{ startMs: 0, endMs: 2_000, speed: 0 }], 44_100), + ).toBeNull(); + expect( + buildEditedTrackSourceAudioFilter([{ startMs: 0, endMs: 2_000, speed: -1 }], 44_100), + ).toBeNull(); + expect( + buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed: Number.NaN }], + 44_100, + ), + ).toBeNull(); + expect( + buildEditedTrackSourceAudioFilter([{ startMs: 0, endMs: 2_000, speed: 1 }], 0.4), + ).toBeNull(); + expect( + buildEditedTrackSourceAudioFilter([{ startMs: -100, endMs: 2_000, speed: 1 }], 44_100), + ).toBeNull(); + expect( + buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed: Number.MAX_SAFE_INTEGER }], + 44_100, + ), + ).toBeNull(); + }); +}); + +describe("native static layout command builders", () => { + const baseConfig = { + inputPath: "input.mp4", + outputPath: "chunk.mp4", + width: 1920, + height: 1080, + frameRate: 60, + bitrate: 8_000_000, + encodingMode: "fast" as const, + contentWidth: 1536, + contentHeight: 864, + offsetX: 192, + offsetY: 108, + backgroundColor: "#101010", + startSec: 120, + durationSec: 60, + }; + + it("builds the primary CUDA overlay layout command", () => { + const args = buildNativeCudaOverlayStaticLayoutArgs(baseConfig); + + expect(args).toContain("-filter_complex"); + expect(args).toContain( + "color=c=0x101010:s=1920x1080:r=60:d=60.000,format=nv12,setrange=limited,hwupload_cuda[bg];" + + "[0:v]scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv:sws_dither=bayer,fps=60,hwupload_cuda[fg];" + + "[bg][fg]overlay_cuda=192:108:shortest=0:repeatlast=1:eof_action=repeat,trim=duration=60.000,setpts=PTS-STARTPTS[out]", + ); + expect(args).toContain("h264_nvenc"); + expect(args).toContain("p1"); + expect(args).not.toContain("yuv420p"); + expect(args).toEqual(expect.arrayContaining(["-ss", "120.000", "-t", "60.000"])); + expect(args).toEqual(expect.arrayContaining([...FFMPEG_BT709_VIDEO_COLOR_ARGS])); + }); + + it("converts full-range canvas pixels and tags native H.264 as BT.709 video range", () => { + const args = buildNativeVideoExportArgs( + "h264_videotoolbox", + { + width: 1920, + height: 1080, + frameRate: 30, + bitrate: 30_000_000, + encodingMode: "quality", + }, + "out.mp4", + ); + + expect(args).toEqual( + expect.arrayContaining([ + "-vf", + "vflip,scale=in_range=full:out_range=tv:sws_dither=bayer", + "-colorspace", + "bt709", + "-color_primaries", + "bt709", + "-color_trc", + "bt709", + "-color_range", + "tv", + ]), + ); + }); + + it("builds the stable CUDA scale plus CPU pad fallback command", () => { + const args = buildNativeCudaScaleCpuPadStaticLayoutArgs(baseConfig); + + expect(args).toEqual( + expect.arrayContaining([ + "-vf", + "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv:sws_dither=bayer,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010", + "-map", + "0:v:0", + "-an", + ]), + ); + expect(args).toEqual(expect.arrayContaining([...FFMPEG_BT709_VIDEO_COLOR_ARGS])); + }); + + it("sanitizes unsupported background colors to the safe dark fallback", () => { + const args = buildNativeCudaScaleCpuPadStaticLayoutArgs({ + ...baseConfig, + backgroundColor: "linear-gradient(red, blue)", + }); + + expect(args).toContain( + "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv:sws_dither=bayer,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010", + ); + }); + + it("builds a precomposited background command for image wallpaper and shadow", () => { + const args = buildNativeStaticBackgroundRenderArgs({ + ...baseConfig, + outputPath: "background.png", + backgroundImagePath: "wallpaper.jpg", + maskPath: "mask.pgm", + shadowIntensity: 0.67, + }); + const filterComplex = args[args.indexOf("-filter_complex") + 1]; + + expect(args).toEqual(expect.arrayContaining(["-i", "wallpaper.jpg", "-i", "mask.pgm"])); + expect(filterComplex).toContain( + "scale=w=1920:h=1080:force_original_aspect_ratio=increase,crop=w=1920:h=1080", + ); + expect(filterComplex).toContain("split=3"); + expect(filterComplex).toContain("gblur=sigma=32.16:steps=2"); + expect(filterComplex).toContain("overlay=x=119:y=35:format=auto"); + expect(args).toEqual(expect.arrayContaining(["-frames:v", "1", "background.png"])); + }); + + it("pre-blurs image wallpapers for native fallback static backgrounds", () => { + const args = buildNativeStaticBackgroundRenderArgs({ + ...baseConfig, + outputPath: "background.png", + backgroundImagePath: "wallpaper.jpg", + backgroundBlurPx: 36, + }); + const filterComplex = args[args.indexOf("-filter_complex") + 1]; + + expect(filterComplex).toContain("[bg0]gblur=sigma=36:steps=2[bg_blur]"); + expect(filterComplex).toContain("[bg_blur]format=rgba[out]"); + }); + + it("builds a precomposited static layout command with a squircle alpha mask", () => { + const args = buildNativePrecompositedStaticLayoutArgs({ + ...baseConfig, + staticBackgroundPath: "background.png", + maskPath: "mask.pgm", + borderRadius: 12.5, + }); + const filterComplex = args[args.indexOf("-filter_complex") + 1]; + + expect(args).toEqual(expect.arrayContaining(["-i", "background.png", "-i", "mask.pgm"])); + expect(filterComplex).toContain( + "scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=full,fps=60,format=rgba", + ); + expect(filterComplex).toContain("[fgbase][mask]alphamerge[fg]"); + expect(filterComplex).toContain("overlay=x=192:y=108:format=auto"); + expect(filterComplex).toContain( + "scale=in_range=full:out_range=tv:sws_dither=bayer,format=yuv420p[out]", + ); + expect(args).toContain("h264_nvenc"); + expect(args).toEqual(expect.arrayContaining(["-pix_fmt", "yuv420p"])); + expect(args).toEqual(expect.arrayContaining([...FFMPEG_BT709_VIDEO_COLOR_ARGS])); + }); + + it("creates an opaque PGM mask for square video corners and a partial mask for radius", () => { + const squareMask = createNativeSquircleMaskPgmBuffer(4, 4, 0); + expect(squareMask.subarray(squareMask.length - 16)).toEqual(Buffer.alloc(16, 255)); + + const roundedMask = createNativeSquircleMaskPgmBuffer(8, 8, 4); + const header = Buffer.from("P5\n8 8\n255\n", "ascii"); + const pixels = roundedMask.subarray(header.length); + expect(pixels[0]).toBeLessThan(255); + expect(pixels[4 * 8 + 4]).toBe(255); + }); + + it("splits long exports into bounded chunks", () => { + expect(buildNativeStaticLayoutChunks(367.5, 120)).toEqual([ + { index: 0, startSec: 0, durationSec: 120 }, + { index: 1, startSec: 120, durationSec: 120 }, + { index: 2, startSec: 240, durationSec: 120 }, + { index: 3, startSec: 360, durationSec: 7.5 }, + ]); + }); + + it("builds concat args for already encoded chunks", () => { + expect(buildNativeConcatArgs({ listPath: "chunks.txt", outputPath: "out.mp4" })).toEqual([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "concat", + "-safe", + "0", + "-i", + "chunks.txt", + "-c", + "copy", + "-movflags", + "+faststart", + "out.mp4", + ]); + }); + + it("detects CUDA OOM as a retryable fast-path failure", () => { + expect( + isNativeCudaOutOfMemory( + "cu->cuMemAlloc(&data, size) failed -> CUDA_ERROR_OUT_OF_MEMORY: out of memory", + ), + ).toBe(true); + expect(isNativeCudaOutOfMemory("FFmpeg exited with code 1")).toBe(false); + }); +}); diff --git a/electron/ipc/nativeVideoExport.ts b/electron/ipc/nativeVideoExport.ts new file mode 100644 index 000000000..8b9a0fa32 --- /dev/null +++ b/electron/ipc/nativeVideoExport.ts @@ -0,0 +1,771 @@ +import { + getShadowFilterPadding, + VIDEO_SHADOW_LAYER_PROFILES, +} from "../../src/lib/exporter/shadowProfile"; +import { getSquirclePathPoints } from "../../src/lib/geometry/squircle"; +import { ATEMPO_FILTER_EPSILON, buildAtempoFilters } from "./ffmpeg/filters"; + +const NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL = 4; +const MIN_EDITED_TRACK_TEMPO_SPEED = 0.5; +const MAX_EDITED_TRACK_TEMPO_SPEED = 2; + +export const FFMPEG_BT709_VIDEO_COLOR_ARGS = [ + "-colorspace", + "bt709", + "-color_primaries", + "bt709", + "-color_trc", + "bt709", + "-color_range", + "tv", +] as const; + +const FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER = "scale=in_range=auto:out_range=tv:sws_dither=bayer"; +const FFMPEG_AUTO_TO_FULL_RANGE_FILTER = "scale=in_range=auto:out_range=full"; +const FFMPEG_FULL_TO_VIDEO_RANGE_FILTER = "scale=in_range=full:out_range=tv:sws_dither=bayer"; + +export type NativeExportEncodingMode = "fast" | "balanced" | "quality"; + +export type NativeVideoExportAudioMode = "none" | "copy-source" | "trim-source" | "edited-track"; +export type NativeVideoExportEditedTrackStrategy = + | "filtergraph-fast-path" + | "offline-render-fallback"; + +export interface NativeVideoExportStartOptions { + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: NativeExportEncodingMode; + inputMode?: "rawvideo" | "h264-stream"; +} + +export interface NativeVideoExportAudioSegment { + startMs: number; + endMs: number; +} + +export interface NativeVideoExportEditedTrackSegment extends NativeVideoExportAudioSegment { + speed: number; +} + +export interface NativeVideoExportFinishOptions { + audioMode?: NativeVideoExportAudioMode; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: NativeVideoExportAudioSegment[]; + editedTrackStrategy?: NativeVideoExportEditedTrackStrategy; + editedTrackSegments?: NativeVideoExportEditedTrackSegment[]; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; +} + +export interface NativeVideoAudioMuxMetrics { + tempVideoWriteMs?: number; + tempEditedAudioWriteMs?: number; + ffmpegExecMs?: number; + muxedVideoReadMs?: number; + tempVideoBytes?: number; + tempEditedAudioBytes?: number; + muxedVideoBytes?: number; +} + +export type NativeStaticLayoutBackend = + | "cuda-overlay" + | "cuda-scale-cpu-pad" + | "cuda-static-composite" + | "nvidia-cuda-compositor" + | "windows-d3d11-compositor"; + +export interface NativeStaticLayoutExportArgsConfig { + inputPath: string; + outputPath: string; + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: NativeExportEncodingMode; + contentWidth: number; + contentHeight: number; + offsetX: number; + offsetY: number; + sourceCropX?: number; + sourceCropY?: number; + sourceCropWidth?: number; + sourceCropHeight?: number; + backgroundColor: string; + backgroundImagePath?: string | null; + backgroundBlurPx?: number; + staticBackgroundPath?: string | null; + maskPath?: string | null; + borderRadius?: number; + shadowIntensity?: number; + startSec?: number; + durationSec?: number; +} + +export interface NativeStaticLayoutChunk { + index: number; + startSec: number; + durationSec: number; +} + +export function getNativeVideoInputByteSize(width: number, height: number): number { + return width * height * NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL; +} + +export function parseAvailableFfmpegEncoders(stdout: string): Set { + const encoders = new Set(); + + for (const line of stdout.split(/\r?\n/)) { + const match = line.match(/^\s*[A-Z.]{6}\s+([a-z0-9_]+)/i); + if (match?.[1]) { + encoders.add(match[1]); + } + } + + return encoders; +} + +export function getPreferredNativeVideoEncoders(platform: NodeJS.Platform): string[] { + switch (platform) { + case "darwin": + return ["h264_videotoolbox", "libx264"]; + case "win32": + return ["h264_nvenc", "h264_qsv", "h264_amf", "h264_mf", "libx264"]; + case "linux": + return ["h264_nvenc", "h264_qsv", "libx264"]; + default: + return ["libx264"]; + } +} + +function getLibx264ModeArgs(encodingMode: NativeExportEncodingMode): string[] { + switch (encodingMode) { + case "fast": + return ["-preset", "ultrafast", "-tune", "zerolatency"]; + case "quality": + return ["-preset", "slow"]; + case "balanced": + default: + return ["-preset", "medium"]; + } +} + +function getBitrateArgs(bitrate: number): string[] { + const effectiveBitrate = Math.max(1_500_000, Math.round(bitrate)); + const maxRate = Math.max(effectiveBitrate, Math.round(effectiveBitrate * 1.2)); + const bufferSize = Math.max(maxRate * 2, effectiveBitrate * 2); + + return [ + "-b:v", + String(effectiveBitrate), + "-maxrate", + String(maxRate), + "-bufsize", + String(bufferSize), + ]; +} + +function getNvencStaticLayoutModeArgs(encodingMode: NativeExportEncodingMode): string[] { + const lowLatencyRateControlArgs = [ + "-rc", + "vbr", + "-multipass", + "disabled", + "-rc-lookahead", + "0", + "-surfaces", + "32", + ]; + switch (encodingMode) { + case "quality": + return ["-preset", "p1", "-tune", "hq", ...lowLatencyRateControlArgs]; + case "balanced": + return ["-preset", "p1", "-tune", "ll", ...lowLatencyRateControlArgs]; + case "fast": + default: + return ["-preset", "p1", "-tune", "ull", ...lowLatencyRateControlArgs]; + } +} + +function formatFfmpegColor(value: string): string { + const trimmed = value.trim(); + const hex = trimmed.match(/^#?([0-9a-f]{6})$/i)?.[1]; + return hex ? `0x${hex.toLowerCase()}` : "0x101010"; +} + +function clampUnitInterval(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + + return Math.min(1, Math.max(0, value)); +} + +function formatFfmpegNumber(value: number): string { + return Number.isInteger(value) + ? String(value) + : value.toFixed(6).replace(/0+$/, "").replace(/\.$/, ""); +} + +function isPointInsidePolygon(x: number, y: number, points: Array<{ x: number; y: number }>) { + let inside = false; + for ( + let index = 0, previousIndex = points.length - 1; + index < points.length; + previousIndex = index++ + ) { + const current = points[index]; + const previous = points[previousIndex]; + const intersects = + current.y > y !== previous.y > y && + x < ((previous.x - current.x) * (y - current.y)) / (previous.y - current.y) + current.x; + + if (intersects) { + inside = !inside; + } + } + + return inside; +} + +export function createNativeSquircleMaskPgmBuffer( + width: number, + height: number, + radius: number, +): Buffer { + const safeWidth = Math.max(1, Math.round(width)); + const safeHeight = Math.max(1, Math.round(height)); + const clampedRadius = Math.min(Math.max(0, radius), Math.min(safeWidth, safeHeight) / 2); + const header = Buffer.from(`P5\n${safeWidth} ${safeHeight}\n255\n`, "ascii"); + const pixels = Buffer.alloc(safeWidth * safeHeight, 255); + + if (clampedRadius <= 0.5) { + return Buffer.concat([header, pixels]); + } + + const points = getSquirclePathPoints({ + x: 0, + y: 0, + width: safeWidth, + height: safeHeight, + radius: clampedRadius, + }); + const samples = [ + [0.25, 0.25], + [0.75, 0.25], + [0.25, 0.75], + [0.75, 0.75], + ] as const; + + for (let y = 0; y < safeHeight; y += 1) { + for (let x = 0; x < safeWidth; x += 1) { + let coveredSamples = 0; + for (const [sampleX, sampleY] of samples) { + if (isPointInsidePolygon(x + sampleX, y + sampleY, points)) { + coveredSamples += 1; + } + } + pixels[y * safeWidth + x] = Math.round((coveredSamples / samples.length) * 255); + } + } + + return Buffer.concat([header, pixels]); +} + +function pushFfmpegTimeSliceArgs( + args: string[], + startSec: number | undefined, + durationSec: number | undefined, +) { + if (Number.isFinite(startSec) && (startSec ?? 0) > 0) { + args.push("-ss", formatFfmpegSeconds((startSec ?? 0) * 1000)); + } + + if (Number.isFinite(durationSec) && (durationSec ?? 0) > 0) { + args.push("-t", formatFfmpegSeconds((durationSec ?? 0) * 1000)); + } +} + +export function buildNativeVideoExportArgs( + encoder: string, + options: NativeVideoExportStartOptions, + outputPath: string, +): string[] { + const args = [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "rawvideo", + "-pix_fmt", + "rgba", + "-s:v", + `${options.width}x${options.height}`, + "-framerate", + String(options.frameRate), + "-i", + "pipe:0", + "-vf", + `vflip,${FFMPEG_FULL_TO_VIDEO_RANGE_FILTER}`, + "-an", + "-c:v", + encoder, + "-g", + String(Math.max(1, Math.round(options.frameRate * 5))), + ...getBitrateArgs(options.bitrate), + ]; + + if (encoder === "libx264") { + args.push(...getLibx264ModeArgs(options.encodingMode)); + } + + args.push( + "-pix_fmt", + "yuv420p", + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, + "-movflags", + "+faststart", + outputPath, + ); + return args; +} + +export function buildNativeCudaOverlayStaticLayoutArgs( + config: NativeStaticLayoutExportArgsConfig, +): string[] { + const backgroundColor = formatFfmpegColor(config.backgroundColor); + const durationSec = formatFfmpegSeconds(Math.max(0.001, config.durationSec ?? 1) * 1000); + const args = ["-y", "-hide_banner", "-loglevel", "error"]; + pushFfmpegTimeSliceArgs(args, config.startSec, config.durationSec); + args.push( + "-hwaccel", + "cuda", + "-hwaccel_output_format", + "cuda", + "-i", + config.inputPath, + "-filter_complex", + `color=c=${backgroundColor}:s=${config.width}x${config.height}:r=${config.frameRate}:d=${durationSec},format=nv12,setrange=limited,hwupload_cuda[bg];[0:v]scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,${FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER},fps=${config.frameRate},hwupload_cuda[fg];[bg][fg]overlay_cuda=${config.offsetX}:${config.offsetY}:shortest=0:repeatlast=1:eof_action=repeat,trim=duration=${durationSec},setpts=PTS-STARTPTS[out]`, + "-map", + "[out]", + "-an", + "-r", + String(config.frameRate), + "-c:v", + "h264_nvenc", + ...getNvencStaticLayoutModeArgs(config.encodingMode), + ...getBitrateArgs(config.bitrate), + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, + "-movflags", + "+faststart", + config.outputPath, + ); + return args; +} + +export function buildNativeCudaScaleCpuPadStaticLayoutArgs( + config: NativeStaticLayoutExportArgsConfig, +): string[] { + const backgroundColor = formatFfmpegColor(config.backgroundColor); + const args = ["-y", "-hide_banner", "-loglevel", "error"]; + pushFfmpegTimeSliceArgs(args, config.startSec, config.durationSec); + args.push( + "-hwaccel", + "cuda", + "-hwaccel_output_format", + "cuda", + "-i", + config.inputPath, + "-vf", + `scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,${FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER},fps=${config.frameRate},pad=w=${config.width}:h=${config.height}:x=${config.offsetX}:y=${config.offsetY}:color=${backgroundColor}`, + "-map", + "0:v:0", + "-an", + "-r", + String(config.frameRate), + "-c:v", + "h264_nvenc", + ...getNvencStaticLayoutModeArgs(config.encodingMode), + ...getBitrateArgs(config.bitrate), + "-pix_fmt", + "yuv420p", + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, + "-movflags", + "+faststart", + config.outputPath, + ); + return args; +} + +export function buildNativeStaticBackgroundRenderArgs( + config: NativeStaticLayoutExportArgsConfig, +): string[] { + const backgroundColor = formatFfmpegColor(config.backgroundColor); + const backgroundBlurPx = Math.max(0, config.backgroundBlurPx ?? 0); + const args = ["-y", "-hide_banner", "-loglevel", "error"]; + if (config.backgroundImagePath) { + args.push("-i", config.backgroundImagePath); + } else { + args.push( + "-f", + "lavfi", + "-i", + `color=c=${backgroundColor}:s=${config.width}x${config.height}:d=1`, + ); + } + + const shadowStrength = clampUnitInterval(config.shadowIntensity ?? 0); + const shadowLayers = + shadowStrength > 0 && config.maskPath + ? VIDEO_SHADOW_LAYER_PROFILES.map((layer) => ({ + offsetY: layer.offsetScale * shadowStrength, + alpha: clampUnitInterval(layer.alphaScale * shadowStrength), + blur: Math.max(0, layer.blurScale * shadowStrength), + })).filter((layer) => layer.alpha > 0) + : []; + + if (shadowLayers.length > 0 && config.maskPath) { + args.push("-i", config.maskPath); + } + + const filterParts = [ + config.backgroundImagePath + ? `[0:v]scale=w=${config.width}:h=${config.height}:force_original_aspect_ratio=increase,crop=w=${config.width}:h=${config.height},setsar=1,format=rgba[bg0]` + : "[0:v]format=rgba[bg0]", + ]; + let currentBackgroundLabel = "bg0"; + if (backgroundBlurPx > 0 && config.backgroundImagePath) { + filterParts.push( + `[${currentBackgroundLabel}]gblur=sigma=${formatFfmpegNumber( + Math.min(96, backgroundBlurPx), + )}:steps=2[bg_blur]`, + ); + currentBackgroundLabel = "bg_blur"; + } + + if (shadowLayers.length > 0) { + filterParts.push( + `[1:v]format=gray,split=${shadowLayers.length}${shadowLayers + .map((_, index) => `[shadow_mask_source_${index}]`) + .join("")}`, + ); + } + + shadowLayers.forEach((layer, index) => { + const padding = getShadowFilterPadding(layer.blur, layer.offsetY); + const paddedWidth = config.contentWidth + padding * 2; + const paddedHeight = config.contentHeight + padding * 2; + const positionedMaskLabel = `shadow_mask_positioned_${index}`; + const shadowMaskLabel = `shadow_mask_${index}`; + const shadowColorLabel = `shadow_color_${index}`; + const shadowLabel = `shadow_${index}`; + const nextBackgroundLabel = `bg${index + 1}`; + const blurFilter = + layer.blur > 0 ? `,gblur=sigma=${formatFfmpegNumber(layer.blur)}:steps=2` : ""; + + filterParts.push( + `[shadow_mask_source_${index}]lut=y=val*${formatFfmpegNumber( + layer.alpha, + )},pad=w=${paddedWidth}:h=${paddedHeight}:x=${padding}:y=${Math.round( + padding + layer.offsetY, + )}:color=black${blurFilter}[${positionedMaskLabel}]`, + `[${positionedMaskLabel}]format=gray[${shadowMaskLabel}]`, + `color=c=black:s=${paddedWidth}x${paddedHeight}:d=1,format=rgba[${shadowColorLabel}]`, + `[${shadowColorLabel}][${shadowMaskLabel}]alphamerge[${shadowLabel}]`, + `[${currentBackgroundLabel}][${shadowLabel}]overlay=x=${ + config.offsetX - padding + }:y=${config.offsetY - padding}:format=auto[${nextBackgroundLabel}]`, + ); + currentBackgroundLabel = nextBackgroundLabel; + }); + + filterParts.push(`[${currentBackgroundLabel}]format=rgba[out]`); + args.push( + "-filter_complex", + filterParts.join(";"), + "-map", + "[out]", + "-frames:v", + "1", + config.outputPath, + ); + return args; +} + +export function buildNativePrecompositedStaticLayoutArgs( + config: NativeStaticLayoutExportArgsConfig, +): string[] { + if (!config.staticBackgroundPath) { + throw new Error("Native precomposited static layout requires a static background path"); + } + + const durationSec = formatFfmpegSeconds(Math.max(0.001, config.durationSec ?? 1) * 1000); + const useMask = Boolean(config.maskPath && (config.borderRadius ?? 0) > 0.5); + const args = ["-y", "-hide_banner", "-loglevel", "error"]; + pushFfmpegTimeSliceArgs(args, config.startSec, config.durationSec); + args.push( + "-hwaccel", + "cuda", + "-hwaccel_output_format", + "cuda", + "-i", + config.inputPath, + "-loop", + "1", + "-framerate", + String(config.frameRate), + "-t", + durationSec, + "-i", + config.staticBackgroundPath, + ); + + if (useMask && config.maskPath) { + args.push( + "-loop", + "1", + "-framerate", + String(config.frameRate), + "-t", + durationSec, + "-i", + config.maskPath, + ); + } + + const foregroundFilter = `[0:v]scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,${FFMPEG_AUTO_TO_FULL_RANGE_FILTER},fps=${config.frameRate},format=rgba[fgbase]`; + const maskFilter = useMask ? ";[2:v]format=gray[mask];[fgbase][mask]alphamerge[fg]" : ""; + const foregroundLabel = useMask ? "fg" : "fgbase"; + const filterComplex = `${foregroundFilter}${maskFilter};[1:v]format=rgba[bg];[bg][${foregroundLabel}]overlay=x=${config.offsetX}:y=${config.offsetY}:format=auto,trim=duration=${durationSec},setpts=PTS-STARTPTS,${FFMPEG_FULL_TO_VIDEO_RANGE_FILTER},format=yuv420p[out]`; + + args.push( + "-filter_complex", + filterComplex, + "-map", + "[out]", + "-an", + "-r", + String(config.frameRate), + "-c:v", + "h264_nvenc", + ...getNvencStaticLayoutModeArgs(config.encodingMode), + ...getBitrateArgs(config.bitrate), + "-pix_fmt", + "yuv420p", + ...FFMPEG_BT709_VIDEO_COLOR_ARGS, + "-movflags", + "+faststart", + config.outputPath, + ); + return args; +} + +export function buildNativeConcatArgs(config: { listPath: string; outputPath: string }): string[] { + return [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "concat", + "-safe", + "0", + "-i", + config.listPath, + "-c", + "copy", + "-movflags", + "+faststart", + config.outputPath, + ]; +} + +export function buildNativeStaticLayoutChunks( + durationSec: number, + chunkDurationSec: number, +): NativeStaticLayoutChunk[] { + if (!Number.isFinite(durationSec) || durationSec <= 0) { + return []; + } + + const safeChunkDuration = Math.max(1, Math.min(300, Math.floor(chunkDurationSec))); + const chunks: NativeStaticLayoutChunk[] = []; + for ( + let startSec = 0, index = 0; + startSec < durationSec; + startSec += safeChunkDuration, index++ + ) { + chunks.push({ + index, + startSec, + durationSec: Math.min(safeChunkDuration, durationSec - startSec), + }); + } + + return chunks; +} + +export function isNativeCudaOutOfMemory(stderr: string): boolean { + return /CUDA_ERROR_OUT_OF_MEMORY|cuMemAlloc.+out of memory/i.test(stderr); +} + +function formatFfmpegSeconds(milliseconds: number): string { + return (milliseconds / 1000).toFixed(3); +} + +export function buildTrimmedSourceAudioFilter( + segments: NativeVideoExportAudioSegment[], +): string | null { + if (segments.length === 0) { + return null; + } + + const filterParts: string[] = []; + const segmentLabels: string[] = []; + + segments.forEach((segment, index) => { + const label = `trimmed_audio_${index}`; + filterParts.push( + `[1:a]atrim=start=${formatFfmpegSeconds(segment.startMs)}:end=${formatFfmpegSeconds(segment.endMs)},asetpts=PTS-STARTPTS[${label}]`, + ); + segmentLabels.push(`[${label}]`); + }); + + if (segmentLabels.length === 1) { + filterParts.push(`${segmentLabels[0]}anull[aout]`); + } else { + filterParts.push(`${segmentLabels.join("")}concat=n=${segmentLabels.length}:v=0:a=1[aout]`); + } + + return filterParts.join(";"); +} + +export function buildEditedTrackSourceAudioFilter( + segments: NativeVideoExportEditedTrackSegment[], + sourceSampleRate: number, +): string | null { + if (segments.length === 0 || !Number.isFinite(sourceSampleRate) || sourceSampleRate <= 0) { + return null; + } + + const normalizedSourceSampleRate = Math.round(sourceSampleRate); + if (normalizedSourceSampleRate < 1) { + return null; + } + + const filterParts: string[] = []; + const segmentLabels: string[] = []; + let hasInvalidSegment = false; + + segments.forEach((segment, index) => { + if ( + !Number.isFinite(segment.startMs) || + !Number.isFinite(segment.endMs) || + segment.startMs < 0 || + segment.endMs < 0 + ) { + hasInvalidSegment = true; + return; + } + + if (segment.endMs - segment.startMs <= 0.5) { + hasInvalidSegment = true; + return; + } + + const label = `edited_audio_${index}`; + const speed = segment.speed; + if ( + !Number.isFinite(speed) || + speed < MIN_EDITED_TRACK_TEMPO_SPEED || + speed > MAX_EDITED_TRACK_TEMPO_SPEED + ) { + hasInvalidSegment = true; + return; + } + + const segmentFilter = [ + `[1:a]atrim=start=${formatFfmpegSeconds(segment.startMs)}:end=${formatFfmpegSeconds(segment.endMs)}`, + "asetpts=PTS-STARTPTS", + ]; + + const tempoFilters = buildAtempoFilters(speed); + if (tempoFilters.length > 0) { + segmentFilter.push(...tempoFilters); + } else if (Math.abs(speed - 1) > ATEMPO_FILTER_EPSILON) { + hasInvalidSegment = true; + return; + } + + filterParts.push(`${segmentFilter.join(",")}[${label}]`); + segmentLabels.push(`[${label}]`); + }); + + if (hasInvalidSegment || segmentLabels.length === 0) { + return null; + } + + if (segmentLabels.length === 1) { + filterParts.push(`${segmentLabels[0]}anull[aout]`); + } else { + filterParts.push(`${segmentLabels.join("")}concat=n=${segmentLabels.length}:v=0:a=1[aout]`); + } + + return filterParts.join(";"); +} + +/** + * Builds FFmpeg arguments for a zero-copy H.264 stream export. + * FFmpeg receives a pre-encoded Annex B H.264 stream on stdin (produced by the + * browser's hardware VideoEncoder) and copies it straight into an MP4 container + * — no re-encoding step, no raw pixel IPC traffic. + */ +export function buildNativeH264StreamExportArgs(config: { + frameRate: number; + outputPath: string; +}): string[] { + return [ + "-y", + "-hide_banner", + "-loglevel", + "error", + // Input 0: pre-encoded H.264 Annex B stream from browser VideoEncoder via stdin + "-f", + "h264", + "-r", + String(config.frameRate), + "-i", + "pipe:0", + "-an", // audio handled separately by muxNativeVideoExportAudio + "-c:v", + "copy", + "-movflags", + "+faststart", + config.outputPath, + ]; +} + +export function getEditedAudioExtension(mimeType?: string | null): string { + if (!mimeType) { + return ".webm"; + } + + if (mimeType.includes("wav")) { + return ".wav"; + } + + if (mimeType.includes("mp4") || mimeType.includes("m4a")) { + return ".m4a"; + } + + if (mimeType.includes("ogg")) { + return ".ogg"; + } + + return ".webm"; +} diff --git a/electron/ipc/paths/binaries.test.ts b/electron/ipc/paths/binaries.test.ts new file mode 100644 index 000000000..361861739 --- /dev/null +++ b/electron/ipc/paths/binaries.test.ts @@ -0,0 +1,83 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Windows native helper path resolution", () => { + let tempRoot: string; + let appPath: string; + + beforeEach(async () => { + tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-helper-paths-")); + appPath = path.join(tempRoot, "App"); + await fs.mkdir(appPath, { recursive: true }); + + vi.resetModules(); + vi.doMock("electron", () => ({ + app: { + isPackaged: false, + getAppPath: () => appPath, + }, + })); + }); + + afterEach(async () => { + vi.resetModules(); + vi.doUnmock("electron"); + await fs.rm(tempRoot, { recursive: true, force: true }); + }); + + it("resolves a requested platform tag independently of the host platform", async () => { + const { getNativeArchTag } = await import("./binaries"); + + expect(getNativeArchTag("win32")).toBe( + process.arch === "arm64" ? "win32-arm64" : "win32-x64", + ); + }); + + it("prefers the branch-staged helper over a stale local CMake build in dev", async () => { + const buildOutputPath = path.join( + appPath, + "electron", + "native", + "wgc-capture", + "build", + "Release", + "wgc-capture.exe", + ); + const prebundledPath = path.join( + appPath, + "electron", + "native", + "bin", + process.arch === "arm64" ? "win32-arm64" : "win32-x64", + "wgc-capture.exe", + ); + await fs.mkdir(path.dirname(buildOutputPath), { recursive: true }); + await fs.mkdir(path.dirname(prebundledPath), { recursive: true }); + await fs.writeFile(buildOutputPath, "old-local-build"); + await fs.writeFile(prebundledPath, "branch-staged-helper"); + + const { getWindowsCaptureExePath } = await import("./binaries"); + + expect(getWindowsCaptureExePath()).toBe(prebundledPath); + }); + + it("falls back to the local CMake build when no staged helper exists", async () => { + const buildOutputPath = path.join( + appPath, + "electron", + "native", + "wgc-capture", + "build", + "Release", + "wgc-capture.exe", + ); + await fs.mkdir(path.dirname(buildOutputPath), { recursive: true }); + await fs.writeFile(buildOutputPath, "local-build"); + + const { getWindowsCaptureExePath } = await import("./binaries"); + + expect(getWindowsCaptureExePath()).toBe(buildOutputPath); + }); +}); diff --git a/electron/ipc/paths/binaries.ts b/electron/ipc/paths/binaries.ts new file mode 100644 index 000000000..77c8dd35f --- /dev/null +++ b/electron/ipc/paths/binaries.ts @@ -0,0 +1,273 @@ +import { execFile } from "node:child_process"; +import { existsSync, constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { app } from "electron"; +import { nativeHelperMigrationPromise, setNativeHelperMigrationPromise } from "../state"; + +const execFileAsync = promisify(execFile); + +/** + * Resolve a path within the app bundle, handling asar unpacking in production. + * Files listed in asarUnpack are extracted to app.asar.unpacked/ and must be + * accessed via that path instead of the asar virtual filesystem. + */ +export function resolveUnpackedAppPath(...segments: string[]): string { + const base = app.getAppPath(); + const resolved = path.join(base, ...segments); + if (app.isPackaged) { + return resolved.replace(/\.asar([/\\])/, ".asar.unpacked$1"); + } + return resolved; +} + +export function getNativeCaptureHelperSourcePath(): string { + return resolveUnpackedAppPath("electron", "native", "ScreenCaptureKitRecorder.swift"); +} + +export function getNativeArchTag(platform: NodeJS.Platform = process.platform): string { + if (platform === "darwin") { + return process.arch === "arm64" ? "darwin-arm64" : "darwin-x64"; + } + + if (platform === "win32") { + return process.arch === "arm64" ? "win32-arm64" : "win32-x64"; + } + + if (platform === "linux") { + return process.arch === "arm64" ? "linux-arm64" : "linux-x64"; + } + + return `${platform}-${process.arch}`; +} + +export function getPrebundledNativeHelperPath( + binaryName: string, + archTag = getNativeArchTag(), +): string { + return resolveUnpackedAppPath("electron", "native", "bin", archTag, binaryName); +} + +export function resolvePreferredWindowsNativeHelperPath( + helperDirectory: string, + binaryName: string, +): string { + const buildOutputPath = resolveUnpackedAppPath( + "electron", + "native", + helperDirectory, + "build", + "Release", + binaryName, + ); + const prebundledPath = getPrebundledNativeHelperPath(binaryName, getNativeArchTag("win32")); + + if (app.isPackaged && existsSync(prebundledPath)) { + return prebundledPath; + } + + // Source checkouts should run the helper staged in the branch instead of a + // stale local CMake build left over from an earlier test run. + if (existsSync(prebundledPath)) { + return prebundledPath; + } + + if (existsSync(buildOutputPath)) { + return buildOutputPath; + } + + return buildOutputPath; +} + +export function getBundledWhisperExecutableCandidates(): string[] { + const binaryNames = + process.platform === "win32" + ? ["whisper-cli.exe", "whisper-cpp.exe", "whisper.exe", "main.exe"] + : ["whisper-cli", "whisper-cpp", "whisper", "main"]; + + return binaryNames.map((binaryName) => getPrebundledNativeHelperPath(binaryName)); +} + +export function getNativeCaptureHelperBinaryPath(): string { + return path.join(app.getPath("userData"), "native-tools", "recordly-screencapturekit-helper"); +} + +export function getSystemCursorHelperSourcePath(): string { + return resolveUnpackedAppPath("electron", "native", "SystemCursorAssets.swift"); +} + +export function getSystemCursorHelperBinaryPath(): string { + return path.join(app.getPath("userData"), "native-tools", "recordly-system-cursors"); +} + +export function getNativeCursorMonitorSourcePath(): string { + return resolveUnpackedAppPath("electron", "native", "NativeCursorMonitor.swift"); +} + +export function getNativeCursorMonitorBinaryPath(): string { + return path.join(app.getPath("userData"), "native-tools", "recordly-native-cursor-monitor"); +} + +export function getNativeWindowListSourcePath(): string { + return resolveUnpackedAppPath("electron", "native", "ScreenCaptureKitWindowList.swift"); +} + +export function getNativeWindowListBinaryPath(): string { + return path.join(app.getPath("userData"), "native-tools", "recordly-window-list"); +} + +export function getWindowsCaptureExePath(): string { + return resolvePreferredWindowsNativeHelperPath("wgc-capture", "wgc-capture.exe"); +} + +export function getCursorMonitorExePath(): string { + return resolvePreferredWindowsNativeHelperPath("cursor-monitor", "cursor-monitor.exe"); +} + +async function migrateLegacyNativeHelperBinaries(): Promise { + const legacyToCurrentPaths: Array<[string, string]> = [ + [ + path.join( + app.getPath("userData"), + "native-tools", + "openscreen-screencapturekit-helper", + ), + getNativeCaptureHelperBinaryPath(), + ], + [ + path.join(app.getPath("userData"), "native-tools", "openscreen-window-list"), + getNativeWindowListBinaryPath(), + ], + [ + path.join(app.getPath("userData"), "native-tools", "openscreen-system-cursors"), + getSystemCursorHelperBinaryPath(), + ], + [ + path.join(app.getPath("userData"), "native-tools", "openscreen-native-cursor-monitor"), + getNativeCursorMonitorBinaryPath(), + ], + ]; + + for (const [legacyPath, currentPath] of legacyToCurrentPaths) { + if (legacyPath === currentPath || existsSync(currentPath) || !existsSync(legacyPath)) { + continue; + } + + try { + await fs.mkdir(path.dirname(currentPath), { recursive: true }); + await fs.rename(legacyPath, currentPath); + } catch (error) { + console.warn("[native-tools] Failed to migrate helper binary", { + legacyPath, + currentPath, + error, + }); + } + } +} + +export async function ensureNativeHelperMigration(): Promise { + if (!nativeHelperMigrationPromise) { + setNativeHelperMigrationPromise( + migrateLegacyNativeHelperBinaries().catch((error) => { + setNativeHelperMigrationPromise(null); + throw error; + }), + ); + } + + return nativeHelperMigrationPromise!; +} + +export async function ensureSwiftHelperBinary( + sourcePath: string, + binaryPath: string, + label: string, + prebundledBinaryName?: string, +): Promise { + if (prebundledBinaryName) { + const prebundledPath = getPrebundledNativeHelperPath(prebundledBinaryName); + try { + await fs.access(prebundledPath, fsConstants.X_OK); + if (app.isPackaged) { + return prebundledPath; + } + + const [sourceStat, prebundledStat] = await Promise.all([ + fs.stat(sourcePath), + fs.stat(prebundledPath), + ]); + if (prebundledStat.mtimeMs >= sourceStat.mtimeMs) { + return prebundledPath; + } + } catch { + if (app.isPackaged) { + throw new Error( + `${label} is missing from this app build (${prebundledPath}). Reinstall or update the app.`, + ); + } + } + } + + const helperDir = path.dirname(binaryPath); + await fs.mkdir(helperDir, { recursive: true }); + + let shouldCompile = false; + try { + const [sourceStat, binaryStat] = await Promise.all([ + fs.stat(sourcePath), + fs.stat(binaryPath).catch(() => null), + ]); + shouldCompile = !binaryStat || sourceStat.mtimeMs > binaryStat.mtimeMs; + } catch (error) { + throw new Error(`${label} source is unavailable: ${String(error)}`); + } + + if (!shouldCompile) { + return binaryPath; + } + + try { + await execFileAsync("swiftc", ["-O", sourcePath, "-o", binaryPath], { + encoding: "utf8", + timeout: 120000, + }); + } catch (error) { + const err = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string }; + const details = [err.stderr, err.stdout].filter(Boolean).join("\n").trim(); + throw new Error(details || `Failed to compile ${label}`); + } + + return binaryPath; +} + +export async function ensureNativeCaptureHelperBinary(): Promise { + await ensureNativeHelperMigration(); + return ensureSwiftHelperBinary( + getNativeCaptureHelperSourcePath(), + getNativeCaptureHelperBinaryPath(), + "native ScreenCaptureKit helper", + "recordly-screencapturekit-helper", + ); +} + +export async function ensureNativeWindowListBinary(): Promise { + await ensureNativeHelperMigration(); + return ensureSwiftHelperBinary( + getNativeWindowListSourcePath(), + getNativeWindowListBinaryPath(), + "native ScreenCaptureKit window list helper", + "recordly-window-list", + ); +} + +export async function ensureNativeCursorMonitorBinary(): Promise { + await ensureNativeHelperMigration(); + return ensureSwiftHelperBinary( + getNativeCursorMonitorSourcePath(), + getNativeCursorMonitorBinaryPath(), + "native cursor monitor helper", + "recordly-native-cursor-monitor", + ); +} diff --git a/electron/ipc/project/atomicSave.test.ts b/electron/ipc/project/atomicSave.test.ts new file mode 100644 index 000000000..34646af34 --- /dev/null +++ b/electron/ipc/project/atomicSave.test.ts @@ -0,0 +1,112 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { getProjectBackupPath, writeProjectFileAtomically } from "./atomicSave"; + +describe("writeProjectFileAtomically", () => { + let tempDir: string; + let projectPath: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-atomic-project-")); + projectPath = path.join(tempDir, "demo.recordly"); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + async function expectNoTemporaryArtifacts() { + const entries = await fs.readdir(tempDir); + expect(entries.filter((entry) => entry.endsWith(".tmp"))).toEqual([]); + } + + it("commits a complete new project without creating a backup", async () => { + await writeProjectFileAtomically(projectPath, '{"version":1}'); + + await expect(fs.readFile(projectPath, "utf-8")).resolves.toBe('{"version":1}'); + await expect(fs.access(getProjectBackupPath(projectPath))).rejects.toMatchObject({ + code: "ENOENT", + }); + await expectNoTemporaryArtifacts(); + await expectNoTemporaryArtifacts(); + }); + + it("removes a stale backup when the target has no previous generation", async () => { + await fs.writeFile(getProjectBackupPath(projectPath), "stale-project"); + + await writeProjectFileAtomically(projectPath, '{"version":1}'); + + await expect(fs.access(getProjectBackupPath(projectPath))).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("preserves the previous complete generation before replacement", async () => { + await writeProjectFileAtomically(projectPath, '{"version":1,"name":"old"}'); + await writeProjectFileAtomically(projectPath, '{"version":1,"name":"new"}'); + + await expect(fs.readFile(projectPath, "utf-8")).resolves.toBe('{"version":1,"name":"new"}'); + await expect(fs.readFile(getProjectBackupPath(projectPath), "utf-8")).resolves.toBe( + '{"version":1,"name":"old"}', + ); + await expectNoTemporaryArtifacts(); + }); + + it("keeps the active generation unchanged when backup commit fails", async () => { + await writeProjectFileAtomically(projectPath, '{"version":1,"name":"old"}'); + await fs.mkdir(getProjectBackupPath(projectPath)); + + await expect( + writeProjectFileAtomically(projectPath, '{"version":1,"name":"new"}'), + ).rejects.toBeDefined(); + + await expect(fs.readFile(projectPath, "utf-8")).resolves.toBe('{"version":1,"name":"old"}'); + await expectNoTemporaryArtifacts(); + + await fs.rm(getProjectBackupPath(projectPath), { recursive: true }); + await writeProjectFileAtomically(projectPath, '{"version":1,"name":"retry"}'); + await expect(fs.readFile(projectPath, "utf-8")).resolves.toBe( + '{"version":1,"name":"retry"}', + ); + await expect(fs.readFile(getProjectBackupPath(projectPath), "utf-8")).resolves.toBe( + '{"version":1,"name":"old"}', + ); + await expectNoTemporaryArtifacts(); + }); + + it("serializes overlapping writes to the same project", async () => { + await writeProjectFileAtomically(projectPath, '{"revision":1}'); + + // Each call enters the queue synchronously before its first await, preserving invocation order. + await Promise.all([ + writeProjectFileAtomically(projectPath, '{"revision":2}'), + writeProjectFileAtomically(projectPath, '{"revision":3}'), + ]); + + await expect(fs.readFile(projectPath, "utf-8")).resolves.toBe('{"revision":3}'); + await expect(fs.readFile(getProjectBackupPath(projectPath), "utf-8")).resolves.toBe( + '{"revision":2}', + ); + await expectNoTemporaryArtifacts(); + }); + + it.skipIf(process.platform === "win32")( + "preserves exact project permission bits despite the process umask", + async () => { + await fs.writeFile(projectPath, '{"revision":1}', { mode: 0o666 }); + await fs.chmod(projectPath, 0o666); + const previousUmask = process.umask(0o077); + + try { + await writeProjectFileAtomically(projectPath, '{"revision":2}'); + } finally { + process.umask(previousUmask); + } + + expect((await fs.stat(projectPath)).mode & 0o777).toBe(0o666); + }, + ); +}); diff --git a/electron/ipc/project/atomicSave.ts b/electron/ipc/project/atomicSave.ts new file mode 100644 index 000000000..d2440ffe5 --- /dev/null +++ b/electron/ipc/project/atomicSave.ts @@ -0,0 +1,146 @@ +import { randomUUID } from "node:crypto"; +import { constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; + +const pendingWrites = new Map>(); + +const unsupportedDirectorySyncErrors = new Set([ + "EACCES", + "EINVAL", + "EISDIR", + "ENOSYS", + "ENOTSUP", + "EOPNOTSUPP", + "EPERM", +]); + +export function getProjectBackupPath(projectPath: string): string { + return `${projectPath}.bak`; +} + +function getQueueKey(projectPath: string): string { + const resolvedPath = path.resolve(projectPath); + return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath; +} + +function createTemporaryPath(parentDir: string, label: string): string { + return path.join(parentDir, `.recordly-${label}-${process.pid}-${randomUUID()}.tmp`); +} + +async function getExistingFileMode(filePath: string): Promise { + try { + return (await fs.stat(filePath)).mode & 0o777; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return undefined; + } + throw error; + } +} + +async function writeSyncedTemporaryFile( + filePath: string, + contents: string, + mode?: number, +): Promise { + const handle = await fs.open(filePath, "wx", mode); + try { + await handle.writeFile(contents, "utf-8"); + if (mode !== undefined) { + await handle.chmod(mode); + } + await handle.sync(); + } finally { + await handle.close(); + } +} + +async function syncExistingFile(filePath: string): Promise { + const handle = await fs.open(filePath, "r+"); + try { + await handle.sync(); + } finally { + await handle.close(); + } +} + +async function syncParentDirectory(parentDir: string): Promise { + if (process.platform === "win32") { + return; + } + + try { + const handle = await fs.open(parentDir, "r"); + try { + await handle.sync(); + } finally { + await handle.close(); + } + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (!code || !unsupportedDirectorySyncErrors.has(code)) { + throw error; + } + } +} + +async function preservePreviousGeneration( + targetPath: string, + backupPath: string, + backupTemporaryPath: string, +): Promise { + try { + await fs.copyFile(targetPath, backupTemporaryPath, fsConstants.COPYFILE_EXCL); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + await fs.rm(backupPath, { force: true }); + return; + } + throw error; + } + + await syncExistingFile(backupTemporaryPath); + await fs.rename(backupTemporaryPath, backupPath); +} + +async function commitProjectFile(projectPath: string, contents: string): Promise { + const targetPath = path.resolve(projectPath); + const parentDir = path.dirname(targetPath); + const backupPath = getProjectBackupPath(targetPath); + const temporaryPath = createTemporaryPath(parentDir, "project"); + const backupTemporaryPath = createTemporaryPath(parentDir, "backup"); + const existingMode = await getExistingFileMode(targetPath); + + try { + await writeSyncedTemporaryFile(temporaryPath, contents, existingMode); + await preservePreviousGeneration(targetPath, backupPath, backupTemporaryPath); + await fs.rename(temporaryPath, targetPath); + await syncParentDirectory(parentDir); + } finally { + await Promise.all([ + fs.rm(temporaryPath, { force: true }).catch(() => undefined), + fs.rm(backupTemporaryPath, { force: true }).catch(() => undefined), + ]); + } +} + +export async function writeProjectFileAtomically( + projectPath: string, + contents: string, +): Promise { + const queueKey = getQueueKey(projectPath); + const previousWrite = pendingWrites.get(queueKey) ?? Promise.resolve(); + const currentWrite = previousWrite + .catch(() => undefined) + .then(() => commitProjectFile(projectPath, contents)); + pendingWrites.set(queueKey, currentWrite); + + try { + await currentWrite; + } finally { + if (pendingWrites.get(queueKey) === currentWrite) { + pendingWrites.delete(queueKey); + } + } +} diff --git a/electron/ipc/project/createUntitledProject.test.ts b/electron/ipc/project/createUntitledProject.test.ts new file mode 100644 index 000000000..261da980b --- /dev/null +++ b/electron/ipc/project/createUntitledProject.test.ts @@ -0,0 +1,26 @@ +import { afterEach, expect, it } from "vitest"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createUntitledProject } from "./createUntitledProject"; +const dirs: string[] = []; +afterEach(async () => { + await Promise.all(dirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); +}); +it("publishes complete, uniquely named projects under concurrent creation without replacing existing projects", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-new-")); + dirs.push(dir); + await fs.writeFile(path.join(dir, "Untitled Project.recordly"), "original"); + const paths = await Promise.all( + [1, 2, 3].map((n) => createUntitledProject(dir, JSON.stringify({ id: n }))), + ); + expect(paths.map((p) => path.basename(p)).sort()).toEqual([ + "Untitled Project 1.recordly", + "Untitled Project 2.recordly", + "Untitled Project 3.recordly", + ]); + expect(await fs.readFile(path.join(dir, "Untitled Project.recordly"), "utf8")).toBe("original"); + for (let i = 0; i < paths.length; i++) + expect(JSON.parse(await fs.readFile(paths[i], "utf8"))).toEqual({ id: i + 1 }); + expect((await fs.readdir(dir)).filter((p) => p.endsWith(".tmp"))).toEqual([]); +}); diff --git a/electron/ipc/project/createUntitledProject.ts b/electron/ipc/project/createUntitledProject.ts new file mode 100644 index 000000000..b7bc37792 --- /dev/null +++ b/electron/ipc/project/createUntitledProject.ts @@ -0,0 +1,31 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; +/** Publish a complete new project without overwriting a concurrent or existing save. */ +export async function createUntitledProject(directory: string, contents: string) { + await fs.mkdir(directory, { recursive: true }); + const temporary = path.join(directory, `.recordly-new-${randomUUID()}.tmp`); + try { + const file = await fs.open(temporary, "wx"); + try { + await file.writeFile(contents, "utf8"); + await file.sync(); + } finally { + await file.close(); + } + for (let suffix = 0; ; suffix++) { + const target = path.join( + directory, + `Untitled Project${suffix ? ` ${suffix}` : ""}.recordly`, + ); + try { + await fs.link(temporary, target); + return target; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; + } + } + } finally { + await fs.rm(temporary, { force: true }); + } +} diff --git a/electron/ipc/project/firstFrameThumbnail.ts b/electron/ipc/project/firstFrameThumbnail.ts new file mode 100644 index 000000000..403f5e950 --- /dev/null +++ b/electron/ipc/project/firstFrameThumbnail.ts @@ -0,0 +1,45 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; +import { + PROJECT_THUMBNAIL_WIDTH, + PROJECT_THUMBNAIL_HEIGHT, +} from "../../../src/lib/projectThumbnail"; +const run = promisify(execFile); +let queue = Promise.resolve(); + +export function createProjectFirstFrameThumbnail(sourcePath: string): Promise { + const task = queue.then(async () => { + const result = await run( + getFfmpegBinaryPath(), + [ + "-hide_banner", + "-loglevel", + "error", + "-nostdin", + "-threads", + "1", + "-i", + sourcePath, + "-frames:v", + "1", + "-an", + "-vf", + `scale=${PROJECT_THUMBNAIL_WIDTH}:${PROJECT_THUMBNAIL_HEIGHT}:force_original_aspect_ratio=increase,crop=${PROJECT_THUMBNAIL_WIDTH}:${PROJECT_THUMBNAIL_HEIGHT}`, + "-f", + "image2pipe", + "-c:v", + "png", + "pipe:1", + ], + { encoding: "buffer", maxBuffer: 4 * 1024 * 1024, timeout: 15000, windowsHide: true }, + ); + if (!result.stdout.length) throw new Error("Project source has no preview frame"); + return result.stdout; + }); + queue = task.then( + () => undefined, + () => undefined, + ); + return task; +} diff --git a/electron/ipc/project/manager.test.ts b/electron/ipc/project/manager.test.ts new file mode 100644 index 000000000..682ed9454 --- /dev/null +++ b/electron/ipc/project/manager.test.ts @@ -0,0 +1,368 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("local media path policy", () => { + let tempRoot: string; + let appDataPath: string; + let userDataPath: string; + let tempPath: string; + let appPath: string; + + beforeEach(async () => { + tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-media-policy-")); + appDataPath = path.join(tempRoot, "AppData"); + userDataPath = path.join(tempRoot, "UserData"); + tempPath = path.join(tempRoot, "Temp"); + appPath = path.join(tempRoot, "App"); + + await Promise.all( + [appDataPath, userDataPath, tempPath, appPath].map((dirPath) => + fs.mkdir(dirPath, { recursive: true }), + ), + ); + + vi.resetModules(); + vi.doMock("electron", () => ({ + app: { + isPackaged: false, + getAppPath: () => appPath, + getPath: (name: string) => { + if (name === "appData") return appDataPath; + if (name === "userData") return userDataPath; + if (name === "temp") return tempPath; + return tempRoot; + }, + setPath: () => undefined, + }, + })); + }); + + afterEach(async () => { + vi.resetModules(); + vi.doUnmock("electron"); + vi.doUnmock("../../mediaServer"); + vi.doUnmock("./firstFrameThumbnail"); + if (tempRoot) { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + + it("reads library previews without switching the active project and rejects unknown projects", async () => { + vi.doMock("../../mediaServer", () => ({ + getMediaServerBaseUrl: () => "http://127.0.0.1:1234", + buildMediaUrl: (base: string, file: string) => + `${base}/media?path=${encodeURIComponent(file)}`, + })); + const manager = await import("./manager"); + const state = await import("../state"); + const source = path.join(tempRoot, "video.mp4"); + await fs.writeFile(source, "video"); + const projectsDir = await manager.getProjectsDir(); + const projectPath = path.join(projectsDir, "preview.recordly"); + await fs.writeFile( + projectPath, + JSON.stringify({ version: 1, videoPath: source, editor: {} }), + ); + state.setCurrentProjectPath("active.recordly"); + state.setCurrentVideoPath("active.mp4"); + const result = await manager.readProjectPreview(projectPath); + expect(result.videoUrl).toContain(encodeURIComponent(source)); + expect(result.webcamUrl).toBeNull(); + expect(state.currentProjectPath).toBe("active.recordly"); + expect(state.currentVideoPath).toBe("active.mp4"); + await expect( + manager.readProjectPreview(path.join(tempRoot, "unknown.recordly")), + ).rejects.toThrow("not in the library"); + }); + + it("rejects existing media files outside allowed directories until they are approved", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const exportPath = path.join(downloadsPath, "export-test.mp4"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(exportPath, "test-video"); + + const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import( + "./manager" + ); + + await expect(isAllowedLocalMediaPath(exportPath)).resolves.toBe(false); + + await rememberApprovedLocalReadPath(exportPath); + + await expect(isAllowedLocalMediaPath(exportPath)).resolves.toBe(true); + }); + + it("rejects missing media files outside the allowed directories", async () => { + const missingPath = path.join(tempRoot, "Downloads", "missing.mp4"); + const { isAllowedLocalMediaPath } = await import("./manager"); + + await expect(isAllowedLocalMediaPath(missingPath)).resolves.toBe(false); + }); + + it("allows approved media paths before the file exists", async () => { + const pendingExportPath = path.join(tempRoot, "Downloads", "pending-export.mp4"); + const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import( + "./manager" + ); + + await rememberApprovedLocalReadPath(pendingExportPath); + + await expect(isAllowedLocalMediaPath(pendingExportPath)).resolves.toBe(true); + }); + + it("approves media-server access for approved external files resolved through the URL policy", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const videoPath = path.join(downloadsPath, "external-video.mp4"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(videoPath, "test-video"); + const resolvedVideoPath = await fs.realpath(videoPath); + + const { resolveApprovedLocalMediaPath, rememberApprovedLocalReadPath } = await import( + "./manager" + ); + const { isAllowedMediaPath } = await import("../../mediaServer"); + + // Unapproved external paths are rejected before they ever reach the media server. + expect(isAllowedMediaPath(videoPath)).toBe(false); + await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBeNull(); + + // Once the user opts in (via dialog/export/etc.) the path is approved. + await rememberApprovedLocalReadPath(videoPath); + + await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBe(resolvedVideoPath); + expect(isAllowedMediaPath(videoPath)).toBe(true); + }); + + it("rejects existing non-media files when resolving local media URLs", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const textPath = path.join(downloadsPath, "notes.txt"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(textPath, "not media"); + + const { resolveApprovedLocalMediaPath } = await import("./manager"); + const { isAllowedMediaPath } = await import("../../mediaServer"); + + await expect(resolveApprovedLocalMediaPath(textPath)).resolves.toBeNull(); + expect(isAllowedMediaPath(textPath)).toBe(false); + }); + + it("rejects symlinks under allowed prefixes that point outside the allowlist", async () => { + const outsideTarget = path.join(tempRoot, "outside-secret.mp4"); + const symlinkInsideUserData = path.join(userDataPath, "shortcut-to-secret.mp4"); + await fs.writeFile(outsideTarget, "secret-bytes"); + + try { + await fs.symlink(outsideTarget, symlinkInsideUserData); + } catch (error) { + // Windows requires Developer Mode or admin to create file symlinks. If + // we can't create one, the bypass we're guarding against also can't be + // crafted on this machine, so skipping is safe. + if ((error as NodeJS.ErrnoException).code === "EPERM") { + return; + } + throw error; + } + + const { isAllowedLocalMediaPath, resolveApprovedLocalMediaPath } = await import( + "./manager" + ); + + await expect(isAllowedLocalMediaPath(symlinkInsideUserData)).resolves.toBe(false); + await expect(resolveApprovedLocalMediaPath(symlinkInsideUserData)).resolves.toBeNull(); + }); + + it("generates a missing project poster once and keeps it cached", async () => { + const png = Buffer.alloc(24); + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(png); + png.writeUInt32BE(640, 16); + png.writeUInt32BE(480, 20); + const generate = vi.fn().mockResolvedValue(png); + vi.doMock("./firstFrameThumbnail", () => ({ createProjectFirstFrameThumbnail: generate })); + const manager = await import("./manager"); + const source = path.join(tempRoot, "source.mp4"); + const projectPath = path.join(tempRoot, "test.recordly"); + await fs.writeFile(source, "video"); + await fs.writeFile( + projectPath, + JSON.stringify({ version: 2, videoPath: source, editor: {} }), + ); + const before = await fs.readFile(projectPath, "utf8"); + const entry = await manager.buildProjectLibraryEntry(projectPath, tempRoot); + expect(entry?.thumbnailPath).toBeNull(); + await manager.refreshProjectThumbnail(projectPath, await fs.stat(projectPath)); + expect((await manager.buildProjectLibraryEntry(projectPath, tempRoot))?.thumbnailPath).toBe( + manager.getProjectThumbnailPath(projectPath), + ); + expect(generate).toHaveBeenCalledWith(source); + await manager.buildProjectLibraryEntry(projectPath, tempRoot); + expect(generate).toHaveBeenCalledTimes(1); + expect(await fs.readFile(projectPath, "utf8")).toBe(before); + }); + + it("preserves an existing project thumbnail when no replacement is provided", async () => { + const projectPath = path.join(tempRoot, "Projects", "demo.recordly"); + const thumbnailDataUrl = `data:image/png;base64,${Buffer.from("png-thumbnail").toString("base64")}`; + await fs.mkdir(path.dirname(projectPath), { recursive: true }); + + const { getProjectThumbnailPath, saveProjectThumbnail } = await import("./manager"); + const thumbnailPath = getProjectThumbnailPath(projectPath); + + await saveProjectThumbnail(projectPath, thumbnailDataUrl); + await saveProjectThumbnail(projectPath, undefined); + + await expect(fs.readFile(thumbnailPath, "utf8")).resolves.toBe("png-thumbnail"); + }); + + it("loads project files that start with a UTF-8 byte order mark", async () => { + const videoPath = path.join(tempPath, "recording.mp4"); + const projectPath = path.join(tempPath, "recording.recordly"); + await fs.writeFile(videoPath, "test-video"); + await fs.writeFile( + projectPath, + `\uFEFF${JSON.stringify({ + version: 1, + videoPath, + editor: {}, + })}`, + "utf-8", + ); + + const { loadProjectFromPath } = await import("./manager"); + + const result = await loadProjectFromPath(projectPath); + expect(result.success).toBe(true); + expect(result.path).toBe(projectPath); + expect(result.project).toMatchObject({ videoPath }); + }); + + it("rejects invalid project payloads before approving media paths", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const videoPath = path.join(downloadsPath, "recording.mp4"); + const projectPath = path.join(tempPath, "invalid.recordly"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(videoPath, "test-video"); + await fs.writeFile( + projectPath, + JSON.stringify({ + videoPath, + editor: {}, + }), + "utf-8", + ); + + const { loadProjectFromPath, resolveApprovedLocalMediaPath } = await import("./manager"); + + const result = await loadProjectFromPath(projectPath); + expect(result.success).toBe(false); + expect(result.message).toBe("Invalid project file format"); + await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBeNull(); + }); + + it("approves editor audioRegions audioPath entries when loading a project", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const videoPath = path.join(tempPath, "recording.mp4"); + const audioPath = path.join(downloadsPath, "music.ogg"); + const projectPath = path.join(tempPath, "recording.recordly"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(videoPath, "test-video"); + await fs.writeFile(audioPath, "test-audio"); + await fs.writeFile( + projectPath, + JSON.stringify({ + version: 1, + videoPath, + editor: { + audioRegions: [{ id: "a1", startMs: 0, endMs: 1000, audioPath, volume: 1 }], + }, + }), + "utf-8", + ); + + const { loadProjectFromPath, resolveApprovedLocalMediaPath } = await import("./manager"); + const resolvedAudioPath = await fs.realpath(audioPath); + + const result = await loadProjectFromPath(projectPath); + expect(result.success).toBe(true); + await expect(resolveApprovedLocalMediaPath(audioPath)).resolves.toBe(resolvedAudioPath); + }); + it("returns a library entry while a thumbnail decode is still pending", async () => { + let release!: (data: Buffer) => void; + const pending = new Promise((resolve) => { + release = resolve; + }); + const generate = vi.fn(() => pending); + vi.doMock("./firstFrameThumbnail", () => ({ createProjectFirstFrameThumbnail: generate })); + const manager = await import("./manager"); + const source = path.join(tempRoot, "slow.mp4"); + const projectPath = path.join(tempRoot, "slow.recordly"); + await fs.writeFile(source, "video"); + await fs.writeFile( + projectPath, + JSON.stringify({ version: 2, videoPath: source, editor: {} }), + ); + const ready = vi.fn(); + const entry = await manager.buildProjectLibraryEntry(projectPath, tempRoot, ready); + expect(entry?.path).toBe(projectPath); + expect(entry?.thumbnailPath).toBeNull(); + const task = manager.refreshProjectThumbnail(projectPath, await fs.stat(projectPath)); + expect(ready).not.toHaveBeenCalled(); + release(Buffer.from("fixture thumbnail")); + await task; + expect(generate).toHaveBeenCalledTimes(1); + expect(ready).toHaveBeenCalledWith(expect.objectContaining({ path: projectPath })); + }); + + it("replaces a thumbnail symlink without overwriting its target", async () => { + vi.doMock("./firstFrameThumbnail", () => ({ + createProjectFirstFrameThumbnail: vi.fn().mockResolvedValue(Buffer.from("new poster")), + })); + const manager = await import("./manager"); + const source = path.join(tempRoot, "source.mp4"); + const projectPath = path.join(tempRoot, "linked.recordly"); + const target = path.join(tempRoot, "private.txt"); + await fs.writeFile(source, "video"); + await fs.writeFile( + projectPath, + JSON.stringify({ version: 2, videoPath: source, editor: {} }), + ); + await fs.writeFile(target, "must remain intact"); + const thumbnail = manager.getProjectThumbnailPath(projectPath); + try { + await fs.symlink(target, thumbnail); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EPERM") return; + throw error; + } + await manager.refreshProjectThumbnail(projectPath, await fs.stat(projectPath)); + expect(await fs.readFile(target, "utf8")).toBe("must remain intact"); + expect((await fs.lstat(thumbnail)).isSymbolicLink()).toBe(false); + expect(await fs.readFile(thumbnail, "utf8")).toBe("new poster"); + }); + + it("discards a thumbnail when the project changes during decoding", async () => { + let release!: (data: Buffer) => void; + const pending = new Promise((resolve) => { + release = resolve; + }); + const generate = vi.fn(() => pending); + vi.doMock("./firstFrameThumbnail", () => ({ createProjectFirstFrameThumbnail: generate })); + const manager = await import("./manager"); + const source = path.join(tempRoot, "source.mp4"); + const projectPath = path.join(tempRoot, "edited.recordly"); + await fs.writeFile(source, "video"); + await fs.writeFile( + projectPath, + JSON.stringify({ version: 2, videoPath: source, editor: {} }), + ); + const task = manager.refreshProjectThumbnail(projectPath, await fs.stat(projectPath)); + await vi.waitFor(() => expect(generate).toHaveBeenCalled()); + await fs.appendFile(projectPath, " "); + release(Buffer.from("obsolete poster")); + expect(await task).toBeNull(); + await expect(fs.stat(manager.getProjectThumbnailPath(projectPath))).rejects.toMatchObject({ + code: "ENOENT", + }); + }); +}); diff --git a/electron/ipc/project/manager.ts b/electron/ipc/project/manager.ts new file mode 100644 index 000000000..4b066aba5 --- /dev/null +++ b/electron/ipc/project/manager.ts @@ -0,0 +1,584 @@ +import { randomUUID } from "node:crypto"; +import { createProjectFirstFrameThumbnail } from "./firstFrameThumbnail"; +import { buildMediaUrl, getMediaServerBaseUrl } from "../../mediaServer"; +import type { ProjectPreviewData } from "../../../src/types/projectPreview"; +import { hasFreshProjectThumbnail } from "./thumbnailFreshness"; +import { type Stats, existsSync, constants as fsConstants, realpathSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { app } from "electron"; +import { RECORDINGS_DIR, USER_DATA_PATH } from "../../appPaths"; +import { isSupportedLocalMediaPath } from "../../mediaTypes"; +import { + LEGACY_PROJECT_FILE_EXTENSIONS, + MAX_RECENT_PROJECTS, + PROJECT_FILE_EXTENSION, + PROJECT_THUMBNAIL_SUFFIX, + PROJECTS_DIRECTORY_NAME, + RECENT_PROJECTS_FILE, + RECORDINGS_SETTINGS_FILE, +} from "../constants"; +import { + approvedLocalReadPaths, + currentProjectPath, + setCurrentProjectPath, + setCurrentRecordingSession, + setCurrentVideoPath, + setCustomRecordingsDir, + setRecordingsDirLoaded, +} from "../state"; +import type { ProjectLibraryEntry, RecordingSessionData } from "../types"; +import { + getRecordingsDir, + normalizePath, + normalizeVideoSourcePath, + parseJsonWithByteOrderMark, +} from "../utils"; + +export { normalizePath, normalizeVideoSourcePath }; + +export function getAssetRootPath() { + if (app.isPackaged) { + return path.join(process.resourcesPath, "assets"); + } + + return path.join(app.getAppPath(), "public"); +} + +export function isPathInsideDirectory(candidatePath: string, directoryPath: string) { + const normalizedCandidatePath = normalizePath(candidatePath); + const normalizedDirectoryPath = normalizePath(directoryPath); + return ( + normalizedCandidatePath === normalizedDirectoryPath || + normalizedCandidatePath.startsWith(`${normalizedDirectoryPath}${path.sep}`) + ); +} + +export function isAllowedLocalReadPath(candidatePath: string) { + const allowedPrefixes = [ + RECORDINGS_DIR, + USER_DATA_PATH, + getAssetRootPath(), + app.getPath("temp"), + ]; + const normalizedCandidatePath = normalizePath(candidatePath); + + // Canonicalize so a symlink placed under an allowed prefix can't smuggle in a + // target that lives outside it. realpathSync throws when the path doesn't + // exist yet (e.g. a pending export approved before the file is written) — in + // that case fall back to the lexical path, which can only succeed via the + // approvedLocalReadPaths check below since no symlink target exists yet. + let canonicalCandidatePath = normalizedCandidatePath; + try { + canonicalCandidatePath = normalizePath(realpathSync(normalizedCandidatePath)); + } catch { + // File may not exist yet; keep the lexical path. + } + + // Security: only allow paths under app-managed directories or paths the user + // has explicitly opted into (recording session sources, files chosen via + // dialog, app-produced exports). The lexical path must satisfy the policy + // AND the canonical (real) path must satisfy it too, so a symlink under an + // allowed prefix that points outside the allowlist is rejected. Previously + // this returned true for any existing file, which made the allowlist a no-op + // for read-local-file and the local media URL handler. + const lexicalAllowed = + allowedPrefixes.some((prefix) => isPathInsideDirectory(normalizedCandidatePath, prefix)) || + approvedLocalReadPaths.has(normalizedCandidatePath); + if (!lexicalAllowed) { + return false; + } + + if (canonicalCandidatePath === normalizedCandidatePath) { + return true; + } + + return ( + allowedPrefixes.some((prefix) => isPathInsideDirectory(canonicalCandidatePath, prefix)) || + approvedLocalReadPaths.has(canonicalCandidatePath) + ); +} + +// Keep loopback media-server access restricted to allowlisted or explicitly +// approved files. Direct renderer-side read-local-file calls can be more +// permissive, but URL-based serving must stay scoped so arbitrary paths do not +// become fetchable inside the app. +export async function isAllowedLocalMediaPath(candidatePath: string) { + const normalizedCandidatePath = normalizePath(candidatePath); + return isAllowedLocalReadPath(normalizedCandidatePath); +} + +async function collectApprovedLocalReadPaths(filePath?: string | null): Promise { + const normalizedPath = normalizeVideoSourcePath(filePath); + if (!normalizedPath) { + return []; + } + + const approvedPaths = [normalizePath(normalizedPath)]; + + try { + const realPath = await fs.realpath(approvedPaths[0]); + const normalizedRealPath = normalizePath(realPath); + if (!approvedPaths.includes(normalizedRealPath)) { + approvedPaths.push(normalizedRealPath); + } + } catch { + // Ignore missing files; the eventual read will surface the real error. + } + + return approvedPaths; +} + +export async function rememberApprovedLocalReadPath(filePath?: string | null) { + const normalizedPath = normalizeVideoSourcePath(filePath); + if (!normalizedPath) { + return; + } + + const approvedPaths = await collectApprovedLocalReadPaths(normalizedPath); + for (const approvedPath of approvedPaths) { + approvedLocalReadPaths.add(approvedPath); + } +} + +export async function resolveApprovedLocalMediaPath(candidatePath: string): Promise { + const normalizedCandidatePath = normalizePath(candidatePath); + const realPath = await fs.realpath(normalizedCandidatePath).catch(() => null); + + if (!realPath) { + return null; + } + + const stat = await fs.stat(realPath).catch(() => null); + if (!stat?.isFile() || !isSupportedLocalMediaPath(realPath)) { + return null; + } + + if (!(await isAllowedLocalMediaPath(realPath))) { + return null; + } + + await rememberApprovedLocalReadPath(candidatePath); + return realPath; +} + +export async function replaceApprovedSessionLocalReadPaths( + filePaths: Array, +) { + const nextApprovedPaths = new Set(); + const approvedPathLists = await Promise.all( + filePaths.map((filePath) => collectApprovedLocalReadPaths(filePath)), + ); + + for (const approvedPathList of approvedPathLists) { + for (const approvedPath of approvedPathList) { + nextApprovedPaths.add(approvedPath); + } + } + + approvedLocalReadPaths.clear(); + for (const approvedPath of nextApprovedPaths) { + approvedLocalReadPaths.add(approvedPath); + } +} + +export async function resolveProjectMediaSources( + project: unknown, +): Promise< + | { success: true; videoPath: string; webcamPath: string | null } + | { success: false; message: string } +> { + if (!project || typeof project !== "object") { + return { success: false, message: "Invalid project file format" }; + } + + const rawVideoPath = (project as { videoPath?: unknown }).videoPath; + if (typeof rawVideoPath !== "string") { + return { success: false, message: "Project file is missing a video path" }; + } + + const normalizedVideoPath = normalizeVideoSourcePath(rawVideoPath); + if (!normalizedVideoPath) { + return { success: false, message: "Project file is missing a valid video path" }; + } + + try { + await fs.access(normalizedVideoPath, fsConstants.F_OK); + } catch { + return { + success: false, + message: `Project video file not found: ${normalizedVideoPath}`, + }; + } + + const rawWebcamPath = + typeof (project as { editor?: { webcam?: { sourcePath?: unknown } } }).editor?.webcam + ?.sourcePath === "string" + ? ((project as { editor?: { webcam?: { sourcePath?: string } } }).editor?.webcam + ?.sourcePath ?? null) + : null; + const normalizedWebcamPath = normalizeVideoSourcePath(rawWebcamPath); + + if (!normalizedWebcamPath) { + return { + success: true, + videoPath: normalizedVideoPath, + webcamPath: null, + }; + } + + try { + await fs.access(normalizedWebcamPath, fsConstants.F_OK); + return { + success: true, + videoPath: normalizedVideoPath, + webcamPath: normalizedWebcamPath, + }; + } catch { + return { + success: true, + videoPath: normalizedVideoPath, + webcamPath: null, + }; + } +} + +export async function getProjectsDir() { + const projectsDir = path.join(await getRecordingsDir(), PROJECTS_DIRECTORY_NAME); + await fs.mkdir(projectsDir, { recursive: true }); + return projectsDir; +} + +export async function persistRecordingsDirectorySetting(nextDir: string) { + setCustomRecordingsDir(path.resolve(nextDir)); + setRecordingsDirLoaded(true); + await fs.writeFile( + RECORDINGS_SETTINGS_FILE, + JSON.stringify({ recordingsDir: path.resolve(nextDir) }, null, 2), + "utf-8", + ); +} + +export function hasProjectFileExtension(filePath: string) { + const extension = path.extname(filePath).replace(/^\./, "").toLowerCase(); + return [PROJECT_FILE_EXTENSION, ...LEGACY_PROJECT_FILE_EXTENSIONS].includes(extension); +} + +export function getProjectThumbnailPath(projectPath: string) { + return `${projectPath}${PROJECT_THUMBNAIL_SUFFIX}`; +} + +export async function saveProjectThumbnail(projectPath: string, thumbnailDataUrl?: string | null) { + const thumbnailPath = getProjectThumbnailPath(projectPath); + if (thumbnailDataUrl === undefined) { + return existsSync(thumbnailPath) ? thumbnailPath : null; + } + + if (!thumbnailDataUrl) { + await fs.rm(thumbnailPath, { force: true }).catch(() => undefined); + return null; + } + + const match = thumbnailDataUrl.match(/^data:image\/png;base64,(.+)$/); + if (!match) { + throw new Error("Project thumbnail must be a PNG data URL."); + } + + await fs.writeFile(thumbnailPath, Buffer.from(match[1], "base64")); + return thumbnailPath; +} + +export async function loadRecentProjectPaths() { + try { + const content = await fs.readFile(RECENT_PROJECTS_FILE, "utf-8"); + const parsed = parseJsonWithByteOrderMark<{ paths?: unknown }>(content); + return Array.isArray(parsed.paths) + ? parsed.paths.filter( + (value): value is string => + typeof value === "string" && value.trim().length > 0, + ) + : []; + } catch { + return []; + } +} + +export async function saveRecentProjectPaths(paths: string[]) { + const normalizedPaths = Array.from(new Set(paths.map((value) => normalizePath(value)))).slice( + 0, + MAX_RECENT_PROJECTS, + ); + await fs.writeFile( + RECENT_PROJECTS_FILE, + JSON.stringify({ paths: normalizedPaths }, null, 2), + "utf-8", + ); +} + +export async function rememberRecentProject(projectPath: string) { + if (!hasProjectFileExtension(projectPath)) { + return; + } + + const existingPaths = await loadRecentProjectPaths(); + await saveRecentProjectPaths([projectPath, ...existingPaths]); +} + +export type ProjectThumbnailReady = { path: string; thumbnailPath: string; updatedAt: number }; +const thumbnailJobs = new Map>(); + +function sameProjectRevision(left: Stats, right: Stats) { + return ( + left.mtimeMs === right.mtimeMs && + left.ctimeMs === right.ctimeMs && + left.size === right.size && + left.ino === right.ino && + left.dev === right.dev + ); +} + +export function refreshProjectThumbnail(projectPath: string, revision: Stats) { + const pending = thumbnailJobs.get(projectPath); + if (pending) return pending; + const task = (async (): Promise => { + const thumbnailPath = getProjectThumbnailPath(projectPath); + let temporary: string | undefined; + try { + if (!sameProjectRevision(revision, await fs.stat(projectPath))) return null; + const project = parseJsonWithByteOrderMark(await fs.readFile(projectPath, "utf-8")); + if (!isLoadableProjectData(project)) return null; + const media = await resolveProjectMediaSources(project); + if (!media.success) return null; + const thumbnail = await createProjectFirstFrameThumbnail(media.videoPath); + if (!sameProjectRevision(revision, await fs.stat(projectPath))) return null; + if (!(await hasFreshProjectThumbnail(thumbnailPath, revision.mtimeMs))) { + temporary = `${thumbnailPath}.${randomUUID()}.tmp`; + await fs.writeFile(temporary, thumbnail, { flag: "wx" }); + // Stamp the source revision, rather than generation time, so a later edit invalidates it. + await fs.utimes(temporary, new Date(), Math.ceil(revision.mtimeMs) / 1000); + if (!sameProjectRevision(revision, await fs.stat(projectPath))) return null; + // Rename replaces a sidecar symlink itself; it never writes through its target. + await fs.rename(temporary, thumbnailPath); + } + return { path: projectPath, thumbnailPath, updatedAt: revision.mtimeMs }; + } catch { + // Offline, corrupt or deleted media must not hold up the library. + return null; + } finally { + if (temporary) await fs.rm(temporary, { force: true }).catch(() => undefined); + } + })().finally(() => thumbnailJobs.delete(projectPath)); + thumbnailJobs.set(projectPath, task); + return task; +} + +export async function buildProjectLibraryEntry( + projectPath: string, + projectsDir: string, + onThumbnailReady?: (ready: ProjectThumbnailReady) => void, +): Promise { + try { + const normalizedPath = normalizePath(projectPath); + if (!hasProjectFileExtension(normalizedPath)) { + return null; + } + + const stats = await fs.stat(normalizedPath); + if (!stats.isFile()) { + return null; + } + + const thumbnailPath = getProjectThumbnailPath(normalizedPath); + const thumbnailExists = await hasFreshProjectThumbnail(thumbnailPath, stats.mtimeMs); + if (!thumbnailExists) { + void refreshProjectThumbnail(normalizedPath, stats) + .then((ready) => { + if (ready) onThumbnailReady?.(ready); + }) + .catch(() => undefined); + } + + return { + path: normalizedPath, + name: path + .basename(normalizedPath) + .replace( + new RegExp( + `\\.(${[PROJECT_FILE_EXTENSION, ...LEGACY_PROJECT_FILE_EXTENSIONS].join("|")})$`, + "i", + ), + "", + ), + updatedAt: stats.mtimeMs, + createdAt: stats.birthtimeMs || stats.ctimeMs, + thumbnailPath: thumbnailExists ? thumbnailPath : null, + isCurrent: Boolean( + currentProjectPath && normalizePath(currentProjectPath) === normalizedPath, + ), + isInProjectsDirectory: path.dirname(normalizedPath) === normalizePath(projectsDir), + }; + } catch { + return null; + } +} + +export async function listProjectLibraryEntries( + onThumbnailReady?: (ready: ProjectThumbnailReady) => void, +) { + const projectsDir = await getProjectsDir(); + const projectPaths: string[] = []; + + try { + const entries = await fs.readdir(projectsDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile()) { + continue; + } + + const entryPath = path.join(projectsDir, entry.name); + if (hasProjectFileExtension(entryPath)) { + projectPaths.push(entryPath); + } + } + } catch { + // Ignore directory read failures and fall back to recent files. + } + + const recentProjectPaths = await loadRecentProjectPaths(); + const candidatePaths = Array.from(new Set([...projectPaths, ...recentProjectPaths])); + const entries = ( + await Promise.all( + candidatePaths.map((candidatePath) => + buildProjectLibraryEntry(candidatePath, projectsDir, onThumbnailReady), + ), + ) + ) + .filter((entry): entry is ProjectLibraryEntry => entry != null) + .sort((left, right) => right.updatedAt - left.updatedAt); + + await saveRecentProjectPaths(entries.map((entry) => entry.path)); + + return { + projectsDir, + entries, + }; +} + +function isLoadableProjectData(projectData: unknown) { + if (!projectData || typeof projectData !== "object" || Array.isArray(projectData)) { + return false; + } + + const candidate = projectData as { + version?: unknown; + projectId?: unknown; + videoPath?: unknown; + editor?: unknown; + }; + + return ( + typeof candidate.version === "number" && + (candidate.projectId === undefined || typeof candidate.projectId === "string") && + typeof candidate.videoPath === "string" && + candidate.videoPath.trim().length > 0 && + candidate.editor != null && + typeof candidate.editor === "object" && + !Array.isArray(candidate.editor) + ); +} + +/** Read a listed project's preview without changing the active project or recording session. */ +export async function readProjectPreview(projectPath: string): Promise { + if (typeof projectPath !== "string") throw new Error("Invalid project path"); + const normalizedPath = normalizePath(projectPath); + const { entries } = await listProjectLibraryEntries(); + if (!entries.some((entry) => entry.path === normalizedPath)) + throw new Error("Project is not in the library"); + const project = parseJsonWithByteOrderMark(await fs.readFile(normalizedPath, "utf-8")); + if (!isLoadableProjectData(project)) throw new Error("Invalid project file format"); + const media = await resolveProjectMediaSources(project); + if (!media.success) throw new Error(media.message); + const baseUrl = getMediaServerBaseUrl(); + if (!baseUrl) throw new Error("Media server is not ready"); + await rememberApprovedLocalReadPath(media.videoPath); + if (media.webcamPath) await rememberApprovedLocalReadPath(media.webcamPath); + return { + project: project as ProjectPreviewData["project"], + videoUrl: buildMediaUrl(baseUrl, media.videoPath), + webcamUrl: media.webcamPath ? buildMediaUrl(baseUrl, media.webcamPath) : null, + }; +} + +export async function loadProjectFromPath(projectPath: string) { + const normalizedPath = normalizePath(projectPath); + let project: unknown; + try { + const content = await fs.readFile(normalizedPath, "utf-8"); + project = parseJsonWithByteOrderMark(content); + } catch (error) { + return { + success: false, + canceled: false, + message: `Failed to read project file: ${error instanceof Error ? error.message : String(error)}`, + }; + } + if (!isLoadableProjectData(project)) { + return { + success: false, + canceled: false, + message: "Invalid project file format", + }; + } + const mediaSources = await resolveProjectMediaSources(project); + + if (!mediaSources.success) { + return { + success: false, + canceled: false, + message: mediaSources.message, + }; + } + const projectObj = project as Record; + const editorObj = projectObj?.editor as Record | undefined; + const audioTracks = editorObj?.audioTracks as { sourcePath?: unknown }[] | undefined; + const audioRegions = editorObj?.audioRegions as { audioPath?: unknown }[] | undefined; + const approvedProjectPaths: Array = [ + mediaSources.videoPath, + mediaSources.webcamPath, + ]; + if (Array.isArray(audioTracks)) { + for (const track of audioTracks) { + if (typeof track?.sourcePath === "string") { + approvedProjectPaths.push(track.sourcePath); + } + } + } + if (Array.isArray(audioRegions)) { + for (const region of audioRegions) { + if (typeof region?.audioPath === "string") { + approvedProjectPaths.push(region.audioPath); + } + } + } + await replaceApprovedSessionLocalReadPaths(approvedProjectPaths); + await rememberRecentProject(normalizedPath); + + setCurrentProjectPath(normalizedPath); + setCurrentVideoPath(mediaSources.videoPath); + setCurrentRecordingSession({ + videoPath: mediaSources.videoPath, + webcamPath: mediaSources.webcamPath, + timeOffsetMs: 0, + } as RecordingSessionData); + + return { + success: true, + path: normalizedPath, + project, + }; +} + +export function isTrustedProjectPath(filePath?: string | null): boolean { + if (!filePath || !currentProjectPath) return false; + return normalizePath(filePath) === normalizePath(currentProjectPath); +} diff --git a/electron/ipc/project/recentMetadata.test.ts b/electron/ipc/project/recentMetadata.test.ts new file mode 100644 index 000000000..c20f49dee --- /dev/null +++ b/electron/ipc/project/recentMetadata.test.ts @@ -0,0 +1,17 @@ +import { expect, it, vi } from "vitest"; +import { persistRecentMetadata } from "./recentMetadata"; +it("reports a metadata warning without rejecting an already committed operation", async () => { + const log = vi.spyOn(console, "warn").mockImplementation(() => undefined); + const result = await persistRecentMetadata(async () => { + throw Error("disk full"); + }); + expect(result).toContain("operation completed"); + expect(result).toContain("disk full"); + expect(log).toHaveBeenCalledOnce(); + log.mockRestore(); +}); +it("has no warning after a successful metadata update", async () => { + const update = vi.fn().mockResolvedValue(undefined); + await expect(persistRecentMetadata(update)).resolves.toBeUndefined(); + expect(update).toHaveBeenCalledOnce(); +}); diff --git a/electron/ipc/project/recentMetadata.ts b/electron/ipc/project/recentMetadata.ts new file mode 100644 index 000000000..174bf365c --- /dev/null +++ b/electron/ipc/project/recentMetadata.ts @@ -0,0 +1,13 @@ +/** Library history is secondary to a filesystem operation that already committed. */ +export async function persistRecentMetadata( + action: () => Promise, +): Promise { + try { + await action(); + return undefined; + } catch (error) { + const warning = `Project operation completed, but recent-project history could not be updated: ${String(error)}`; + console.warn(warning); + return warning; + } +} diff --git a/electron/ipc/project/renameLibraryProject.test.ts b/electron/ipc/project/renameLibraryProject.test.ts new file mode 100644 index 000000000..048b6cfb7 --- /dev/null +++ b/electron/ipc/project/renameLibraryProject.test.ts @@ -0,0 +1,32 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { expect, it } from "vitest"; +import { renameLibraryProject } from "./renameLibraryProject"; + +it("renames a library project with its preview without overwriting another project", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-rename-")); + const original = path.join(dir, "Original.recordly"), + target = path.join(dir, "Renamed.recordly"); + const sidecars = (file: string) => [`${file}.png`]; + try { + await fs.writeFile(original, "project"); + await fs.writeFile(`${original}.png`, "preview"); + await fs.writeFile(target, "keep"); + await expect( + renameLibraryProject(original, "Renamed", [original], sidecars), + ).rejects.toThrow(); + expect(await fs.readFile(target, "utf8")).toBe("keep"); + await fs.rm(target); + await expect( + renameLibraryProject(original, "../escape", [original], sidecars), + ).rejects.toThrow(); + await expect(renameLibraryProject(original, "Renamed", [], sidecars)).rejects.toThrow(); + expect(await renameLibraryProject(original, "Renamed", [original], sidecars)).toBe(target); + expect(await fs.readFile(target, "utf8")).toBe("project"); + expect(await fs.readFile(`${target}.png`, "utf8")).toBe("preview"); + await expect(fs.access(original)).rejects.toThrow(); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } +}); diff --git a/electron/ipc/project/renameLibraryProject.ts b/electron/ipc/project/renameLibraryProject.ts new file mode 100644 index 000000000..476646f01 --- /dev/null +++ b/electron/ipc/project/renameLibraryProject.ts @@ -0,0 +1,47 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +export async function renameLibraryProject( + source: string, + name: string, + allowedPaths: string[], + sidecars: (path: string) => string[], +) { + if (!allowedPaths.includes(source)) throw new Error("Project is not in the library"); + const clean = name.trim(); + if ( + !clean || + clean === "." || + clean === ".." || + /[<>:"/\\|?*]/.test(clean) || + [...clean].some((char) => char.charCodeAt(0) < 32) || + /[. ]$/.test(clean) + ) + throw new Error("Choose a valid project name"); + const target = path.join(path.dirname(source), `${clean}${path.extname(source)}`); + if (target === source) return target; + // An exclusive link prevents a rename from overwriting another project. + await fs.link(source, target); + const copied: string[] = []; + try { + const previous = sidecars(source), + next = sidecars(target); + for (let i = 0; i < previous.length; i++) { + try { + await fs.copyFile(previous[i], next[i], fs.constants.COPYFILE_EXCL); + copied.push(next[i]); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + } + await fs.unlink(source); + } catch (error) { + await Promise.all([target, ...copied].map((file) => fs.rm(file, { force: true }))); + throw error; + } + // Auxiliary files can be cleaned up independently once the project has moved. + await Promise.all( + sidecars(source).map((file) => fs.rm(file, { force: true }).catch(() => undefined)), + ); + return target; +} diff --git a/electron/ipc/project/session.ts b/electron/ipc/project/session.ts new file mode 100644 index 000000000..7b2131aee --- /dev/null +++ b/electron/ipc/project/session.ts @@ -0,0 +1,140 @@ +import { constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { RECORDING_SESSION_MANIFEST_SUFFIX } from "../constants"; +import type { RecordingSessionData, RecordingSessionManifest } from "../types"; +import { normalizeVideoSourcePath, parseJsonWithByteOrderMark } from "../utils"; + +function normalizeRecordingTimeOffsetMs(value: unknown): number { + return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0; +} + +export function getRecordingSessionManifestPath(videoPath: string) { + const extension = path.extname(videoPath); + const baseName = path.basename(videoPath, extension); + return path.join(path.dirname(videoPath), `${baseName}${RECORDING_SESSION_MANIFEST_SUFFIX}`); +} + +export async function persistRecordingSessionManifest( + session: RecordingSessionData, +): Promise { + const normalizedVideoPath = normalizeVideoSourcePath(session.videoPath); + if (!normalizedVideoPath) { + return; + } + + const normalizedWebcamPath = normalizeVideoSourcePath(session.webcamPath ?? null); + const manifestPath = getRecordingSessionManifestPath(normalizedVideoPath); + + if (!normalizedWebcamPath) { + await fs.rm(manifestPath, { force: true }); + return; + } + + const manifest: RecordingSessionManifest = { + version: 2, + videoFileName: path.basename(normalizedVideoPath), + webcamFileName: path.basename(normalizedWebcamPath), + timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs), + }; + + await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); +} + +export async function resolveRecordingSessionManifest( + videoPath?: string | null, +): Promise { + const normalizedVideoPath = normalizeVideoSourcePath(videoPath); + if (!normalizedVideoPath) { + return null; + } + + const manifestPath = getRecordingSessionManifestPath(normalizedVideoPath); + + try { + const content = await fs.readFile(manifestPath, "utf-8"); + const parsed = parseJsonWithByteOrderMark>(content); + if (parsed.version !== 1 && parsed.version !== 2) { + return null; + } + + const webcamFileName = + typeof parsed.webcamFileName === "string" && parsed.webcamFileName.trim() + ? parsed.webcamFileName.trim() + : null; + if (!webcamFileName) { + return { + videoPath: normalizedVideoPath, + webcamPath: null, + timeOffsetMs: normalizeRecordingTimeOffsetMs(parsed.timeOffsetMs), + }; + } + + const webcamPath = path.join(path.dirname(normalizedVideoPath), webcamFileName); + const webcamExists = await fs + .access(webcamPath, fsConstants.F_OK) + .then(() => true) + .catch(() => false); + + return { + videoPath: normalizedVideoPath, + webcamPath: webcamExists ? webcamPath : null, + timeOffsetMs: normalizeRecordingTimeOffsetMs(parsed.timeOffsetMs), + }; + } catch { + return null; + } +} + +export async function resolveLinkedWebcamPath(videoPath?: string | null): Promise { + const normalizedVideoPath = normalizeVideoSourcePath(videoPath); + if (!normalizedVideoPath) { + return null; + } + + const extension = path.extname(normalizedVideoPath); + const baseName = path.basename(normalizedVideoPath, extension); + if (!baseName || baseName.endsWith("-webcam")) { + return null; + } + + const candidateExtensions = Array.from( + new Set([extension, ".webm", ".mp4", ".mov", ".mkv", ".avi"].filter(Boolean)), + ); + + for (const candidateExtension of candidateExtensions) { + const candidatePath = path.join( + path.dirname(normalizedVideoPath), + `${baseName}-webcam${candidateExtension}`, + ); + + try { + await fs.access(candidatePath, fsConstants.F_OK); + return candidatePath; + } catch { + continue; + } + } + + return null; +} + +export async function resolveRecordingSession( + videoPath?: string | null, +): Promise { + const manifestSession = await resolveRecordingSessionManifest(videoPath); + if (manifestSession) { + return manifestSession; + } + + const normalizedVideoPath = normalizeVideoSourcePath(videoPath); + if (!normalizedVideoPath) { + return null; + } + + const linkedWebcamPath = await resolveLinkedWebcamPath(normalizedVideoPath); + return { + videoPath: normalizedVideoPath, + webcamPath: linkedWebcamPath, + }; +} diff --git a/electron/ipc/project/thumbnailFreshness.test.ts b/electron/ipc/project/thumbnailFreshness.test.ts new file mode 100644 index 000000000..c70c576b1 --- /dev/null +++ b/electron/ipc/project/thumbnailFreshness.test.ts @@ -0,0 +1,32 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { expect, it } from "vitest"; +import { hasFreshProjectThumbnail } from "./thumbnailFreshness"; + +it("rejects legacy, stale, missing and broken previews while accepting a fresh current-size or larger PNG", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-preview-")); + const file = path.join(dir, "preview.png"); + try { + expect(await hasFreshProjectThumbnail(file, 0)).toBe(false); + const header = Buffer.alloc(24); + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(header); + header.writeUInt32BE(320, 16); + header.writeUInt32BE(180, 20); + await fs.writeFile(file, header); + expect(await hasFreshProjectThumbnail(file, 0)).toBe(false); + header.writeUInt32BE(640, 16); + header.writeUInt32BE(480, 20); + await fs.writeFile(file, header); + expect(await hasFreshProjectThumbnail(file, 0)).toBe(true); + header.writeUInt32BE(1600, 16); + header.writeUInt32BE(1200, 20); + await fs.writeFile(file, header); + expect(await hasFreshProjectThumbnail(file, 0)).toBe(true); + expect(await hasFreshProjectThumbnail(file, Date.now() + 10000)).toBe(false); + await fs.writeFile(file, "broken"); + expect(await hasFreshProjectThumbnail(file, 0)).toBe(false); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } +}); diff --git a/electron/ipc/project/thumbnailFreshness.ts b/electron/ipc/project/thumbnailFreshness.ts new file mode 100644 index 000000000..89f33cf62 --- /dev/null +++ b/electron/ipc/project/thumbnailFreshness.ts @@ -0,0 +1,28 @@ +import fs from "node:fs/promises"; +import { + PROJECT_THUMBNAIL_WIDTH, + PROJECT_THUMBNAIL_HEIGHT, +} from "../../../src/lib/projectThumbnail"; + +// Older releases wrote 320px previews. Hide those and previews predating edits; +// the editor replaces them with a current-size render when returning home. +export async function hasFreshProjectThumbnail(thumbnailPath: string, projectModifiedAt: number) { + let file: Awaited> | undefined; + try { + file = await fs.open(thumbnailPath, "r"); + const stat = await file.stat(); + if (stat.mtimeMs < projectModifiedAt) return false; + const header = Buffer.alloc(24); + const { bytesRead } = await file.read(header, 0, 24, 0); + return ( + bytesRead === 24 && + header.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) && + header.readUInt32BE(16) >= PROJECT_THUMBNAIL_WIDTH && + header.readUInt32BE(20) >= PROJECT_THUMBNAIL_HEIGHT + ); + } catch { + return false; + } finally { + await file?.close(); + } +} diff --git a/electron/ipc/project/trashProjects.test.ts b/electron/ipc/project/trashProjects.test.ts new file mode 100644 index 000000000..fc1107346 --- /dev/null +++ b/electron/ipc/project/trashProjects.test.ts @@ -0,0 +1,34 @@ +import { expect, it, vi } from "vitest"; +import { trashLibraryProjects } from "./trashProjects"; +it("rejects paths outside the project library before touching files", async () => { + const trash = vi.fn(); + await expect( + trashLibraryProjects(["/private/tmp/a.recordly", "/private/tmp/private.txt"], { + list: async () => ({ entries: [{ path: "/private/tmp/a.recordly" }] }), + trash, + thumbnailPath: (p) => p + ".png", + }), + ).rejects.toThrow("not in the library"); + expect(trash).not.toHaveBeenCalled(); +}); +it("trashes only selected project files, deduplicates paths, and reports partial failures", async () => { + const trash = vi.fn(async (p: string) => { + if (p.endsWith("b.recordly")) throw Error("locked"); + }); + const result = await trashLibraryProjects( + ["/private/tmp/a.recordly", "/private/tmp/a.recordly", "/private/tmp/b.recordly"], + { + list: async () => ({ + entries: [{ path: "/private/tmp/a.recordly" }, { path: "/private/tmp/b.recordly" }], + }), + trash, + thumbnailPath: (p) => p + ".missing.png", + }, + ); + expect(result.deleted).toEqual(["/private/tmp/a.recordly"]); + expect(result.errors).toEqual(["Could not trash b.recordly"]); + expect(trash.mock.calls.map(([p]) => p)).toEqual([ + "/private/tmp/a.recordly", + "/private/tmp/b.recordly", + ]); +}); diff --git a/electron/ipc/project/trashProjects.ts b/electron/ipc/project/trashProjects.ts new file mode 100644 index 000000000..8ef1e2c84 --- /dev/null +++ b/electron/ipc/project/trashProjects.ts @@ -0,0 +1,45 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import { getProjectBackupPath } from "./atomicSave"; + +type Dependencies = { + list: () => Promise<{ entries: Array<{ path: string }> }>; + trash: (path: string) => Promise; + thumbnailPath: (path: string) => string; +}; +/** Only library project files may be trashed; referenced recordings are never touched. */ +export async function trashLibraryProjects(paths: unknown, deps: Dependencies) { + if ( + !Array.isArray(paths) || + !paths.length || + paths.length > 500 || + paths.some((p) => typeof p !== "string") + ) + throw new Error("Invalid project selection"); + const allowed = new Set((await deps.list()).entries.map((e) => path.resolve(e.path))); + const selected = [...new Set((paths as string[]).map((p) => path.resolve(p)))]; + if (selected.some((p) => !allowed.has(p))) throw new Error("Project is not in the library"); + const deleted: string[] = []; + const errors: string[] = []; + for (const projectPath of selected) { + try { + await deps.trash(projectPath); + deleted.push(projectPath); + for (const sidecar of [ + deps.thumbnailPath(projectPath), + getProjectBackupPath(projectPath), + ]) { + try { + await fs.access(sidecar); + await deps.trash(sidecar); + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== "ENOENT") + errors.push(`Could not trash project sidecar: ${sidecar}`); + } + } + } catch { + errors.push(`Could not trash ${path.basename(projectPath)}`); + } + } + return { deleted, errors }; +} diff --git a/electron/ipc/recording/audioFilters.test.ts b/electron/ipc/recording/audioFilters.test.ts new file mode 100644 index 000000000..1e80e2665 --- /dev/null +++ b/electron/ipc/recording/audioFilters.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; + +import { + BROWSER_MIC_SIDECAR_FILTERS, + BROWSER_MIC_SIDECAR_NO_AGC_GAIN_FILTERS, + getBrowserMicSidecarFilters, + RECORDING_AUDIO_SIDECAR_DEBUG_ENV, + shouldKeepRecordingAudioSidecars, + WINDOWS_NATIVE_MIC_PRE_FILTERS, +} from "./audioFilters"; + +describe("Windows native mic pre-filter policy", () => { + it("keeps repair filters without automatic gain or loudness normalization", () => { + expect(WINDOWS_NATIVE_MIC_PRE_FILTERS).toContain("adeclip=threshold=1"); + expect( + WINDOWS_NATIVE_MIC_PRE_FILTERS.some((filter) => + /(^|,)(loudnorm|dynaudnorm|volume)=/i.test(filter), + ), + ).toBe(false); + }); + + it("keeps native audio sidecars only when explicitly requested", () => { + expect(shouldKeepRecordingAudioSidecars({})).toBe(false); + expect( + shouldKeepRecordingAudioSidecars({ + [RECORDING_AUDIO_SIDECAR_DEBUG_ENV]: "1", + }), + ).toBe(true); + expect( + shouldKeepRecordingAudioSidecars({ + [RECORDING_AUDIO_SIDECAR_DEBUG_ENV]: "true", + }), + ).toBe(true); + expect( + shouldKeepRecordingAudioSidecars({ + [RECORDING_AUDIO_SIDECAR_DEBUG_ENV]: "off", + }), + ).toBe(false); + }); +}); + +describe("browser microphone sidecar post-processing", () => { + it("keeps the browser fallback chain conservative and speech-oriented", () => { + expect(BROWSER_MIC_SIDECAR_FILTERS).toEqual([ + "adeclip=threshold=1", + "adeclick=w=40:o=75:t=3:b=2", + "highpass=f=85", + "lowpass=f=9500", + "afftdn=nr=10:nf=-45:tn=1", + "alimiter=limit=0.92:level=0", + ]); + expect( + BROWSER_MIC_SIDECAR_FILTERS.some((filter) => + /(^|,)(loudnorm|dynaudnorm|volume)=/i.test(filter), + ), + ).toBe(false); + }); + + it("adds bounded speech gain only for the no-AGC browser mic profile", () => { + expect(getBrowserMicSidecarFilters("processed")).toEqual(BROWSER_MIC_SIDECAR_FILTERS); + expect(getBrowserMicSidecarFilters("no-agc")).toEqual([ + "adeclip=threshold=1", + "adeclick=w=40:o=75:t=3:b=2", + "highpass=f=85", + "lowpass=f=9500", + "afftdn=nr=10:nf=-45:tn=1", + ...BROWSER_MIC_SIDECAR_NO_AGC_GAIN_FILTERS, + ]); + expect(BROWSER_MIC_SIDECAR_NO_AGC_GAIN_FILTERS).toContain( + "speechnorm=p=0.92:e=12:c=2:r=0.0005:f=0.001", + ); + expect(BROWSER_MIC_SIDECAR_NO_AGC_GAIN_FILTERS).toContain("alimiter=limit=0.92:level=0"); + }); +}); diff --git a/electron/ipc/recording/audioFilters.ts b/electron/ipc/recording/audioFilters.ts new file mode 100644 index 000000000..b465f5f20 --- /dev/null +++ b/electron/ipc/recording/audioFilters.ts @@ -0,0 +1,40 @@ +// Keep native mic capture dry by default. Automatic loudness normalization +// amplified wireless-headset noise and WASAPI discontinuities during beta tests. +export const WINDOWS_NATIVE_MIC_PRE_FILTERS = ["adeclip=threshold=1"]; + +// Browser mic fallback uses Chromium/WebRTC voice processing, but beta tests +// showed its realtime AGC can introduce short crackle bursts on some Realtek +// and headset paths. Default to no AGC, then restore usable level offline with +// bounded speech expansion and a limiter. +export const BROWSER_MIC_SIDECAR_BASE_FILTERS = [ + "adeclip=threshold=1", + "adeclick=w=40:o=75:t=3:b=2", + "highpass=f=85", + "lowpass=f=9500", + "afftdn=nr=10:nf=-45:tn=1", +]; + +export const BROWSER_MIC_SIDECAR_NO_AGC_GAIN_FILTERS = [ + "speechnorm=p=0.92:e=12:c=2:r=0.0005:f=0.001", + "alimiter=limit=0.92:level=0", +]; + +export const BROWSER_MIC_SIDECAR_FILTERS = [ + ...BROWSER_MIC_SIDECAR_BASE_FILTERS, + "alimiter=limit=0.92:level=0", +]; + +export function getBrowserMicSidecarFilters(profile?: string | null) { + if (profile === "no-agc") { + return [...BROWSER_MIC_SIDECAR_BASE_FILTERS, ...BROWSER_MIC_SIDECAR_NO_AGC_GAIN_FILTERS]; + } + + return BROWSER_MIC_SIDECAR_FILTERS; +} + +export const RECORDING_AUDIO_SIDECAR_DEBUG_ENV = "RECORDLY_KEEP_RECORDING_AUDIO_SIDECARS"; // not used yet, because we need to have seperate audio files for system and mic for each recording + +export function shouldKeepRecordingAudioSidecars(env: NodeJS.ProcessEnv = process.env) { + const value = env[RECORDING_AUDIO_SIDECAR_DEBUG_ENV]?.trim().toLowerCase(); + return value === "1" || value === "true" || value === "yes" || value === "on"; +} diff --git a/electron/ipc/recording/diagnostics.test.ts b/electron/ipc/recording/diagnostics.test.ts new file mode 100644 index 000000000..a0092a602 --- /dev/null +++ b/electron/ipc/recording/diagnostics.test.ts @@ -0,0 +1,446 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +type ExecFileCallback = (error: Error | null, stdout?: string, stderr?: string) => void; + +describe("getCompanionAudioFallbackPaths", () => { + let tempRoot: string; + let appDataPath: string; + let userDataPath: string; + let tempPath: string; + let appPath: string; + let execFileMock: ReturnType; + + beforeEach(async () => { + tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-companion-audio-")); + appDataPath = path.join(tempRoot, "AppData"); + userDataPath = path.join(tempRoot, "UserData"); + tempPath = path.join(tempRoot, "Temp"); + appPath = path.join(tempRoot, "App"); + await Promise.all( + [appDataPath, userDataPath, tempPath, appPath].map((dirPath) => + fs.mkdir(dirPath, { recursive: true }), + ), + ); + execFileMock = vi.fn( + ( + _file: string, + _args: string[], + _options: Record, + callback: ExecFileCallback, + ) => { + callback(null, "", ""); + }, + ); + + vi.resetModules(); + vi.doMock("electron", () => ({ + app: { + isPackaged: false, + getAppPath: () => appPath, + getPath: (name: string) => { + if (name === "appData") return appDataPath; + if (name === "userData") return userDataPath; + if (name === "temp") return tempPath; + return tempRoot; + }, + setPath: () => undefined, + }, + })); + vi.doMock("node:child_process", () => ({ + execFile: execFileMock, + })); + vi.doMock("../ffmpeg/binary", () => ({ + getFfmpegBinaryPath: () => "ffmpeg", + getFfprobeBinaryPath: () => "ffprobe", + })); + }); + + afterEach(async () => { + vi.resetModules(); + vi.doUnmock("electron"); + vi.doUnmock("node:child_process"); + vi.doUnmock("../ffmpeg/binary"); + if (tempRoot) { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + + it("returns companion audio files directly when the video has no embedded audio", async () => { + const videoPath = path.join(tempRoot, "recording.mp4"); + const systemPath = path.join(tempRoot, "recording.system.wav"); + const micPath = path.join(tempRoot, "recording.mic.wav"); + + await Promise.all([ + fs.writeFile(videoPath, "video"), + fs.writeFile(systemPath, "system"), + fs.writeFile(micPath, "mic"), + ]); + + execFileMock.mockImplementation( + ( + _file: string, + _args: string[], + _options: Record, + callback: ExecFileCallback, + ) => { + const error = new Error("ffmpeg probe failed") as Error & { stderr?: string }; + error.stderr = "Stream #0:0: Video: h264"; + callback(error, "", error.stderr); + }, + ); + + const { getCompanionAudioFallbackPaths } = await import("./diagnostics"); + + await expect(getCompanionAudioFallbackPaths(videoPath)).resolves.toEqual([ + systemPath, + micPath, + ]); + }); + + it("keeps the embedded source audio and adds the mic companion when both are present", async () => { + const videoPath = path.join(tempRoot, "recording.mp4"); + const systemPath = path.join(tempRoot, "recording.system.wav"); + const micPath = path.join(tempRoot, "recording.mic.wav"); + + await Promise.all([ + fs.writeFile(videoPath, "video"), + fs.writeFile(systemPath, "system"), + fs.writeFile(micPath, "mic"), + ]); + + execFileMock.mockImplementation( + ( + _file: string, + _args: string[], + _options: Record, + callback: ExecFileCallback, + ) => { + const error = new Error("ffmpeg probe found embedded audio") as Error & { + stderr?: string; + }; + error.stderr = "Stream #0:1: Audio: aac"; + callback(error, "", error.stderr); + }, + ); + + const { getCompanionAudioFallbackPaths } = await import("./diagnostics"); + + await expect(getCompanionAudioFallbackPaths(videoPath)).resolves.toEqual([ + videoPath, + micPath, + ]); + }); + + it("returns both mac sidecars instead of the video when a system sidecar exists", async () => { + const videoPath = path.join(tempRoot, "recording.mp4"); + const systemPath = path.join(tempRoot, "recording.system.m4a"); + const micPath = path.join(tempRoot, "recording.mic.m4a"); + + await Promise.all([ + fs.writeFile(videoPath, "video"), + fs.writeFile(systemPath, "system"), + fs.writeFile(micPath, "mic"), + ]); + + execFileMock.mockImplementation( + ( + _file: string, + _args: string[], + _options: Record, + callback: ExecFileCallback, + ) => { + const error = new Error("ffmpeg probe found embedded audio") as Error & { + stderr?: string; + }; + error.stderr = "Stream #0:1: Audio: aac"; + callback(error, "", error.stderr); + }, + ); + + const { getCompanionAudioFallbackPaths } = await import("./diagnostics"); + + // The inline mp4 track holds system audio only, so returning [videoPath] + // here silently dropped the microphone. + await expect(getCompanionAudioFallbackPaths(videoPath)).resolves.toEqual([ + systemPath, + micPath, + ]); + }); + + it("prefers the mac mic companion alone when embedded audio already exists and no system sidecar is present", async () => { + const videoPath = path.join(tempRoot, "recording.mp4"); + const micPath = path.join(tempRoot, "recording.mic.m4a"); + + await Promise.all([fs.writeFile(videoPath, "video"), fs.writeFile(micPath, "mic")]); + + execFileMock.mockImplementation( + ( + _file: string, + _args: string[], + _options: Record, + callback: ExecFileCallback, + ) => { + const error = new Error("ffmpeg probe found embedded audio") as Error & { + stderr?: string; + }; + error.stderr = "Stream #0:1: Audio: aac"; + callback(error, "", error.stderr); + }, + ); + + const { getCompanionAudioFallbackPaths } = await import("./diagnostics"); + + await expect(getCompanionAudioFallbackPaths(videoPath)).resolves.toEqual([micPath]); + }); + + it("loads saved sidecar timing metadata alongside companion audio paths", async () => { + const videoPath = path.join(tempRoot, "recording.mp4"); + const micPath = path.join(tempRoot, "recording.mic.webm"); + + await Promise.all([ + fs.writeFile(videoPath, "video"), + fs.writeFile(micPath, "mic"), + fs.writeFile(`${micPath}.json`, `\ufeff${JSON.stringify({ startDelayMs: 2750 })}`), + ]); + + execFileMock.mockImplementation( + ( + _file: string, + _args: string[], + _options: Record, + callback: ExecFileCallback, + ) => { + const error = new Error("ffmpeg probe failed") as Error & { stderr?: string }; + error.stderr = "Stream #0:0: Video: h264"; + callback(error, "", error.stderr); + }, + ); + + const { getCompanionAudioFallbackInfo } = await import("./diagnostics"); + + await expect(getCompanionAudioFallbackInfo(videoPath)).resolves.toEqual({ + paths: [micPath], + startDelayMsByPath: { + [micPath]: 2750, + }, + }); + }); + + it("scales audio mux timeout for long recordings", async () => { + const { getRecordingAudioMuxTimeoutMs } = await import("./diagnostics"); + + expect(getRecordingAudioMuxTimeoutMs(0)).toBe(5 * 60 * 1000); + expect(getRecordingAudioMuxTimeoutMs(29 * 60 + 29.41)).toBeGreaterThan(120000); + expect(getRecordingAudioMuxTimeoutMs(29 * 60 + 29.41)).toBeCloseTo( + (29 * 60 + 29.41) * 1000 + 60 * 1000, + 0, + ); + }); + + it("uses video stream frames when container duration is misleading", async () => { + const { parseFfprobeVideoStreamDuration } = await import("./diagnostics"); + + expect( + parseFfprobeVideoStreamDuration( + JSON.stringify({ + streams: [ + { + duration: "18.000000", + nb_read_frames: "540", + avg_frame_rate: "30/1", + }, + ], + }), + ), + ).toEqual({ + durationSeconds: 18, + frameCount: 540, + frameRate: 30, + }); + }); + + it("derives video stream duration from frame count when stream duration is absent", async () => { + const { parseFfprobeVideoStreamDuration } = await import("./diagnostics"); + + expect( + parseFfprobeVideoStreamDuration( + JSON.stringify({ + streams: [ + { + nb_read_frames: "540", + avg_frame_rate: "30/1", + }, + ], + }), + ), + ).toEqual({ + durationSeconds: 18, + frameCount: 540, + frameRate: 30, + }); + }); + + it("writes a recording diagnostics sidecar with stream and audio probes", async () => { + const videoPath = path.join(tempRoot, "recording-123.mp4"); + const micPath = path.join(tempRoot, "recording-123.mic.wav"); + await Promise.all([ + fs.writeFile(videoPath, "video"), + fs.writeFile(micPath, "mic"), + fs.writeFile(`${micPath}.json`, JSON.stringify({ startDelayMs: 125 })), + ]); + + execFileMock.mockImplementation( + ( + file: string, + args: string[], + _options: Record, + callback: ExecFileCallback, + ) => { + if (file === "ffprobe" || args.includes("-of")) { + callback( + null, + JSON.stringify({ + streams: [ + { + duration: "18.000000", + nb_read_frames: "540", + avg_frame_rate: "30/1", + }, + ], + }), + "", + ); + return; + } + + const error = new Error("ffmpeg probe") as Error & { stderr?: string }; + error.stderr = "Duration: 00:00:18.00, start: 0.000000"; + callback(error, "", error.stderr); + }, + ); + + const { getRecordingDiagnosticsPath, writeRecordingDiagnosticsSnapshot } = await import( + "./diagnostics" + ); + + const diagnosticsPath = await writeRecordingDiagnosticsSnapshot(videoPath, { + backend: "windows-wgc", + phase: "mux-start", + expectedDurationMs: 60_000, + outputPath: videoPath, + microphonePath: micPath, + details: { + hasMicrophone: true, + }, + }); + const diagnostics = JSON.parse(await fs.readFile(diagnosticsPath, "utf8")); + + expect(diagnosticsPath).toBe(getRecordingDiagnosticsPath(videoPath)); + expect(diagnostics.events).toHaveLength(1); + expect(diagnostics.latest.expectedDurationMs).toBe(60_000); + expect(diagnostics.latest.media.video.stream).toEqual({ + durationSeconds: 18, + frameCount: 540, + frameRate: 30, + }); + expect(diagnostics.latest.media.microphone).toMatchObject({ + path: micPath, + exists: true, + containerDurationSeconds: 18, + startDelayMs: 125, + }); + }); + + it("ignores invalid sidecar timing metadata values", async () => { + const micPath = path.join(tempRoot, "recording.mic.wav"); + await Promise.all([ + fs.writeFile(micPath, "mic"), + fs.writeFile(`${micPath}.json`, JSON.stringify({ startDelayMs: -250 })), + ]); + + const { getCompanionAudioStartDelayMs } = await import("./diagnostics"); + + await expect(getCompanionAudioStartDelayMs(micPath)).resolves.toBeNull(); + }); + + it("classifies wall-clock mic chunk gaps covered by pause intervals", async () => { + const { summarizeMicrophoneChunkTiming } = await import("./diagnostics"); + + expect( + summarizeMicrophoneChunkTiming( + [ + { + index: 0, + size: 1024, + elapsedMs: 250, + deltaMs: null, + recordedElapsedMs: 250, + recordedDeltaMs: null, + }, + { + index: 1, + size: 1024, + elapsedMs: 8250, + deltaMs: 8000, + recordedElapsedMs: 500, + recordedDeltaMs: 250, + }, + ], + [{ startElapsedMs: 250, endElapsedMs: 8250, durationMs: 7750 }], + 250, + ), + ).toMatchObject({ + status: "pause-accounted", + wallClockGapCount: 1, + recordedGapCount: 0, + pausedDurationMs: 7750, + }); + }); + + it("flags recorded mic chunk gaps that remain after pause accounting", async () => { + const { summarizeMicrophoneChunkTiming } = await import("./diagnostics"); + + expect( + summarizeMicrophoneChunkTiming( + [ + { + index: 0, + size: 1024, + elapsedMs: 250, + deltaMs: null, + recordedElapsedMs: 250, + recordedDeltaMs: null, + }, + { + index: 1, + size: 1024, + elapsedMs: 2500, + deltaMs: 2250, + recordedElapsedMs: 2500, + recordedDeltaMs: 2250, + }, + ], + [], + 250, + ), + ).toMatchObject({ + status: "needs-review", + wallClockGapCount: 1, + recordedGapCount: 1, + }); + }); + + it("rejects tiny MP4 container-only outputs before they reach the editor", async () => { + const videoPath = path.join(tempRoot, "recording-123.mp4"); + await fs.writeFile(videoPath, Buffer.alloc(261)); + + const { validateRecordedVideo } = await import("./diagnostics"); + + await expect(validateRecordedVideo(videoPath)).rejects.toThrow( + "Recorded output is too small to contain playable video", + ); + }); +}); diff --git a/electron/ipc/recording/diagnostics.ts b/electron/ipc/recording/diagnostics.ts new file mode 100644 index 000000000..2c7c7cd61 --- /dev/null +++ b/electron/ipc/recording/diagnostics.ts @@ -0,0 +1,637 @@ +import { isLibrarySequenceSource } from "./sequenceSource"; +import { execFile } from "node:child_process"; +import fs from "node:fs/promises"; +import { promisify } from "node:util"; +import { COMPANION_AUDIO_LAYOUTS } from "../constants"; +import { getFfmpegBinaryPath, getFfprobeBinaryPath } from "../ffmpeg/binary"; +import { lastNativeCaptureDiagnostics, setLastNativeCaptureDiagnostics } from "../state"; +import type { CompanionAudioCandidate, NativeCaptureDiagnostics } from "../types"; +import { parseJsonWithByteOrderMark } from "../utils"; + +const execFileAsync = promisify(execFile); +export const MIN_VALID_RECORDED_VIDEO_BYTES = 1024; +export const RECORDING_AUDIO_MUX_MIN_TIMEOUT_MS = 5 * 60 * 1000; +export const RECORDING_AUDIO_MUX_MAX_TIMEOUT_MS = 2 * 60 * 60 * 1000; + +type CompanionAudioTimingMetadata = { + startDelayMs?: number; +}; + +export type MicrophoneChunkTimingEvent = { + index: number; + size: number; + elapsedMs: number; + deltaMs: number | null; + recordedElapsedMs?: number; + recordedDeltaMs?: number | null; +}; + +export type MicrophonePauseInterval = { + startElapsedMs: number; + endElapsedMs?: number; + durationMs?: number; +}; + +export type VideoStreamDurationProbe = { + durationSeconds: number | null; + frameCount: number | null; + frameRate: number | null; +}; + +export type RecordingDiagnosticsSnapshot = { + backend: NativeCaptureDiagnostics["backend"]; + phase: + | `${NativeCaptureDiagnostics["phase"]}` + | "pad" + | "mic-sidecar" + | "mux-start" + | "mux-complete" + | "mux-error"; + expectedDurationMs?: number | null; + outputPath?: string | null; + systemAudioPath?: string | null; + microphonePath?: string | null; + processOutput?: string; + error?: string; + details?: Record; +}; + +type RecordingDiagnosticsLog = { + version: 1; + createdAt: string; + updatedAt: string; + videoPath: string; + diagnosticsPath: string; + events: unknown[]; + latest?: unknown; +}; + +export function recordNativeCaptureDiagnostics( + diagnostics: Omit, +) { + setLastNativeCaptureDiagnostics({ + timestamp: new Date().toISOString(), + ...diagnostics, + }); + + return lastNativeCaptureDiagnostics; +} + +export async function getFileSizeIfPresent(filePath: string | null | undefined) { + if (!filePath) { + return null; + } + + try { + const stat = await fs.stat(filePath); + return stat.size; + } catch { + return null; + } +} + +export function parseFfmpegDurationSeconds(stderr: string) { + const match = stderr.match(/Duration:\s+(\d+):(\d+):(\d+(?:\.\d+)?)/i); + if (!match) { + return null; + } + + const hours = Number(match[1]); + const minutes = Number(match[2]); + const seconds = Number(match[3]); + if (![hours, minutes, seconds].every(Number.isFinite)) { + return null; + } + + return hours * 3600 + minutes * 60 + seconds; +} + +export function getRecordingAudioMuxTimeoutMs(durationSeconds: number) { + if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) { + return RECORDING_AUDIO_MUX_MIN_TIMEOUT_MS; + } + + const realtimeMuxBudgetMs = Math.ceil(durationSeconds * 1000) + 60 * 1000; + return Math.min( + RECORDING_AUDIO_MUX_MAX_TIMEOUT_MS, + Math.max(RECORDING_AUDIO_MUX_MIN_TIMEOUT_MS, realtimeMuxBudgetMs), + ); +} + +function getFiniteNonNegativeNumber(value: unknown) { + return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null; +} + +export function summarizeMicrophoneChunkTiming( + chunkEvents: MicrophoneChunkTimingEvent[] | null | undefined, + pauseIntervals: MicrophonePauseInterval[] | null | undefined = [], + timesliceMs = 250, +) { + const normalizedTimesliceMs = + Number.isFinite(timesliceMs) && timesliceMs > 0 ? Math.round(timesliceMs) : 250; + const thresholdMs = Math.max(600, Math.round(normalizedTimesliceMs * 2.5)); + const events = Array.isArray(chunkEvents) ? chunkEvents : []; + const pauses = Array.isArray(pauseIntervals) ? pauseIntervals : []; + const pausedDurationMs = pauses.reduce((total, interval) => { + const durationMs = getFiniteNonNegativeNumber(interval.durationMs); + return durationMs === null ? total : total + Math.round(durationMs); + }, 0); + + const wallClockGaps = events + .filter((event) => (event.deltaMs ?? 0) > thresholdMs) + .map((event) => ({ + index: event.index, + deltaMs: Math.round(event.deltaMs ?? 0), + elapsedMs: Math.round(event.elapsedMs), + })); + const recordedGaps = events + .filter((event) => { + const recordedDeltaMs = + getFiniteNonNegativeNumber(event.recordedDeltaMs) ?? event.deltaMs ?? 0; + return recordedDeltaMs > thresholdMs; + }) + .map((event) => ({ + index: event.index, + deltaMs: Math.round( + getFiniteNonNegativeNumber(event.recordedDeltaMs) ?? event.deltaMs ?? 0, + ), + recordedElapsedMs: Math.round(event.recordedElapsedMs ?? event.elapsedMs), + })); + const maxWallClockDeltaMs = events.reduce( + (maxDeltaMs, event) => Math.max(maxDeltaMs, event.deltaMs ?? 0), + 0, + ); + const maxRecordedDeltaMs = events.reduce((maxDeltaMs, event) => { + const recordedDeltaMs = + getFiniteNonNegativeNumber(event.recordedDeltaMs) ?? event.deltaMs ?? 0; + return Math.max(maxDeltaMs, recordedDeltaMs); + }, 0); + const status = + recordedGaps.length > 0 + ? "needs-review" + : wallClockGaps.length > 0 && pausedDurationMs > 0 + ? "pause-accounted" + : "ok"; + + return { + status, + timesliceMs: normalizedTimesliceMs, + thresholdMs, + eventCount: events.length, + pauseIntervalCount: pauses.length, + pausedDurationMs, + maxWallClockDeltaMs: Math.round(maxWallClockDeltaMs), + maxRecordedDeltaMs: Math.round(maxRecordedDeltaMs), + wallClockGapCount: wallClockGaps.length, + recordedGapCount: recordedGaps.length, + wallClockGaps: wallClockGaps.slice(0, 10), + recordedGaps: recordedGaps.slice(0, 10), + }; +} + +/** Probe the duration of a media file (in seconds) using the container header. */ +export async function probeMediaDurationSeconds(filePath: string): Promise { + const start = Date.now(); + const ffmpegPath = getFfmpegBinaryPath(); + try { + await execFileAsync(ffmpegPath, ["-i", filePath, "-hide_banner"], { timeout: 5000 }); + } catch (error) { + const stderr = (error as NodeJS.ErrnoException & { stderr?: string })?.stderr ?? ""; + const duration = parseFfmpegDurationSeconds(stderr); + if (duration !== null) { + return duration; + } + } finally { + console.log(`[PERF:MAIN] probeMediaDurationSeconds: COMPLETED in ${Date.now() - start}ms`); + } + return 0; +} + +function parsePositiveNumber(value: unknown) { + const parsed = + typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; +} + +function parsePositiveInteger(value: unknown) { + const parsed = parsePositiveNumber(value); + return parsed === null ? null : Math.max(0, Math.round(parsed)); +} + +function parseFrameRate(value: unknown) { + if (typeof value !== "string") { + return null; + } + const [numeratorRaw, denominatorRaw] = value.split("/"); + const numerator = Number(numeratorRaw); + const denominator = Number(denominatorRaw); + if ( + !Number.isFinite(numerator) || + !Number.isFinite(denominator) || + numerator <= 0 || + denominator <= 0 + ) { + return null; + } + + return numerator / denominator; +} + +export function parseFfprobeVideoStreamDuration(output: string): VideoStreamDurationProbe | null { + const parsed = JSON.parse(output) as { + streams?: Array<{ + duration?: unknown; + nb_frames?: unknown; + nb_read_frames?: unknown; + avg_frame_rate?: unknown; + r_frame_rate?: unknown; + }>; + }; + const stream = parsed.streams?.[0]; + if (!stream) { + return null; + } + + const frameRate = parseFrameRate(stream.avg_frame_rate) ?? parseFrameRate(stream.r_frame_rate); + const frameCount = + parsePositiveInteger(stream.nb_read_frames) ?? parsePositiveInteger(stream.nb_frames); + const streamDuration = parsePositiveNumber(stream.duration); + const frameDerivedDuration = + frameCount !== null && frameRate !== null && frameRate > 0 ? frameCount / frameRate : null; + + return { + durationSeconds: streamDuration ?? frameDerivedDuration, + frameCount, + frameRate, + }; +} + +export async function probeVideoStreamDuration( + filePath: string, +): Promise { + const start = Date.now(); + try { + const result = await execFileAsync( + getFfprobeBinaryPath(), + [ + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=duration,nb_frames,nb_read_frames,avg_frame_rate,r_frame_rate", + "-of", + "json", + filePath, + ], + { timeout: 30000, maxBuffer: 2 * 1024 * 1024 }, + ); + const stdout = typeof result === "string" ? result : result.stdout; + return parseFfprobeVideoStreamDuration(stdout); + } catch { + return null; + } finally { + console.log(`[PERF:MAIN] probeVideoStreamDuration: COMPLETED in ${Date.now() - start}ms`); + } +} + +export async function probeVideoStreamDurationSeconds(filePath: string): Promise { + const probe = await probeVideoStreamDuration(filePath); + return probe?.durationSeconds && probe.durationSeconds > 0 + ? probe.durationSeconds + : probeMediaDurationSeconds(filePath); +} + +export function getRecordingDiagnosticsPath(videoPath: string) { + return `${videoPath.replace(/\.[^.]+$/u, "")}.recording-diagnostics.json`; +} + +function truncateDiagnosticsText(value: string | undefined, maxLength = 12000) { + if (!value || value.length <= maxLength) { + return value; + } + + return `${value.slice(-maxLength)}\n[recordly: truncated to last ${maxLength} chars]`; +} + +async function describeMediaFile(filePath: string | null | undefined) { + if (!filePath) { + return null; + } + + try { + const stat = await fs.stat(filePath); + if (!stat.isFile()) { + return { + path: filePath, + exists: false, + }; + } + + return { + path: filePath, + exists: true, + sizeBytes: stat.size, + containerDurationSeconds: await probeMediaDurationSeconds(filePath), + }; + } catch { + return { + path: filePath, + exists: false, + }; + } +} + +async function describeVideoFile(filePath: string | null | undefined) { + const media = await describeMediaFile(filePath); + if (!media?.exists || !filePath) { + return media; + } + + return { + ...media, + stream: await probeVideoStreamDuration(filePath), + }; +} + +async function describeAudioFile(filePath: string | null | undefined) { + const media = await describeMediaFile(filePath); + if (!media?.exists || !filePath) { + return media; + } + + const startDelayMs = await getCompanionAudioStartDelayMs(filePath); + return { + ...media, + startDelayMs, + }; +} + +export async function writeRecordingDiagnosticsSnapshot( + videoPath: string, + snapshot: RecordingDiagnosticsSnapshot, +) { + const diagnosticsPath = getRecordingDiagnosticsPath(videoPath); + const now = new Date().toISOString(); + let existing: RecordingDiagnosticsLog | null = null; + + try { + const raw = await fs.readFile(diagnosticsPath, "utf8"); + const parsed = parseJsonWithByteOrderMark(raw); + if (parsed?.version === 1 && Array.isArray(parsed.events)) { + existing = parsed; + } + } catch { + // First diagnostics event for this recording. + } + + const event = { + timestamp: now, + ...snapshot, + processOutput: truncateDiagnosticsText(snapshot.processOutput), + media: { + video: await describeVideoFile(videoPath), + systemAudio: await describeAudioFile(snapshot.systemAudioPath), + microphone: await describeAudioFile(snapshot.microphonePath), + }, + }; + const log: RecordingDiagnosticsLog = existing ?? { + version: 1, + createdAt: now, + updatedAt: now, + videoPath, + diagnosticsPath, + events: [], + }; + log.updatedAt = now; + log.videoPath = videoPath; + log.diagnosticsPath = diagnosticsPath; + log.events.push(event); + log.latest = event; + + await fs.writeFile(diagnosticsPath, JSON.stringify(log, null, 2), "utf8"); + return diagnosticsPath; +} + +export async function getUsableCompanionAudioCandidates( + videoPath: string, +): Promise { + const basePath = videoPath.replace(/\.[^.]+$/u, ""); + const candidates: CompanionAudioCandidate[] = []; + + for (const layout of COMPANION_AUDIO_LAYOUTS) { + const systemPath = `${basePath}${layout.systemSuffix}`; + const micPath = `${basePath}${layout.micSuffix}`; + const usablePaths: string[] = []; + + for (const companionPath of [systemPath, micPath]) { + try { + const stat = await fs.stat(companionPath); + if (stat.size > 0) { + usablePaths.push(companionPath); + } + } catch { + // Missing companion audio is expected for many recordings. + } + } + + if (usablePaths.length > 0) { + candidates.push({ + platform: layout.platform, + systemPath, + micPath, + usablePaths, + }); + } + } + + return candidates; +} + +async function readCompanionAudioTimingMetadata( + companionPath: string, +): Promise { + try { + const raw = await fs.readFile(`${companionPath}.json`, "utf8"); + const parsed = parseJsonWithByteOrderMark(raw); + if (!parsed || typeof parsed !== "object") { + return null; + } + + return parsed; + } catch { + return null; + } +} + +export async function getCompanionAudioStartDelayMs(companionPath: string) { + const metadata = await readCompanionAudioTimingMetadata(companionPath); + const startDelayMs = metadata?.startDelayMs; + if (!Number.isFinite(startDelayMs) || (startDelayMs ?? 0) < 0) { + return null; + } + + return Math.round(startDelayMs ?? 0); +} + +export async function hasEmbeddedAudioStream(videoPath: string) { + const ffmpegPath = getFfmpegBinaryPath(); + let stderr = ""; + + try { + const result = await execFileAsync( + ffmpegPath, + ["-hide_banner", "-i", videoPath, "-map", "0:a:0", "-frames:a", "1", "-f", "null", "-"], + { timeout: 20000, maxBuffer: 10 * 1024 * 1024 }, + ); + stderr = result.stderr; + } catch (error) { + stderr = (error as NodeJS.ErrnoException & { stderr?: string }).stderr ?? ""; + } + + return /Stream #.*Audio:/i.test(stderr); +} + +export async function getCompanionAudioFallbackPaths(videoPath: string) { + const { paths } = await getCompanionAudioFallbackInfo(videoPath); + return paths; +} + +/** + * Resolve which audio files the editor should play alongside `videoPath`, and + * the start delay recorded for each. + * + * The renderer treats a `.system.`/`.mic.` pair as independent tracks and mutes + * the video's own track when both are present. The macOS helper writes system + * audio to the inline track but keeps both sources as sidecars, so once a mac + * system sidecar exists the sidecars are authoritative and are returned in place + * of the video. Other layouts keep the embedded track and add the mic sidecar. + */ +export async function getCompanionAudioFallbackInfo(videoPath: string) { + const companionCandidates = await getUsableCompanionAudioCandidates(videoPath); + if (companionCandidates.length === 0) { + return { paths: [], startDelayMsByPath: {} }; + } + + let paths: string[]; + if (isLibrarySequenceSource(videoPath)) { + paths = companionCandidates.flatMap((candidate) => candidate.usablePaths); + } else if (await hasEmbeddedAudioStream(videoPath)) { + const hasUsableMacSystemCompanion = companionCandidates.some( + (candidate) => + candidate.platform === "mac" && + candidate.usablePaths.includes(candidate.systemPath), + ); + const usableMacMicOnlyCompanions = Array.from( + new Set( + companionCandidates.flatMap((candidate) => + candidate.platform === "mac" && + !candidate.usablePaths.includes(candidate.systemPath) && + candidate.usablePaths.includes(candidate.micPath) + ? [candidate.micPath] + : [], + ), + ), + ); + + if (!hasUsableMacSystemCompanion && usableMacMicOnlyCompanions.length > 0) { + paths = usableMacMicOnlyCompanions; + } else if (hasUsableMacSystemCompanion) { + // The inline mp4 audio track carries system audio only (the helper skips + // the microphone while system audio is captured), so returning the video + // alone drops the mic entirely. Hand over both mac sidecars instead and + // let the renderer route them as independent system/mic tracks. + paths = Array.from( + new Set( + companionCandidates.flatMap((candidate) => + candidate.platform === "mac" ? candidate.usablePaths : [], + ), + ), + ); + } else { + const companionPaths = Array.from( + new Set( + companionCandidates.flatMap((candidate) => + candidate.usablePaths.filter( + (companionPath) => companionPath === candidate.micPath, + ), + ), + ), + ); + if (companionPaths.length === 0) { + return { paths: [], startDelayMsByPath: {} }; + } + + paths = [videoPath, ...companionPaths]; + } + } else { + paths = Array.from( + new Set(companionCandidates.flatMap((candidate) => candidate.usablePaths)), + ); + } + + const metadataEntries = await Promise.all( + paths.map(async (audioPath) => { + const startDelayMs = await getCompanionAudioStartDelayMs(audioPath); + if (!Number.isFinite(startDelayMs)) { + return null; + } + + return [audioPath, startDelayMs] as const; + }), + ); + + return { + paths, + startDelayMsByPath: Object.fromEntries( + metadataEntries.filter((entry): entry is readonly [string, number] => entry !== null), + ), + }; +} + +export async function validateRecordedVideo(videoPath: string) { + const stat = await fs.stat(videoPath); + if (!stat.isFile()) { + throw new Error(`Recorded output is not a file: ${videoPath}`); + } + + if (stat.size <= 0) { + throw new Error(`Recorded output is empty: ${videoPath}`); + } + + if (stat.size < MIN_VALID_RECORDED_VIDEO_BYTES) { + throw new Error( + `Recorded output is too small to contain playable video (${stat.size} bytes): ${videoPath}`, + ); + } + + const ffmpegPath = getFfmpegBinaryPath(); + let stderr = ""; + + try { + const result = await execFileAsync( + ffmpegPath, + ["-hide_banner", "-i", videoPath, "-map", "0:v:0", "-frames:v", "1", "-f", "null", "-"], + { timeout: 20000, maxBuffer: 10 * 1024 * 1024 }, + ); + stderr = result.stderr; + } catch (error) { + const execError = error as NodeJS.ErrnoException & { stderr?: string }; + const output = execError.stderr?.trim(); + throw new Error(output || `Recorded output could not be decoded: ${videoPath}`); + } + + if (!/Stream #.*Video:/i.test(stderr)) { + throw new Error(`Recorded output does not contain a readable video stream: ${videoPath}`); + } + + const durationSeconds = parseFfmpegDurationSeconds(stderr); + if (durationSeconds === null || durationSeconds <= 0) { + throw new Error(`Recorded output has an invalid duration: ${videoPath}`); + } + + return { + fileSizeBytes: stat.size, + durationSeconds, + }; +} diff --git a/electron/ipc/recording/events.ts b/electron/ipc/recording/events.ts new file mode 100644 index 000000000..5a2653c3b --- /dev/null +++ b/electron/ipc/recording/events.ts @@ -0,0 +1,9 @@ +import { BrowserWindow } from "electron"; + +export function emitRecordingInterrupted(reason: string, message: string) { + BrowserWindow.getAllWindows().forEach((window) => { + if (!window.isDestroyed()) { + window.webContents.send("recording-interrupted", { reason, message }); + } + }); +} diff --git a/electron/ipc/recording/ffmpeg.ts b/electron/ipc/recording/ffmpeg.ts new file mode 100644 index 000000000..50281a39f --- /dev/null +++ b/electron/ipc/recording/ffmpeg.ts @@ -0,0 +1,225 @@ +import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import fs from "node:fs/promises"; +import { resolveLinuxWindowBounds } from "../cursor/bounds"; +import { ffmpegCaptureOutputBuffer } from "../state"; +import type { SelectedSource } from "../types"; +import { getScreen, parseWindowId } from "../utils"; +import { resolveWindowsCaptureDisplay } from "../windowsCaptureSelection"; + +export function getDisplayBoundsForSource(source: SelectedSource) { + return resolveWindowsCaptureDisplay( + source, + getScreen().getAllDisplays(), + getScreen().getPrimaryDisplay(), + ).bounds; +} + +export function getDisplayWorkAreaForSource(source: SelectedSource) { + const allDisplays = getScreen().getAllDisplays(); + const primaryDisplay = getScreen().getPrimaryDisplay(); + const { displayId } = resolveWindowsCaptureDisplay(source, allDisplays, primaryDisplay); + const matched = allDisplays.find((d) => d.id === displayId) ?? primaryDisplay; + return matched.workArea; +} + +export async function buildFfmpegCaptureArgs(source: SelectedSource, outputPath: string) { + const buildOutputArgs = (inputRange: "auto" | "full") => [ + "-an", + "-vf", + `scale=in_range=${inputRange}:out_range=tv:out_color_matrix=bt709:sws_dither=bayer`, + "-c:v", + "libx264", + "-preset", + "veryfast", + "-pix_fmt", + "yuv420p", + "-colorspace", + "bt709", + "-color_primaries", + "bt709", + "-color_trc", + "bt709", + "-color_range", + "tv", + "-movflags", + "+faststart", + outputPath, + ]; + const fullRangeOutputArgs = buildOutputArgs("full"); + + if (process.platform === "win32") { + if (source?.id?.startsWith("window:")) { + const windowId = parseWindowId(source.id); + const windowTitle = + typeof source.windowTitle === "string" + ? source.windowTitle.trim() + : source.name.trim(); + if (!windowId && !windowTitle) { + throw new Error("Missing window identifier for FFmpeg window capture"); + } + + return [ + "-y", + "-f", + "gdigrab", + "-framerate", + "60", + "-draw_mouse", + "0", + "-i", + windowId ? `hwnd=${windowId}` : `title=${windowTitle}`, + ...fullRangeOutputArgs, + ]; + } + + return [ + "-y", + "-f", + "gdigrab", + "-framerate", + "60", + "-draw_mouse", + "0", + "-i", + "desktop", + ...fullRangeOutputArgs, + ]; + } + + if (process.platform === "linux") { + const displayEnv = process.env.DISPLAY || ":0.0"; + if (source?.id?.startsWith("window:")) { + const bounds = await resolveLinuxWindowBounds(source); + if (!bounds) { + throw new Error("Unable to resolve Linux window bounds for FFmpeg capture"); + } + + return [ + "-y", + "-f", + "x11grab", + "-framerate", + "60", + "-draw_mouse", + "0", + "-video_size", + `${Math.max(2, bounds.width)}x${Math.max(2, bounds.height)}`, + "-i", + `${displayEnv}+${Math.round(bounds.x)},${Math.round(bounds.y)}`, + ...fullRangeOutputArgs, + ]; + } + + const bounds = getDisplayBoundsForSource(source); + return [ + "-y", + "-f", + "x11grab", + "-framerate", + "60", + "-draw_mouse", + "0", + "-video_size", + `${Math.max(2, bounds.width)}x${Math.max(2, bounds.height)}`, + "-i", + `${displayEnv}+${Math.round(bounds.x)},${Math.round(bounds.y)}`, + ...fullRangeOutputArgs, + ]; + } + + if (process.platform === "darwin") { + return [ + "-y", + "-f", + "avfoundation", + "-capture_cursor", + "0", + "-framerate", + "60", + "-i", + "1:none", + ...buildOutputArgs("auto"), + ]; + } + + throw new Error(`FFmpeg capture is not supported on ${process.platform}`); +} + +export function waitForFfmpegCaptureStart(process: ChildProcessWithoutNullStreams) { + return new Promise((resolve, reject) => { + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + + const onExit = (code: number | null) => { + cleanup(); + reject( + new Error( + ffmpegCaptureOutputBuffer.trim() || + `FFmpeg exited before recording started (code ${code ?? "unknown"})`, + ), + ); + }; + + const timer = setTimeout(() => { + cleanup(); + resolve(); + }, 900); + + const cleanup = () => { + clearTimeout(timer); + process.off("error", onError); + process.off("exit", onExit); + }; + + process.once("error", onError); + process.once("exit", onExit); + }); +} + +export function waitForFfmpegCaptureStop( + process: ChildProcessWithoutNullStreams, + outputPath: string, +) { + return new Promise((resolve, reject) => { + const onClose = async (code: number | null) => { + cleanup(); + + try { + await fs.access(outputPath); + if (code === 0 || code === null) { + resolve(outputPath); + return; + } + + if (ffmpegCaptureOutputBuffer.includes("Exiting normally")) { + resolve(outputPath); + return; + } + } catch { + // handled below + } + + reject( + new Error( + ffmpegCaptureOutputBuffer.trim() || + `FFmpeg exited with code ${code ?? "unknown"}`, + ), + ); + }; + + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + + const cleanup = () => { + process.off("close", onClose); + process.off("error", onError); + }; + + process.once("close", onClose); + process.once("error", onError); + }); +} diff --git a/electron/ipc/recording/hudCrashRecovery.test.ts b/electron/ipc/recording/hudCrashRecovery.test.ts new file mode 100644 index 000000000..06fd7796c --- /dev/null +++ b/electron/ipc/recording/hudCrashRecovery.test.ts @@ -0,0 +1,420 @@ +import { EventEmitter } from "node:events"; +import { describe, expect, it, vi } from "vitest"; +import { + createHudCrashRecoveryDeps, + createStartTracker, + nativeCaptureSidecarPaths, + type HudCrashRecoveryDeps, + type NativeCaptureState, + stopRecordingWhenHudGoes, +} from "./hudCrashRecovery"; + +function setup(overrides: Partial = {}) { + const hud = new EventEmitter(); + const deps = { + startSettled: vi.fn(async () => {}), + isRecordingWithoutStopRequest: vi.fn(() => true), + isUserRecording: vi.fn(() => true), + stopRecording: vi.fn(async () => ({ success: true, path: "/rec/recording-1.mp4" })), + finishRecording: vi.fn(async (stopped) => stopped), + discardRecording: vi.fn(async () => {}), + takePendingStoppedVideo: vi.fn( + async () => null as { path: string; userRecording: boolean } | null, + ), + takeStoppedCountdownCapture: vi.fn(async () => null as string | null), + muxPendingStoppedVideo: vi.fn(async () => ({ + success: true, + path: "/rec/recording-1.mp4", + })), + setRecordingState: vi.fn(), + reportRecovered: vi.fn(async () => {}), + ...overrides, + } satisfies HudCrashRecoveryDeps; + return { hud, deps }; +} + +const crash = (hud: EventEmitter, reason = "crashed") => + hud.emit("render-process-gone", {}, { reason }); + +describe("stopRecordingWhenHudGoes", () => { + it("stops, finishes and reports the recording, and leaves the recording state", async () => { + const { hud, deps } = setup({ + finishRecording: vi.fn(async () => ({ + success: true, + path: "/rec/recording-1.muxed.mp4", + })), + }); + stopRecordingWhenHudGoes(hud, deps); + + crash(hud); + + await vi.waitFor(() => expect(deps.reportRecovered).toHaveBeenCalled()); + expect(deps.stopRecording).toHaveBeenCalledTimes(1); + expect(deps.finishRecording).toHaveBeenCalledWith({ + success: true, + path: "/rec/recording-1.mp4", + }); + expect(deps.setRecordingState).toHaveBeenCalledWith(false); + expect(deps.reportRecovered).toHaveBeenCalledWith( + { success: true, path: "/rec/recording-1.muxed.mp4" }, + "crashed", + ); + expect(deps.discardRecording).not.toHaveBeenCalled(); + }); + + it("does nothing when no recording is running or Stop was already requested", async () => { + const { hud, deps } = setup({ isRecordingWithoutStopRequest: vi.fn(() => false) }); + stopRecordingWhenHudGoes(hud, deps); + + crash(hud); + + await vi.waitFor(() => expect(deps.isRecordingWithoutStopRequest).toHaveBeenCalled()); + expect(deps.stopRecording).not.toHaveBeenCalled(); + expect(deps.setRecordingState).not.toHaveBeenCalled(); + }); + + it("ignores a clean renderer exit", async () => { + const { hud, deps } = setup(); + stopRecordingWhenHudGoes(hud, deps); + + crash(hud, "clean-exit"); + + await Promise.resolve(); + expect(deps.startSettled).not.toHaveBeenCalled(); + expect(deps.stopRecording).not.toHaveBeenCalled(); + }); + + it("stops and deletes a capture that was only warm-started for the countdown", async () => { + const { hud, deps } = setup({ isUserRecording: vi.fn(() => false) }); + stopRecordingWhenHudGoes(hud, deps); + + crash(hud); + + await vi.waitFor(() => expect(deps.discardRecording).toHaveBeenCalled()); + expect(deps.discardRecording).toHaveBeenCalledWith("/rec/recording-1.mp4"); + expect(deps.finishRecording).not.toHaveBeenCalled(); + expect(deps.setRecordingState).not.toHaveBeenCalled(); + expect(deps.reportRecovered).not.toHaveBeenCalled(); + }); + + it("waits for a running start before it decides", async () => { + let settle = () => {}; + let recording = false; + const { hud, deps } = setup({ + startSettled: vi.fn( + () => + new Promise((resolve) => { + settle = resolve; + }), + ), + isRecordingWithoutStopRequest: vi.fn(() => recording), + isUserRecording: vi.fn(() => false), + }); + stopRecordingWhenHudGoes(hud, deps); + + crash(hud); + await Promise.resolve(); + expect(deps.stopRecording).not.toHaveBeenCalled(); + // The helper comes up after the HUD died. + recording = true; + settle(); + + await vi.waitFor(() => expect(deps.discardRecording).toHaveBeenCalled()); + expect(deps.stopRecording).toHaveBeenCalledTimes(1); + }); + + it("still leaves the recording state and reports when stopping fails", async () => { + const { hud, deps } = setup({ + stopRecording: vi.fn(async () => { + throw new Error("helper gone"); + }), + }); + stopRecordingWhenHudGoes(hud, deps); + + crash(hud, "oom"); + + await vi.waitFor(() => expect(deps.reportRecovered).toHaveBeenCalled()); + expect(deps.setRecordingState).toHaveBeenCalledWith(false); + expect(deps.reportRecovered).toHaveBeenCalledWith( + { success: false, error: "Error: helper gone" }, + "oom", + ); + }); + + it("does not leave an unhandled rejection when the dialog fails", async () => { + const unhandled = vi.fn(); + process.on("unhandledRejection", unhandled); + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const { hud, deps } = setup({ + reportRecovered: vi.fn(async () => { + throw new Error("no display"); + }), + }); + stopRecordingWhenHudGoes(hud, deps); + + crash(hud); + + await vi.waitFor(() => + expect(error).toHaveBeenCalledWith( + "[hud-crash-recovery] Recovery failed:", + expect.any(Error), + ), + ); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(unhandled).not.toHaveBeenCalled(); + } finally { + process.off("unhandledRejection", unhandled); + error.mockRestore(); + } + }); + + it("adds one listener per HUD however many recordings it starts", () => { + const { hud, deps } = setup(); + for (let i = 0; i < 20; i++) stopRecordingWhenHudGoes(hud, deps); + + expect(hud.listenerCount("render-process-gone")).toBe(1); + }); +}); + +describe("createHudCrashRecoveryDeps", () => { + function platform( + state: Partial & { + pendingPath?: string; + pendingFileExists?: boolean; + countdownPath?: string; + countdownFileExists?: boolean; + }, + ) { + const capture: NativeCaptureState = { + nativeActive: false, + nativeStopRequested: false, + windowsActive: false, + windowsStopRequested: false, + userRecording: true, + lastStopEndedUserRecording: true, + ...state, + }; + const io = { + captureState: () => capture, + startSettled: vi.fn(async () => {}), + stopNativeScreenRecording: vi.fn(async () => { + // The real stop clears the active flags before it returns. + capture.nativeActive = false; + capture.windowsActive = false; + return { success: true, path: "C:/rec/recording-1.mp4" }; + }), + muxNativeWindowsRecording: vi.fn(async () => ({ + success: true, + path: "C:/rec/recording-1.mp4", + message: "muxed", + })), + clearWindowsPendingVideo: vi.fn(), + windowsPendingVideoPath: vi.fn(() => state.pendingPath ?? null), + stoppedCountdownPath: vi.fn(() => state.countdownPath ?? null), + clearStoppedCountdownPath: vi.fn(), + fileExists: vi.fn(async (filePath: string) => + filePath === state.countdownPath + ? (state.countdownFileExists ?? true) + : (state.pendingFileExists ?? true), + ), + deleteFile: vi.fn(async () => {}), + telemetryPathFor: (videoPath: string) => `${videoPath}.cursor.json`, + diagnosticsPathFor: (videoPath: string) => + videoPath.replace(/\.mp4$/, ".recording-diagnostics.json"), + setRecordingState: vi.fn(), + reportRecovered: vi.fn(async () => {}), + }; + const hud = new EventEmitter(); + stopRecordingWhenHudGoes(hud, createHudCrashRecoveryDeps(io)); + return { io, crash: () => crash(hud) }; + } + + it("muxes the audio on Windows, as the renderer would after a stop", async () => { + const { io, crash } = platform({ nativeActive: true, windowsActive: true }); + + crash(); + + await vi.waitFor(() => expect(io.reportRecovered).toHaveBeenCalled()); + expect(io.muxNativeWindowsRecording).toHaveBeenCalledTimes(1); + expect(io.reportRecovered).toHaveBeenCalledWith( + { success: true, path: "C:/rec/recording-1.mp4", message: "muxed" }, + "crashed", + ); + expect(io.setRecordingState).toHaveBeenCalledWith(false); + }); + + it("does not mux on macOS", async () => { + const { io, crash } = platform({ nativeActive: true }); + + crash(); + + await vi.waitFor(() => expect(io.reportRecovered).toHaveBeenCalled()); + expect(io.muxNativeWindowsRecording).not.toHaveBeenCalled(); + }); + + it("deletes a Windows countdown capture and its telemetry, without a mux", async () => { + const { io, crash } = platform({ + nativeActive: true, + windowsActive: true, + userRecording: false, + }); + + crash(); + + await vi.waitFor(() => expect(io.deleteFile).toHaveBeenCalledTimes(10)); + expect(io.clearWindowsPendingVideo).toHaveBeenCalled(); + expect(io.deleteFile).toHaveBeenNthCalledWith(1, "C:/rec/recording-1.mp4"); + expect(io.deleteFile).toHaveBeenNthCalledWith(2, "C:/rec/recording-1.mp4.cursor.json"); + expect(io.deleteFile).toHaveBeenNthCalledWith( + 3, + "C:/rec/recording-1.recording-diagnostics.json", + ); + expect(io.muxNativeWindowsRecording).not.toHaveBeenCalled(); + expect(io.reportRecovered).not.toHaveBeenCalled(); + }); + + it("leaves a Windows capture alone once its stop was requested, though it sets nativeActive too", async () => { + const { io, crash } = platform({ + nativeActive: true, + windowsActive: true, + windowsStopRequested: true, + }); + + crash(); + + await vi.waitFor(() => expect(io.startSettled).toHaveBeenCalled()); + await Promise.resolve(); + expect(io.stopNativeScreenRecording).not.toHaveBeenCalled(); + }); + + it("muxes a stopped Windows recording whose renderer died before its mux", async () => { + // After its Stop the renderer has already left the recording state. + const { io, crash } = platform({ + userRecording: false, + lastStopEndedUserRecording: true, + pendingPath: "C:/rec/recording-1.mp4", + }); + + crash(); + + await vi.waitFor(() => expect(io.reportRecovered).toHaveBeenCalled()); + expect(io.stopNativeScreenRecording).not.toHaveBeenCalled(); + expect(io.muxNativeWindowsRecording).toHaveBeenCalledTimes(1); + expect(io.deleteFile).not.toHaveBeenCalled(); + expect(io.setRecordingState).toHaveBeenCalledWith(false); + }); + + it("deletes a stopped Windows countdown capture whose renderer died before deleting it", async () => { + const { io, crash } = platform({ + userRecording: false, + lastStopEndedUserRecording: false, + pendingPath: "C:/rec/recording-1.mp4", + }); + + crash(); + + await vi.waitFor(() => expect(io.deleteFile).toHaveBeenCalledTimes(10)); + expect(io.deleteFile).toHaveBeenNthCalledWith(1, "C:/rec/recording-1.mp4"); + expect(io.clearWindowsPendingVideo).toHaveBeenCalled(); + expect(io.muxNativeWindowsRecording).not.toHaveBeenCalled(); + expect(io.reportRecovered).not.toHaveBeenCalled(); + }); + + it("deletes a countdown capture the renderer stopped but hadn't deleted (macOS)", async () => { + const { io, crash } = platform({ + userRecording: false, + countdownPath: "/rec/recording-1.mp4", + }); + + crash(); + + await vi.waitFor(() => expect(io.deleteFile).toHaveBeenCalledTimes(10)); + expect(io.deleteFile).toHaveBeenNthCalledWith(1, "/rec/recording-1.mp4"); + expect(io.deleteFile).toHaveBeenCalledWith("/rec/recording-1.system.m4a"); + expect(io.clearStoppedCountdownPath).toHaveBeenCalled(); + expect(io.stopNativeScreenRecording).not.toHaveBeenCalled(); + expect(io.reportRecovered).not.toHaveBeenCalled(); + }); + + it("only forgets a stopped countdown the renderer already deleted", async () => { + const { io, crash } = platform({ + userRecording: false, + countdownPath: "/rec/recording-1.mp4", + countdownFileExists: false, + }); + + crash(); + + await vi.waitFor(() => expect(io.clearStoppedCountdownPath).toHaveBeenCalled()); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(io.deleteFile).not.toHaveBeenCalled(); + }); + + it("only forgets a pending Windows video the renderer already deleted", async () => { + const { io, crash } = platform({ + userRecording: false, + pendingPath: "C:/rec/recording-1.mp4", + pendingFileExists: false, + }); + + crash(); + + await vi.waitFor(() => expect(io.clearWindowsPendingVideo).toHaveBeenCalled()); + await Promise.resolve(); + expect(io.muxNativeWindowsRecording).not.toHaveBeenCalled(); + expect(io.reportRecovered).not.toHaveBeenCalled(); + }); +}); + +describe("createStartTracker", () => { + it("waits for every overlapping start, not only the latest", async () => { + const starts = createStartTracker(); + let finishSlow = () => {}; + const slow = starts.track( + () => + new Promise((resolve) => { + finishSlow = resolve; + }), + ); + const fast = starts.track(async () => {}); + void slow(); + await fast(); + + let settled = false; + void starts.settled().then(() => { + settled = true; + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(settled).toBe(false); + + finishSlow(); + await vi.waitFor(() => expect(settled).toBe(true)); + }); + + it("settles after a start that fails", async () => { + const starts = createStartTracker(); + await expect( + starts.track(async () => Promise.reject(new Error("no helper")))(), + ).rejects.toThrow(); + await expect(starts.settled()).resolves.toBeUndefined(); + }); +}); + +describe("nativeCaptureSidecarPaths", () => { + it("lists the audio and native webcam files of one capture, and nothing else", () => { + expect(nativeCaptureSidecarPaths("/rec/recording-1.mp4")).toEqual([ + "/rec/recording-1.system.m4a", + "/rec/recording-1.mic.m4a", + "/rec/recording-1.system.wav", + "/rec/recording-1.mic.wav", + "/rec/recording-1.system.wav.json", + "/rec/recording-1.mic.wav.json", + "/rec/recording-1-webcam.mp4", + ]); + }); + + it("returns nothing for a path that isn't an MP4", () => { + expect(nativeCaptureSidecarPaths("/rec/recording-1.webm")).toEqual([]); + }); +}); diff --git a/electron/ipc/recording/hudCrashRecovery.ts b/electron/ipc/recording/hudCrashRecovery.ts new file mode 100644 index 000000000..8d89bf8e6 --- /dev/null +++ b/electron/ipc/recording/hudCrashRecovery.ts @@ -0,0 +1,264 @@ +export type StopResult = { + success: boolean; + path?: string; + message?: string; + error?: string; +}; + +type CrashableWebContents = { + once( + event: "render-process-gone", + listener: (event: unknown, details: { reason: string }) => void, + ): unknown; +}; + +export type HudCrashRecoveryDeps = { + /** Resolves when no start-native-screen-recording call is still running. */ + startSettled: () => Promise; + /** True while a native screen recording runs and nobody asked it to stop. */ + isRecordingWithoutStopRequest: () => boolean; + /** True after set-recording-state(true): the user sees a recording, not a countdown. */ + isUserRecording: () => boolean; + stopRecording: () => Promise; + /** Work the renderer normally does after a stop (the Windows mux). */ + finishRecording: (stopped: StopResult) => Promise; + /** Deletes a capture the user never started (warm start, countdown). */ + discardRecording: (path: string) => Promise; + /** + * Windows only: a stopped capture whose file still waits for the renderer's mux, and + * whether that Stop ended a recording (rather than a countdown). Resolves to null when + * there is none or its file is already gone. + */ + takePendingStoppedVideo: () => Promise<{ path: string; userRecording: boolean } | null>; + /** + * A countdown capture the renderer stopped but may not have deleted yet. Resolves to + * null when there is none or its file is already gone. + */ + takeStoppedCountdownCapture: () => Promise; + /** Runs the Windows mux for the pending stopped video. */ + muxPendingStoppedVideo: () => Promise; + setRecordingState: (recording: boolean) => void; + reportRecovered: (result: StopResult, reason: string) => Promise; +}; + +const watched = new WeakSet(); + +/** + * Stops a native recording when the renderer that controls it dies (issue #1139). + * + * The HUD renderer owns the Stop button. If it crashes mid-recording, the capture helper + * keeps writing, the tray Stop item talks to the dead renderer, and quitting kills the helper + * before the MP4 gets its moov atom. This finishes the recording the same way Stop does, + * so the file stays playable and the app leaves the recording state. + * + * A capture that was only warm-started for the countdown is stopped and deleted instead, + * as the renderer does when a countdown is cancelled. + * + * Each webContents is watched once, however many recordings it starts. + */ +export function stopRecordingWhenHudGoes(hud: CrashableWebContents, deps: HudCrashRecoveryDeps) { + if (watched.has(hud)) { + return; + } + watched.add(hud); + hud.once("render-process-gone", (_event, details) => { + watched.delete(hud); + if (details.reason === "clean-exit") { + return; + } + void recover(deps, details.reason).catch((error) => { + console.error("[hud-crash-recovery] Recovery failed:", error); + }); + }); +} + +async function recover(deps: HudCrashRecoveryDeps, reason: string) { + // A start still waiting for the helper would otherwise finish with no HUD to stop it. + await deps.startSettled(); + if (!deps.isRecordingWithoutStopRequest()) { + // On Windows the renderer muxes the audio after its Stop went through. If it died in + // between, the stopped file is still waiting: mux it when the Stop ended a recording, + // delete it when the Stop ended a countdown (the renderer would have deleted it next). + const pending = await deps.takePendingStoppedVideo(); + // A countdown the renderer stopped but hadn't deleted yet (any platform). + const stoppedCountdown = pending ? null : await deps.takeStoppedCountdownCapture(); + if (stoppedCountdown) { + await deps.discardRecording(stoppedCountdown); + } else if (pending && !pending.userRecording) { + await deps.discardRecording(pending.path); + } else if (pending) { + console.error( + `[hud-crash-recovery] HUD renderer gone (${reason}) before its mux; muxing the stopped recording`, + ); + let muxed: StopResult; + try { + muxed = await deps.muxPendingStoppedVideo(); + } catch (error) { + muxed = { success: false, error: String(error) }; + } + deps.setRecordingState(false); + await deps.reportRecovered(muxed, reason); + } + return; + } + const userRecording = deps.isUserRecording(); + console.error( + `[hud-crash-recovery] HUD renderer gone (${reason}) during a ${ + userRecording ? "recording" : "countdown" + }; stopping it`, + ); + + let result: StopResult; + try { + result = await deps.stopRecording(); + if (result.success && userRecording) { + result = await deps.finishRecording(result); + } + } catch (error) { + result = { success: false, error: String(error) }; + } + + if (!userRecording) { + if (result.success && result.path) { + await deps.discardRecording(result.path); + } else if (!result.success) { + console.error("[hud-crash-recovery] Could not stop the countdown capture:", result); + } + return; + } + deps.setRecordingState(false); + await deps.reportRecovered(result, reason); +} + +/** What main knows about the native capture, read at the moment recovery needs it. */ +export type NativeCaptureState = { + /** nativeScreenRecordingActive: set by the macOS start and also by the Windows start. */ + nativeActive: boolean; + /** Set by the macOS stop only. */ + nativeStopRequested: boolean; + windowsActive: boolean; + windowsStopRequested: boolean; + userRecording: boolean; + /** Whether the last stop-native-screen-recording ended a recording, not a countdown. */ + lastStopEndedUserRecording: boolean; +}; + +/** + * Builds the recovery steps from main's capture state and handlers, so the platform rules + * (Windows needs the mux the renderer would have run) are testable without Electron. + */ +export function createHudCrashRecoveryDeps(io: { + captureState: () => NativeCaptureState; + startSettled: () => Promise; + stopNativeScreenRecording: () => Promise; + muxNativeWindowsRecording: () => Promise; + clearWindowsPendingVideo: () => void; + windowsPendingVideoPath: () => string | null; + /** Video path of the last renderer stop that ended a countdown, until the next start. */ + stoppedCountdownPath: () => string | null; + clearStoppedCountdownPath: () => void; + fileExists: (filePath: string) => Promise; + deleteFile: (filePath: string) => Promise; + telemetryPathFor: (videoPath: string) => string; + /** The capture diagnostics file written next to the video (Windows). */ + diagnosticsPathFor: (videoPath: string) => string; + setRecordingState: (recording: boolean) => void; + reportRecovered: (result: StopResult, reason: string) => Promise; +}): HudCrashRecoveryDeps { + // Read before stopping: the stop clears the active flags. + let stoppedWindowsCapture = false; + return { + startSettled: io.startSettled, + isRecordingWithoutStopRequest: () => { + const state = io.captureState(); + // A Windows capture also sets nativeActive, but only its own stop flag, so judge + // it by the Windows flags alone. + if (state.windowsActive) { + return !state.windowsStopRequested; + } + return state.nativeActive && !state.nativeStopRequested; + }, + isUserRecording: () => io.captureState().userRecording, + stopRecording: () => { + stoppedWindowsCapture = io.captureState().windowsActive; + return io.stopNativeScreenRecording(); + }, + // On Windows the renderer muxes the audio after a stop; do it here instead. + finishRecording: async (stopped) => + stoppedWindowsCapture ? await io.muxNativeWindowsRecording() : stopped, + discardRecording: async (videoPath) => { + io.clearWindowsPendingVideo(); + for (const filePath of [ + videoPath, + io.telemetryPathFor(videoPath), + io.diagnosticsPathFor(videoPath), + ...nativeCaptureSidecarPaths(videoPath), + ]) { + await io.deleteFile(filePath); + } + }, + takePendingStoppedVideo: async () => { + const pending = io.windowsPendingVideoPath(); + if (!pending) { + return null; + } + if (await io.fileExists(pending)) { + return { + path: pending, + userRecording: io.captureState().lastStopEndedUserRecording, + }; + } + // The renderer already deleted it (a cancelled countdown): forget the stale path. + io.clearWindowsPendingVideo(); + return null; + }, + takeStoppedCountdownCapture: async () => { + const stopped = io.stoppedCountdownPath(); + io.clearStoppedCountdownPath(); + return stopped && (await io.fileExists(stopped)) ? stopped : null; + }, + muxPendingStoppedVideo: () => io.muxNativeWindowsRecording(), + setRecordingState: io.setRecordingState, + reportRecovered: io.reportRecovered, + }; +} + +/** + * Tracks start calls that are still running. `settled` waits for every one of them, + * including starts that overlap, not only the latest. + */ +export function createStartTracker() { + let inFlight: Promise = Promise.resolve(); + return { + track: + (start: (...args: A) => Promise) => + (...args: A): Promise => { + const started = start(...args); + inFlight = Promise.allSettled([inFlight, started]); + return started; + }, + settled: () => inFlight.then(() => undefined), + }; +} + +/** + * The files a native capture writes next to `recording-.mp4`: audio (`.system.m4a` and + * `.mic.m4a` on macOS, `.system.wav` and `.mic.wav` with `.json` metadata on Windows) and a + * natively recorded webcam (`-webcam.mp4`, macOS). Only names derived from the video's own + * base name, so nothing else can match; files that don't exist are skipped by the caller. + */ +export function nativeCaptureSidecarPaths(videoPath: string): string[] { + const base = videoPath.replace(/\.mp4$/i, ""); + if (base === videoPath) { + return []; + } + return [ + ".system.m4a", + ".mic.m4a", + ".system.wav", + ".mic.wav", + ".system.wav.json", + ".mic.wav.json", + "-webcam.mp4", + ].map((suffix) => `${base}${suffix}`); +} diff --git a/electron/ipc/recording/importRecording.ts b/electron/ipc/recording/importRecording.ts new file mode 100644 index 000000000..bab9b17fa --- /dev/null +++ b/electron/ipc/recording/importRecording.ts @@ -0,0 +1,263 @@ +import { composeSequenceWebcam, sequenceWebcamOutputs } from "./sequenceWebcam"; +import { probeNativeVideoMetadata } from "../ffmpeg/metadata"; +import { isLibrarySequenceSource } from "./sequenceSource"; +import { execFile } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { buildMediaUrl, getMediaServerBaseUrl } from "../../mediaServer"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; +import { rememberApprovedLocalReadPath, resolveApprovedLocalMediaPath } from "../project/manager"; +import { getRecordingsDir, getTelemetryPathForVideo } from "../utils"; +import { getUsableCompanionAudioCandidates } from "./diagnostics"; +import { normalizeCursorTelemetrySamples, writeCursorTelemetry } from "../cursor/telemetry"; +import { listRecordings } from "./library"; +import type { + RecordingImportResult, + RecordingWebcamSource, +} from "../../../src/types/recordingLibrary"; + +const run = promisify(execFile); +async function ffmpeg(args: string[], signal?: AbortSignal) { + await run( + getFfmpegBinaryPath(), + ["-hide_banner", "-loglevel", "error", "-nostdin", "-y", ...args], + { signal, timeout: 60 * 60 * 1000, maxBuffer: 1024 * 1024, windowsHide: true }, + ); +} +async function probe(file: string, signal?: AbortSignal) { + const meta = await probeNativeVideoMetadata(getFfmpegBinaryPath(), file, signal); + return { + width: meta.width, + height: meta.height, + fps: meta.frameRate, + duration: meta.duration, + audio: meta.hasAudio, + }; +} +type Format = { width: number; height: number; fps: number }; + +/** Normalize new media once. Existing sequence video is copied, avoiding generation loss. */ +async function normalize( + file: string, + out: string, + format: Format, + copyVideo: boolean, + signal?: AbortSignal, +) { + const meta = await probe(file, signal); + const candidates = await getUsableCompanionAudioCandidates(file); + const companion = candidates[0]; + const system = companion?.usablePaths.includes(companion.systemPath) + ? companion.systemPath + : null; + const mic = companion?.usablePaths.includes(companion.micPath) ? companion.micPath : null; + const args = ["-i", file]; + const filters: string[] = []; + let input = 1; + for (const [kind, source] of [ + ["system", system], + ["mic", mic], + ] as const) { + let label: string; + let delay = 0; + if (source) { + args.push("-i", source); + label = `${input++}:a:0`; + try { + delay = Math.max( + 0, + Number(JSON.parse(await fs.readFile(`${source}.json`, "utf8")).startDelayMs) || + 0, + ); + } catch { + /* Optional capture timing. */ + } + } else if (kind === "system" && meta.audio) { + label = "0:a:0"; + } else { + args.push("-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo"); + label = `${input++}:a:0`; + } + filters.push( + `[${label}]aresample=48000,aformat=sample_fmts=s16:channel_layouts=stereo,asetpts=PTS-STARTPTS,adelay=${Math.round(delay)}:all=1,apad,atrim=duration=${meta.duration}[${kind}]`, + ); + } + if (!copyVideo) + filters.push( + `[0:v:0]setpts=PTS-STARTPTS,scale=${format.width}:${format.height}:force_original_aspect_ratio=decrease,pad=${format.width}:${format.height}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=${format.fps},format=yuv420p[v]`, + ); + args.push( + "-filter_complex", + filters.join(";"), + "-map", + copyVideo ? "0:v:0" : "[v]", + "-map", + "[system]", + "-map", + "[mic]", + "-c:v", + copyVideo ? "copy" : "libx264", + ); + if (!copyVideo) args.push("-preset", "fast", "-crf", "18"); + args.push("-c:a", "pcm_s16le", "-t", String(meta.duration), out); + await ffmpeg(args, signal); +} + +/** The editor and export share one immutable source, with stable offsets across imports. */ +export async function importRecording( + currentPath: string, + recordingPath: string, + currentWebcam?: RecordingWebcamSource, + signal?: AbortSignal, +): Promise { + const entry = (await listRecordings()).find((entry) => entry.path === recordingPath); + if (!entry) throw new Error("Recording is no longer in Videos"); + const current = await resolveApprovedLocalMediaPath(currentPath); + if (!current) throw new Error("Current video is no longer available"); + const server = getMediaServerBaseUrl(); + if (!server) throw new Error("Media server is not ready"); + const root = path.join(await getRecordingsDir(), ".recordly-media"); + await fs.mkdir(root, { recursive: true }); + const base = await probe(current, signal); + const format = { + width: Math.ceil(base.width / 2) * 2, + height: Math.ceil(base.height / 2) * 2, + fps: base.fps, + }; + const id = randomUUID(); + const work = await fs.mkdtemp(path.join(root, "import-")); + const output = path.join(root, `${id}.mp4`); + const stem = output.slice(0, -4); + const outputs = recordingImportOutputs(output); + try { + const normalizedBase = path.join(work, "base.mkv"); + const normalizedNew = path.join(work, "new.mkv"); + // Only our own normalized source format may be copied across appends. + const copyVideo = isLibrarySequenceSource(current); + await normalize(current, normalizedBase, format, copyVideo, signal); + await normalize(entry.path, normalizedNew, format, false, signal); + const baseMeta = await probe(normalizedBase, signal); + const newMeta = await probe(normalizedNew, signal); + await fs.writeFile(path.join(work, "list.txt"), "file 'base.mkv'\nfile 'new.mkv'\n"); + const combined = path.join(work, "combined.mkv"); + await ffmpeg( + [ + "-f", + "concat", + "-safe", + "1", + "-i", + path.join(work, "list.txt"), + "-map", + "0", + "-c", + "copy", + combined, + ], + signal, + ); + await ffmpeg( + [ + "-i", + combined, + "-filter_complex", + "[0:a:0][0:a:1]amix=inputs=2:normalize=0[mix]", + "-map", + "0:v:0", + "-map", + "[mix]", + "-c:v", + "copy", + "-c:a", + "aac", + "-b:a", + "192k", + "-movflags", + "+faststart", + output, + "-map", + "0:a:0", + "-c:a", + "pcm_s16le", + `${stem}.system.wav`, + "-map", + "0:a:1", + "-c:a", + "pcm_s16le", + `${stem}.mic.wav`, + ], + signal, + ); + const sourceStartMs = Math.round(baseMeta.duration * 1000); + const samples = []; + for (const [file, offset, meta] of [ + [current, 0, base], + [entry.path, sourceStartMs, await probe(entry.path, signal)], + ] as const) { + let points: ReturnType = []; + try { + points = normalizeCursorTelemetrySamples( + JSON.parse(await fs.readFile(getTelemetryPathForVideo(file), "utf8")), + ); + } catch { + /* Recording may not contain cursor telemetry. */ + } + const scale = Math.min(format.width / meta.width, format.height / meta.height); + samples.push( + ...points.map((point) => ({ + ...point, + timeMs: point.timeMs + offset, + cx: + (point.cx * meta.width * scale + (format.width - meta.width * scale) / 2) / + format.width, + cy: + (point.cy * meta.height * scale + + (format.height - meta.height * scale) / 2) / + format.height, + })), + ); + } + await writeCursorTelemetry(output, samples); + const webcam = await composeSequenceWebcam( + current, + entry.path, + output, + work, + sourceStartMs, + Math.round(newMeta.duration * 1000), + currentWebcam, + signal, + ); + signal?.throwIfAborted(); + await rememberApprovedLocalReadPath(output); + return { + path: output, + webcam, + url: buildMediaUrl(server, output), + sourceStartMs, + durationMs: Math.round(newMeta.duration * 1000), + totalDurationMs: sourceStartMs + Math.round(newMeta.duration * 1000), + }; + } catch (error) { + await Promise.all(outputs.map((file) => fs.rm(file, { force: true }))); + throw error; + } finally { + await fs.rm(work, { recursive: true, force: true }); + } +} + +function recordingImportOutputs(output: string) { + const stem = output.slice(0, -4); + return [ + ...sequenceWebcamOutputs(output), + output, + `${stem}.system.wav`, + `${stem}.mic.wav`, + `${output}.cursor.json`, + ]; +} +export async function discardRecordingImport(output: string) { + await Promise.all(recordingImportOutputs(output).map((file) => fs.rm(file, { force: true }))); +} diff --git a/electron/ipc/recording/library.test.ts b/electron/ipc/recording/library.test.ts new file mode 100644 index 000000000..d44bf1028 --- /dev/null +++ b/electron/ipc/recording/library.test.ts @@ -0,0 +1,337 @@ +import { execFile } from "node:child_process"; +import { createRequire } from "node:module"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ root: "", approved: new Set() })); +vi.mock("electron", () => ({ + app: { getPath: () => state.root }, + shell: { + trashItem: vi.fn(async (file: string) => { + await fs.mkdir(path.join(state.root, ".test-trash"), { recursive: true }); + await fs.rename(file, path.join(state.root, ".test-trash", path.basename(file))); + }), + }, +})); +vi.mock("../../appPaths", () => ({ + USER_DATA_PATH: "/tmp/recordly-test", + RECORDINGS_DIR: "/tmp/recordly-test", +})); +vi.mock("../utils", () => ({ + getRecordingsDir: async () => state.root, + getTelemetryPathForVideo: (file: string) => `${file}.cursor.json`, + parseJsonWithByteOrderMark: JSON.parse, + normalizeVideoSourcePath: (file: string) => file, + getScreen: vi.fn(), +})); +vi.mock("../../mediaServer", () => ({ + getMediaServerBaseUrl: () => "http://127.0.0.1:9999", + buildMediaUrl: (server: string, file: string) => + `${server}/video?path=${encodeURIComponent(file)}`, +})); +vi.mock("../project/manager", () => ({ + rememberApprovedLocalReadPath: async (file: string) => { + state.approved.add(file); + }, + resolveApprovedLocalMediaPath: async (file: string) => (state.approved.has(file) ? file : null), + isPathInsideDirectory: (file: string, root: string) => file.startsWith(`${root}${path.sep}`), +})); +vi.mock("../ffmpeg/binary", async () => { + const require = createRequire(import.meta.url); + return { + getFfmpegBinaryPath: () => require("ffmpeg-static"), + getFfprobeBinaryPath: () => require("ffprobe-static").path, + }; +}); +import { listRecordings, setRecordingsRemoved, clearRecordingTrashUndo } from "./library"; +import { importRecording, discardRecordingImport } from "./importRecording"; +import { getRecordingThumbnail } from "./thumbnail"; +import { getCompanionAudioFallbackInfo } from "./diagnostics"; +const require = createRequire(import.meta.url); +const ffmpeg = require("ffmpeg-static") as string; +const run = promisify(execFile); +beforeEach(async () => { + state.root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "recordly-library-"))); + state.approved.clear(); +}); +afterEach(async () => { + await clearRecordingTrashUndo(); + await fs.rm(state.root, { recursive: true, force: true }); +}); + +it("lists recordings, moves recordings and their companions to Trash with reversible removals, and excludes companion media and symlinks", async () => { + const first = path.join(state.root, "recording-old.mp4"); + const second = path.join(state.root, "recording-new.mov"); + for (const name of [ + first, + second, + path.join(state.root, "recording-new.webcam.mp4"), + path.join(state.root, "recording-new.mic.wav"), + ]) + await fs.writeFile(name, "fixture"); + await fs.symlink(first, path.join(state.root, "linked.mp4")); + expect((await listRecordings()).map((entry) => entry.path).sort()).toEqual( + [first, second].sort(), + ); + await setRecordingsRemoved([first, second], true); + expect(await listRecordings()).toEqual([]); + await expect(fs.access(first)).rejects.toThrow(); + await expect(fs.access(path.join(state.root, "recording-new.mic.wav"))).rejects.toThrow(); + expect( + (await fs.readdir(state.root)).filter((name) => name.startsWith(".recordly-trash-")), + ).toHaveLength(1); + await setRecordingsRemoved([first, second], false); + expect(await listRecordings()).toHaveLength(2); + expect(await fs.readFile(first, "utf8")).toBe("fixture"); + expect(await fs.readFile(path.join(state.root, "recording-new.mic.wav"), "utf8")).toBe( + "fixture", + ); + await expect(setRecordingsRemoved(["/tmp/outside.mp4"], true)).rejects.toThrow("outside"); +}); + +it("imports different-sized recordings with playable video, separate audio, stable offsets, and untouched originals", async () => { + const base = path.join(state.root, "recording-base.mp4"); + const added = path.join(state.root, "recording-added.mp4"); + await run(ffmpeg, [ + "-v", + "error", + "-f", + "lavfi", + "-i", + "color=c=red:s=160x90:r=30:d=1", + "-f", + "lavfi", + "-i", + "sine=frequency=440:duration=1", + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-c:a", + "aac", + "-shortest", + base, + ]); + await run(ffmpeg, [ + "-v", + "error", + "-f", + "lavfi", + "-i", + "color=c=blue:s=90x160:r=30:d=1", + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + added, + ]); + const webcam = added.replace(".mp4", "-webcam.mp4"); + await run(ffmpeg, [ + "-v", + "error", + "-f", + "lavfi", + "-i", + "color=c=lime:s=80x60:r=30:d=0.8", + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + webcam, + ]); + await fs.writeFile( + added.replace(".mp4", ".recordly-session.json"), + JSON.stringify({ version: 2, webcamFileName: path.basename(webcam), timeOffsetMs: 200 }), + ); + await fs.writeFile( + `${added}.cursor.json`, + JSON.stringify({ + samples: [ + { timeMs: 200, cx: 0.2, cy: 0.4, interactionType: "click", cursorType: "pointer" }, + { + timeMs: 260, + cx: 0.2, + cy: 0.4, + interactionType: "mouseup", + cursorType: "pointer", + }, + { timeMs: 600, cx: 0.4, cy: 0.5, interactionType: "right-click" }, + ], + }), + ); + const original = await fs.readFile(base); + await listRecordings(); + const thumbnail = await getRecordingThumbnail(base); + expect(thumbnail).toMatch(/^data:image\/jpeg;base64,/); + const jpeg = Buffer.from(thumbnail.split(",")[1], "base64"); + expect([...jpeg.subarray(0, 2)]).toEqual([0xff, 0xd8]); + expect(await getRecordingThumbnail(base)).toBe(thumbnail); + await expect(getRecordingThumbnail("/tmp/unapproved.mp4")).rejects.toThrow("outside"); + const result = await importRecording(base, added); + expect(result.sourceStartMs).toBeCloseTo(1000, -1); + expect(result.durationMs).toBeCloseTo(1000, -1); + expect(result.webcam?.visibleRanges).toEqual([{ startMs: 1200, endMs: 2000 }]); + const cursor = JSON.parse(await fs.readFile(`${result.path}.cursor.json`, "utf8")).samples; + expect( + cursor.map((point: { timeMs: number; interactionType: string }) => [ + point.timeMs, + point.interactionType, + ]), + ).toEqual([ + [1200, "click"], + [1260, "mouseup"], + [1600, "right-click"], + ]); + expect(cursor[0].cursorType).toBe("pointer"); + expect(cursor[0].cx).toBeCloseTo(0.405078125); + const webcamPixels = await run( + ffmpeg, + [ + "-v", + "error", + "-ss", + "1.5", + "-i", + result.webcam!.sourcePath!, + "-vf", + "crop=2:2:40:30", + "-frames:v", + "1", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "-", + ], + { encoding: "buffer" }, + ); + expect(webcamPixels.stdout[1]).toBeGreaterThan(200); + const companions = await getCompanionAudioFallbackInfo(result.path); + expect(companions.paths).toHaveLength(2); + expect(companions.paths.every((file) => file.endsWith(".wav"))).toBe(true); + await run(ffmpeg, ["-v", "error", "-i", result.path, "-f", "null", "-"]); + const pixels = await run( + ffmpeg, + [ + "-v", + "error", + "-ss", + "1.5", + "-i", + result.path, + "-vf", + "crop=2:2:80:44", + "-frames:v", + "1", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "-", + ], + { encoding: "buffer" }, + ); + expect(pixels.stdout[2]).toBeGreaterThan(200); // blue centre survives aspect-ratio fitting + const second = await importRecording(result.path, added); + expect(second.sourceStartMs).toBeCloseTo(2000, -1); + expect(second.webcam?.visibleRanges).toEqual([ + { startMs: 1200, endMs: 2000 }, + { startMs: 2200, endMs: 3000 }, + ]); + await discardRecordingImport(result.path); + await expect(fs.access(result.path)).rejects.toThrow(); + await expect(fs.access(result.path.replace(/\.mp4$/, ".mic.wav"))).rejects.toThrow(); + await expect(fs.access(result.path.replace(/\.mp4$/, "-webcam.mp4"))).rejects.toThrow(); + await expect( + fs.access(result.path.replace(/\.mp4$/, ".recordly-session.json")), + ).rejects.toThrow(); + await expect(fs.access(`${result.path}.webcam-ranges.json`)).rejects.toThrow(); + await expect(fs.access(`${result.path}.cursor.json`)).rejects.toThrow(); + await expect(fs.access(second.path)).resolves.toBeUndefined(); + expect(await fs.readFile(base)).toEqual(original); + expect((await listRecordings()).map((entry) => entry.path).sort()).toEqual( + [base, added].sort(), + ); + await expect(importRecording("/tmp/unapproved.mp4", added)).rejects.toThrow( + "no longer available", + ); +}, 60000); + +it("retains staged originals for undo when the OS refuses Trash", async () => { + const { shell } = await import("electron"); + const file = path.join(state.root, "recording-failure.mp4"); + await fs.writeFile(file, "original"); + await setRecordingsRemoved([file], true); + vi.mocked(shell.trashItem).mockRejectedValueOnce(new Error("Trash unavailable")); + await expect(clearRecordingTrashUndo()).rejects.toThrow("Trash unavailable"); + await setRecordingsRemoved([file], false); + expect(await fs.readFile(file, "utf8")).toBe("original"); +}); + +it("undo never overwrites a new file at the original location", async () => { + const file = path.join(state.root, "recording-conflict.mp4"); + await fs.writeFile(file, "original"); + await setRecordingsRemoved([file], true); + await fs.writeFile(file, "new recording"); + await expect(setRecordingsRemoved([file], false)).rejects.toThrow("already exists"); + expect(await fs.readFile(file, "utf8")).toBe("new recording"); +}); + +it("cancels an active import and removes partial outputs without changing originals", async () => { + const base = path.join(state.root, "recording-base.mp4"); + const added = path.join(state.root, "recording-added.mp4"); + await run(ffmpeg, [ + "-v", + "error", + "-f", + "lavfi", + "-i", + "color=c=red:s=640x480:r=30:d=10", + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + base, + ]); + await fs.copyFile(base, added); + state.approved.add(base); + const original = await fs.readFile(base); + const controller = new AbortController(); + const importing = importRecording(base, added, undefined, controller.signal); + const rejected = expect(importing).rejects.toMatchObject({ name: "AbortError" }); + await vi.waitFor(async () => { + const files = await fs.readdir(path.join(state.root, ".recordly-media")); + expect(files.some((name) => name.startsWith("import-"))).toBe(true); + }); + controller.abort(); + await rejected; + expect(await fs.readdir(path.join(state.root, ".recordly-media"))).toEqual([]); + expect(await fs.readFile(base)).toEqual(original); + expect(await fs.readFile(added)).toEqual(original); +}); + +it("restores on volumes without hard links and preserves conflicts", async () => { + const file = path.join(state.root, "recording-exfat.mp4"); + await fs.writeFile(file, "recording"); + await setRecordingsRemoved([file], true); + const link = vi + .spyOn(fs, "link") + .mockRejectedValue(Object.assign(new Error("unsupported"), { code: "ENOTSUP" })); + try { + await setRecordingsRemoved([file], false); + expect(await fs.readFile(file, "utf8")).toBe("recording"); + } finally { + link.mockRestore(); + } +}); + +it("Raw includes camera and audio sources without including metadata or symlinks", async () => { + const files = ["screen.mp4", "screen.webcam.mp4", "screen.mic.wav", "screen.system.m4a"]; + for (const name of [...files, "screen.cursor.json"]) await fs.writeFile(path.join(state.root, name), "fixture"); + await fs.symlink(path.join(state.root, "screen.mp4"), path.join(state.root, "linked.mp4")); + expect((await listRecordings(true)).map(entry => entry.name).sort()).toEqual(files.sort()); + expect((await listRecordings()).map(entry => entry.name)).toEqual(["screen.mp4"]); +}); diff --git a/electron/ipc/recording/library.ts b/electron/ipc/recording/library.ts new file mode 100644 index 000000000..826841375 --- /dev/null +++ b/electron/ipc/recording/library.ts @@ -0,0 +1,176 @@ +import fs from "node:fs/promises"; +import { constants } from "node:fs"; +import path from "node:path"; +import { shell } from "electron"; +import { buildMediaUrl, getMediaServerBaseUrl } from "../../mediaServer"; +import { rememberApprovedLocalReadPath } from "../project/manager"; +import { getRecordingsDir } from "../utils"; +import type { RecordingLibraryEntry } from "../../../src/types/recordingLibrary"; + +let mutation = Promise.resolve(); +const undoBatches = new Map(); +const isRecording = (name: string) => + /\.(mp4|mov|webm|mkv|m4v)$/i.test(name) && !/[.-]webcam[.-]/i.test(name); +const batchKey = (paths: string[]) => JSON.stringify([...new Set(paths)].sort()); + +export function listRecordings(includeSources = false): Promise { + const task = mutation.then(async () => { + const root = await fs.realpath(await getRecordingsDir()); + const server = getMediaServerBaseUrl(); + if (!server) throw new Error("Media server is not ready. Try again."); + const entries = await fs.readdir(root, { withFileTypes: true }); + for (const entry of entries) { + const staged = path.join(root, entry.name); + if ( + entry.isDirectory() && + /^\.recordly-trash-[A-Za-z0-9]{6}$/.test(entry.name) && + ![...undoBatches.values()].some((batch) => batch.bundle === staged) + ) { + await shell.trashItem(staged); + } + } + const result: RecordingLibraryEntry[] = []; + for (const entry of entries) { + if (!entry.isFile() || !(includeSources ? /\.(mp4|mov|webm|mkv|m4v|wav|m4a|mp3|ogg|flac)$/i.test(entry.name) : isRecording(entry.name))) continue; + const filePath = path.join(root, entry.name); + const stat = await fs.stat(filePath); + if (!stat.size) continue; + await rememberApprovedLocalReadPath(filePath); + result.push({ + path: filePath, + name: entry.name, + bytes: stat.size, + createdAt: stat.mtimeMs, + url: buildMediaUrl(server, filePath), + }); + } + return result.sort((a, b) => b.createdAt - a.createdAt); + }); + mutation = task.then( + () => undefined, + () => undefined, + ); + return task; +} + +// Only capture sidecars belonging to this exact recording, never adjacent recordings or projects. +function belongsToRecording(name: string, video: string) { + const stem = video.slice(0, -path.extname(video).length); + return ( + name === video || + name === `${video}.cursor.json` || + (name.startsWith(`${stem}.`) && + /\.(?:system|mic|microphone|audio|webcam)\.(?:wav|webm|mp4|m4a)(?:\.json)?$/i.test( + name, + )) || + name === `${stem}.recordly-session.json` || + name === `${stem}.recording-session.json` || + (name.startsWith(`${stem}-webcam.`) && /\.(?:mp4|webm|mov|mkv|avi)$/i.test(name)) + ); +} + +/** Stage a bundle by rename for Undo; the next removal or app exit sends it to Trash. */ +export function setRecordingsRemoved(paths: string[], removed: boolean): Promise { + const task = mutation.then(async () => { + if ( + typeof removed !== "boolean" || + !Array.isArray(paths) || + !paths.length || + paths.some((value) => typeof value !== "string") + ) + throw new Error("Invalid recording selection"); + const root = await fs.realpath(await getRecordingsDir()); + const selected = [...new Set(paths)]; + for (const candidate of selected) { + if (path.dirname(candidate) !== root || !isRecording(path.basename(candidate))) + throw new Error("Recording is outside the Videos library"); + } + const key = batchKey(selected); + if (!removed) { + const batch = undoBatches.get(key); + if (!batch) + throw new Error( + "This removal can no longer be undone. Restore the files from Trash.", + ); + // Check every destination before restoring any file; never overwrite newer media. + for (const file of batch.files) { + if ( + await fs.lstat(file).then( + () => true, + (error) => { + if (error.code === "ENOENT") return false; + throw error; + }, + ) + ) + throw new Error( + "A file with this name already exists. Restore it from Trash instead.", + ); + } + const restored: string[] = []; + try { + for (const file of batch.files) { + const staged = path.join(batch.bundle, path.basename(file)); + try { + await fs.link(staged, file); + } catch (error) { + if ( + !["EPERM", "ENOTSUP", "EOPNOTSUPP", "EXDEV"].includes( + (error as NodeJS.ErrnoException).code ?? "", + ) + ) + throw error; + await fs.copyFile(staged, file, constants.COPYFILE_EXCL); + } + restored.push(file); + } + } catch (error) { + await Promise.all(restored.map((file) => fs.unlink(file))); + throw error; + } + undoBatches.delete(key); + await fs.rm(batch.bundle, { recursive: true, force: true }); + return; + } + const available = await fs.readdir(root, { withFileTypes: true }); + for (const file of selected) { + if (!available.some((entry) => entry.name === path.basename(file) && entry.isFile())) + throw new Error("Recording is missing or is not a regular file"); + } + const files = available + .filter( + (entry) => + entry.isFile() && + selected.some((file) => belongsToRecording(entry.name, path.basename(file))), + ) + .map((entry) => path.join(root, entry.name)); + await finishPendingTrash(); + const bundle = await fs.mkdtemp(path.join(root, ".recordly-trash-")); + const moved: string[] = []; + try { + for (const file of files) { + await fs.rename(file, path.join(bundle, path.basename(file))); + moved.push(file); + } + undoBatches.set(key, { bundle, files }); + } catch (error) { + for (const file of moved) await fs.rename(path.join(bundle, path.basename(file)), file); + await fs.rmdir(bundle); + throw error; + } + }); + mutation = task.catch(() => undefined); + return task; +} + +async function finishPendingTrash() { + for (const [key, batch] of undoBatches) { + await shell.trashItem(batch.bundle); + undoBatches.delete(key); + } +} +export function clearRecordingTrashUndo() { + const task = mutation.then(finishPendingTrash); + mutation = task.catch(() => undefined); + return task; +} diff --git a/electron/ipc/recording/mac.ts b/electron/ipc/recording/mac.ts new file mode 100644 index 000000000..bec79bc06 --- /dev/null +++ b/electron/ipc/recording/mac.ts @@ -0,0 +1,348 @@ +import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import fs from "node:fs/promises"; +import { BrowserWindow } from "electron"; +import { + persistPendingCursorTelemetry, + snapshotCursorTelemetryForPersistence, +} from "../cursor/telemetry"; +import { + lastNativeCaptureDiagnostics, + nativeCaptureMicrophonePath, + nativeCaptureOutputBuffer, + nativeCaptureStopRequested, + nativeCaptureSystemAudioPath, + nativeCaptureTargetPath, + nativeScreenRecordingActive, + selectedSource, + setCurrentProjectPath, + setCurrentVideoPath, + setNativeCaptureMicrophonePath, + setNativeCaptureProcess, + setNativeCaptureStopRequested, + setNativeCaptureSystemAudioPath, + setNativeCaptureTargetPath, + setNativeScreenRecordingActive, +} from "../state"; +import { moveFileWithOverwrite } from "../utils"; +import { + getFileSizeIfPresent, + recordNativeCaptureDiagnostics, + validateRecordedVideo, +} from "./diagnostics"; +import { emitRecordingInterrupted } from "./events"; +import { getFinalMacCompanionAudioPath } from "./macCompanionAudio"; + +export function waitForNativeCaptureStart(process: ChildProcessWithoutNullStreams) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup(); + reject(new Error("Timed out waiting for ScreenCaptureKit recorder to start")); + }, 12000); + + let stdoutBuffer = ""; + const onStdout = (chunk: Buffer) => { + stdoutBuffer += chunk.toString(); + if (stdoutBuffer.includes("Recording started")) { + cleanup(); + resolve(); + } + }; + + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + + const onExit = (code: number | null) => { + cleanup(); + reject( + new Error( + nativeCaptureOutputBuffer.trim() || + `Native capture helper exited before recording started (code ${code ?? "unknown"})`, + ), + ); + }; + + const cleanup = () => { + clearTimeout(timer); + process.stdout.off("data", onStdout); + process.off("error", onError); + process.off("exit", onExit); + }; + + process.stdout.on("data", onStdout); + process.once("error", onError); + process.once("exit", onExit); + }); +} + +export function waitForNativeCaptureCommand( + process: ChildProcessWithoutNullStreams, + marker: "Recording paused" | "Recording resumed", +) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup(); + reject(new Error(`Timed out waiting for ScreenCaptureKit helper: ${marker}`)); + }, 5000); + + let stdoutBuffer = ""; + const onStdout = (chunk: Buffer) => { + stdoutBuffer += chunk.toString(); + if (stdoutBuffer.includes(marker)) { + cleanup(); + resolve(); + } + }; + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + const onExit = (code: number | null) => { + cleanup(); + reject( + new Error( + `Native capture helper exited before ${marker.toLowerCase()} (code ${code ?? "unknown"})`, + ), + ); + }; + const cleanup = () => { + clearTimeout(timer); + process.stdout.off("data", onStdout); + process.off("error", onError); + process.off("exit", onExit); + }; + + process.stdout.on("data", onStdout); + process.once("error", onError); + process.once("exit", onExit); + }); +} + +export function waitForNativeCaptureStop(process: ChildProcessWithoutNullStreams) { + return new Promise((resolve, reject) => { + const onClose = (code: number | null) => { + cleanup(); + const match = nativeCaptureOutputBuffer.match(/Recording stopped\. Output path: (.+)/); + if (match?.[1]) { + resolve(match[1].trim()); + return; + } + if (code === 0 && nativeCaptureTargetPath) { + resolve(nativeCaptureTargetPath); + return; + } + reject( + new Error( + nativeCaptureOutputBuffer.trim() || + `Native capture helper exited with code ${code ?? "unknown"}`, + ), + ); + }; + + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + + const cleanup = () => { + process.off("close", onClose); + process.off("error", onError); + }; + + process.once("close", onClose); + process.once("error", onError); + }); +} + +export async function muxNativeMacRecordingWithAudio( + videoPath: string, + systemAudioPath?: string | null, + microphonePath?: string | null, +) { + console.log("[mac-mux] Optimization active: keeping tracks separate."); + + // Optimization: instead of heavy FFmpeg muxing, we ensure audio sidecars + // are available alongside the video for the editor. + if (systemAudioPath) { + const finalSystemPath = getFinalMacCompanionAudioPath(videoPath, systemAudioPath, "system"); + try { + const stat = await fs.stat(systemAudioPath); + if (stat.size > 0 && systemAudioPath !== finalSystemPath) { + await moveFileWithOverwrite(systemAudioPath, finalSystemPath); + } + } catch (err) { + console.error(`[mac-mux] Failed to handle system audio:`, err); + } + } + + if (microphonePath) { + const finalMicPath = getFinalMacCompanionAudioPath(videoPath, microphonePath, "mic"); + try { + const stat = await fs.stat(microphonePath); + if (stat.size > 0 && microphonePath !== finalMicPath) { + await moveFileWithOverwrite(microphonePath, finalMicPath); + } + } catch (err) { + console.error(`[mac-mux] Failed to handle mic audio:`, err); + } + } +} + +export function attachNativeCaptureLifecycle(process: ChildProcessWithoutNullStreams) { + process.once("close", () => { + const wasActive = nativeScreenRecordingActive; + setNativeCaptureProcess(null); + + if (!wasActive || nativeCaptureStopRequested) { + return; + } + + setNativeScreenRecordingActive(false); + console.log("[mac-finalize] Optimization active: skipping safety-net muxing."); + setNativeCaptureTargetPath(null); + setNativeCaptureStopRequested(false); + setNativeCaptureSystemAudioPath(null); + setNativeCaptureMicrophonePath(null); + + const sourceName = selectedSource?.name ?? "Screen"; + BrowserWindow.getAllWindows().forEach((window) => { + if (!window.isDestroyed()) { + window.webContents.send("recording-state-changed", { + recording: false, + sourceName, + }); + } + }); + + const reason = nativeCaptureOutputBuffer.includes("WINDOW_UNAVAILABLE") + ? "window-unavailable" + : "capture-stopped"; + const message = + reason === "window-unavailable" + ? "The selected window is no longer capturable. Please reselect a window." + : "Recording stopped unexpectedly."; + + emitRecordingInterrupted(reason, message); + }); +} + +export async function finalizeStoredVideo(videoPath: string) { + console.log("[finalize] Optimization active: skipping safety-net muxing."); + + let validation: { fileSizeBytes: number; durationSeconds: number | null }; + try { + validation = await validateRecordedVideo(videoPath); + } catch (error) { + if ( + lastNativeCaptureDiagnostics?.backend === "mac-screencapturekit" || + lastNativeCaptureDiagnostics?.backend === "windows-wgc" + ) { + recordNativeCaptureDiagnostics({ + backend: lastNativeCaptureDiagnostics.backend, + phase: lastNativeCaptureDiagnostics.phase === "mux" ? "mux" : "stop", + sourceId: lastNativeCaptureDiagnostics.sourceId ?? null, + sourceType: lastNativeCaptureDiagnostics.sourceType ?? "unknown", + displayId: lastNativeCaptureDiagnostics.displayId ?? null, + displayBounds: lastNativeCaptureDiagnostics.displayBounds ?? null, + windowHandle: lastNativeCaptureDiagnostics.windowHandle ?? null, + helperPath: lastNativeCaptureDiagnostics.helperPath ?? null, + outputPath: videoPath, + systemAudioPath: lastNativeCaptureDiagnostics.systemAudioPath ?? null, + microphonePath: lastNativeCaptureDiagnostics.microphonePath ?? null, + osRelease: lastNativeCaptureDiagnostics.osRelease, + supported: lastNativeCaptureDiagnostics.supported, + helperExists: lastNativeCaptureDiagnostics.helperExists, + processOutput: lastNativeCaptureDiagnostics.processOutput, + fileSizeBytes: await getFileSizeIfPresent(videoPath), + error: error instanceof Error ? error.message : String(error), + }); + } + throw error; + } + + snapshotCursorTelemetryForPersistence(); + setCurrentVideoPath(videoPath); + setCurrentProjectPath(null); + try { + await persistPendingCursorTelemetry(videoPath); + } catch (error) { + console.warn("[mac-stop] Failed to persist cursor telemetry:", error); + } + + if ( + lastNativeCaptureDiagnostics?.backend === "mac-screencapturekit" || + lastNativeCaptureDiagnostics?.backend === "windows-wgc" + ) { + recordNativeCaptureDiagnostics({ + backend: lastNativeCaptureDiagnostics.backend, + phase: lastNativeCaptureDiagnostics.phase === "mux" ? "mux" : "stop", + sourceId: lastNativeCaptureDiagnostics.sourceId ?? null, + sourceType: lastNativeCaptureDiagnostics.sourceType ?? "unknown", + displayId: lastNativeCaptureDiagnostics.displayId ?? null, + displayBounds: lastNativeCaptureDiagnostics.displayBounds ?? null, + windowHandle: lastNativeCaptureDiagnostics.windowHandle ?? null, + helperPath: lastNativeCaptureDiagnostics.helperPath ?? null, + outputPath: videoPath, + systemAudioPath: lastNativeCaptureDiagnostics.systemAudioPath ?? null, + microphonePath: lastNativeCaptureDiagnostics.microphonePath ?? null, + osRelease: lastNativeCaptureDiagnostics.osRelease, + supported: lastNativeCaptureDiagnostics.supported, + helperExists: lastNativeCaptureDiagnostics.helperExists, + processOutput: lastNativeCaptureDiagnostics.processOutput, + fileSizeBytes: validation.fileSizeBytes, + }); + } + + return { + success: true, + path: videoPath, + message: + validation.durationSeconds !== null + ? `Video stored successfully (${validation.fileSizeBytes} bytes, ${validation.durationSeconds.toFixed(2)}s)` + : `Video stored successfully`, + }; +} + +export async function recoverNativeMacCaptureOutput() { + const macDiagnostics = + lastNativeCaptureDiagnostics?.backend === "mac-screencapturekit" + ? lastNativeCaptureDiagnostics + : null; + const diagnosticsPath = macDiagnostics?.outputPath ?? null; + const candidatePath = nativeCaptureTargetPath ?? diagnosticsPath; + const systemAudioPath = nativeCaptureSystemAudioPath ?? macDiagnostics?.systemAudioPath ?? null; + const microphonePath = nativeCaptureMicrophonePath ?? macDiagnostics?.microphonePath ?? null; + + if (!candidatePath) { + return null; + } + + try { + if (systemAudioPath || microphonePath) { + try { + await muxNativeMacRecordingWithAudio( + candidatePath, + systemAudioPath, + microphonePath, + ); + } catch (muxError) { + console.warn("Failed to mux audio during recovery:", muxError); + } + } + + return await finalizeStoredVideo(candidatePath); + } catch (error) { + recordNativeCaptureDiagnostics({ + backend: "mac-screencapturekit", + phase: "stop", + outputPath: candidatePath, + systemAudioPath, + microphonePath, + processOutput: nativeCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: await getFileSizeIfPresent(candidatePath), + error: String(error), + }); + return null; + } +} diff --git a/electron/ipc/recording/macCompanionAudio.test.ts b/electron/ipc/recording/macCompanionAudio.test.ts new file mode 100644 index 000000000..269a76623 --- /dev/null +++ b/electron/ipc/recording/macCompanionAudio.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { getFinalMacCompanionAudioPath } from "./macCompanionAudio"; + +describe("mac companion audio paths", () => { + it("preserves the helper's AAC container extension", () => { + expect( + getFinalMacCompanionAudioPath( + "/Users/egg/Recordly/recording-1.mp4", + "/Users/egg/Recordly/recording-1.mic.m4a", + "mic", + ), + ).toBe("/Users/egg/Recordly/recording-1.mic.m4a"); + }); + + it("preserves legacy sidecar extensions instead of renaming bytes", () => { + expect( + getFinalMacCompanionAudioPath( + "/Users/egg/Recordly/recording-1.mp4", + "/tmp/recordly-native.system.webm", + "system", + ), + ).toBe("/Users/egg/Recordly/recording-1.system.webm"); + }); + + it("keeps dotted directories when the video path has no extension", () => { + expect( + getFinalMacCompanionAudioPath( + "/Users/egg/Recordly.videos/recording-1", + "/tmp/recordly-native.mic.m4a", + "mic", + ), + ).toBe("/Users/egg/Recordly.videos/recording-1.mic.m4a"); + }); +}); diff --git a/electron/ipc/recording/macCompanionAudio.ts b/electron/ipc/recording/macCompanionAudio.ts new file mode 100644 index 000000000..fdfc9e26b --- /dev/null +++ b/electron/ipc/recording/macCompanionAudio.ts @@ -0,0 +1,16 @@ +import path from "node:path"; + +export type MacCompanionAudioSuffix = "system" | "mic"; + +export function getFinalMacCompanionAudioPath( + videoPath: string, + sourceAudioPath: string, + suffix: MacCompanionAudioSuffix, +) { + const separatorIndex = Math.max(videoPath.lastIndexOf("/"), videoPath.lastIndexOf("\\")); + const videoDirectory = separatorIndex >= 0 ? videoPath.slice(0, separatorIndex + 1) : ""; + const videoFileName = separatorIndex >= 0 ? videoPath.slice(separatorIndex + 1) : videoPath; + const videoPathWithoutExt = `${videoDirectory}${path.parse(videoFileName).name}`; + const sourceExtension = path.extname(sourceAudioPath).toLowerCase() || ".m4a"; + return `${videoPathWithoutExt}.${suffix}${sourceExtension}`; +} diff --git a/electron/ipc/recording/sequenceSource.ts b/electron/ipc/recording/sequenceSource.ts new file mode 100644 index 000000000..45abfee34 --- /dev/null +++ b/electron/ipc/recording/sequenceSource.ts @@ -0,0 +1,9 @@ +import path from "node:path"; + +/** Sources generated by Videos imports have independent, lossless audio companions. */ +export function isLibrarySequenceSource(file: string): boolean { + return ( + path.basename(path.dirname(file)) === ".recordly-media" && + /^[0-9a-f-]{36}\.mp4$/.test(path.basename(file)) + ); +} diff --git a/electron/ipc/recording/sequenceWebcam.ts b/electron/ipc/recording/sequenceWebcam.ts new file mode 100644 index 000000000..a6ed5c9c0 --- /dev/null +++ b/electron/ipc/recording/sequenceWebcam.ts @@ -0,0 +1,142 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; +import { probeNativeVideoMetadata } from "../ffmpeg/metadata"; +import { resolveRecordingSession, persistRecordingSessionManifest } from "../project/session"; +import { rememberApprovedLocalReadPath, resolveApprovedLocalMediaPath } from "../project/manager"; +import type { RecordingWebcamSource } from "../../../src/types/recordingLibrary"; + +const run = promisify(execFile); +type Range = { startMs: number; endMs: number }; +const rangesPath = (video: string) => `${video}.webcam-ranges.json`; +export const sequenceWebcamOutputs = (video: string) => [ + video.replace(/\.mp4$/, "-webcam.mp4"), + video.replace(/\.mp4$/, ".recordly-session.json"), + rangesPath(video), +]; +async function linked(video: string): Promise { + const session = await resolveRecordingSession(video); + if (!session?.webcamPath) return undefined; + const webcamPath = await fs.realpath(session.webcamPath); + if (path.dirname(webcamPath) !== path.dirname(video)) + throw new Error("Linked webcam is outside the recording folder"); + let visibleRanges: Range[] | undefined; + try { + visibleRanges = JSON.parse(await fs.readFile(rangesPath(video), "utf8")); + } catch { + /* Original recordings have no range file. */ + } + return { sourcePath: webcamPath, timeOffsetMs: session.timeOffsetMs ?? 0, visibleRanges }; +} +async function ffmpeg(args: string[], signal?: AbortSignal) { + await run( + getFfmpegBinaryPath(), + ["-hide_banner", "-loglevel", "error", "-nostdin", "-y", ...args], + { signal, timeout: 60 * 60 * 1000, maxBuffer: 1024 * 1024 }, + ); +} + +/** Keep webcam media separate from the screen, including blank spans for screen-only clips. */ +export async function composeSequenceWebcam( + current: string, + added: string, + output: string, + work: string, + baseDurationMs: number, + addedDurationMs: number, + currentWebcam?: RecordingWebcamSource, + signal?: AbortSignal, +) { + const base = currentWebcam === undefined ? await linked(current) : currentWebcam; + if (currentWebcam?.sourcePath) { + const approved = await resolveApprovedLocalMediaPath(currentWebcam.sourcePath); + if (!approved) throw new Error("Current webcam is no longer available"); + base!.sourcePath = approved; + } + const next = await linked(added); + const firstPath = base?.sourcePath || next?.sourcePath; + if (!firstPath) return undefined; + const meta = await probeNativeVideoMetadata(getFfmpegBinaryPath(), firstPath, signal); + const width = Math.ceil(meta.width / 2) * 2; + const height = Math.ceil(meta.height / 2) * 2; + const ranges: Range[] = []; + for (const [index, source, durationMs, offsetMs] of [ + [0, base, baseDurationMs, 0], + [1, next, addedDurationMs, baseDurationMs], + ] as const) { + const duration = durationMs / 1000; + const args: string[] = []; + let filter = `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30,format=yuv420p`; + if (source?.sourcePath) { + const info = await probeNativeVideoMetadata( + getFfmpegBinaryPath(), + source.sourcePath, + signal, + ); + const delayMs = Number.isFinite(source.timeOffsetMs) ? source.timeOffsetMs : 0; + args.push("-i", source.sourcePath); + filter = `trim=start=${Math.max(0, -delayMs) / 1000},setpts=PTS-STARTPTS,${filter},tpad=start_mode=add:start_duration=${Math.max(0, delayMs) / 1000}:stop_mode=add:stop_duration=${duration},trim=duration=${duration}`; + const active = source.visibleRanges ?? [ + { + startMs: Math.max(0, delayMs), + endMs: Math.min(durationMs, info.duration * 1000 + delayMs), + }, + ]; + for (const range of active) { + if (!Number.isFinite(range.startMs) || !Number.isFinite(range.endMs)) continue; + const startMs = Math.max(0, range.startMs); + const endMs = Math.min(durationMs, range.endMs); + if (endMs > startMs) + ranges.push({ startMs: offsetMs + startMs, endMs: offsetMs + endMs }); + } + } else { + args.push("-f", "lavfi", "-i", `color=c=black:s=${width}x${height}:r=30:d=${duration}`); + } + await ffmpeg( + [ + ...args, + "-an", + "-vf", + filter, + "-t", + String(duration), + "-c:v", + "libx264", + "-preset", + "fast", + "-crf", + "18", + path.join(work, `webcam-${index}.mp4`), + ], + signal, + ); + } + await fs.writeFile( + path.join(work, "webcam-list.txt"), + "file 'webcam-0.mp4'\nfile 'webcam-1.mp4'\n", + ); + const webcamPath = sequenceWebcamOutputs(output)[0]; + await ffmpeg( + [ + "-f", + "concat", + "-safe", + "1", + "-i", + path.join(work, "webcam-list.txt"), + "-an", + "-c:v", + "copy", + "-movflags", + "+faststart", + webcamPath, + ], + signal, + ); + await persistRecordingSessionManifest({ videoPath: output, webcamPath, timeOffsetMs: 0 }); + await fs.writeFile(rangesPath(output), JSON.stringify(ranges)); + await rememberApprovedLocalReadPath(webcamPath); + return { sourcePath: webcamPath, timeOffsetMs: 0, visibleRanges: ranges }; +} diff --git a/electron/ipc/recording/shareThumbnail.test.ts b/electron/ipc/recording/shareThumbnail.test.ts new file mode 100644 index 000000000..825809cec --- /dev/null +++ b/electron/ipc/recording/shareThumbnail.test.ts @@ -0,0 +1,68 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, it, vi } from "vitest"; +import ffmpeg from "ffmpeg-static"; + +vi.mock("../ffmpeg/binary", () => ({ getFfmpegBinaryPath: () => ffmpeg })); +import { createShareThumbnail } from "./shareThumbnail"; +const run = promisify(execFile); + +describe("shared recording preview", () => { + it("extracts the first frame, preserving aspect ratio and bounding its size", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "recordly-poster-test-")); + try { + const video = path.join(dir, "red-then-blue.mp4"); + await run(ffmpeg!, [ + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + "color=red:s=1280x720:d=0.1:r=30", + "-f", + "lavfi", + "-i", + "color=blue:s=1280x720:d=0.1:r=30", + "-filter_complex", + "[0:v][1:v]concat=n=2:v=1:a=0", + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + video, + ]); + const poster = await createShareThumbnail(video, new AbortController().signal); + expect(poster.subarray(0, 2)).toEqual(Buffer.from([0xff, 0xd8])); + const decoded = await new Promise((resolve, reject) => { + const child = execFile( + ffmpeg!, + [ + "-hide_banner", + "-loglevel", + "error", + "-i", + "pipe:0", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "pipe:1", + ], + { encoding: "buffer", maxBuffer: 1024 * 1024 }, + (error, stdout) => (error ? reject(error) : resolve(stdout)), + ); + child.stdin!.end(poster); + }); + expect(decoded.length).toBe(640 * 360 * 3); + expect(decoded[0]).toBeGreaterThan(220); + expect(decoded[1]).toBeLessThan(30); + expect(decoded[2]).toBeLessThan(30); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, 20000); +}); diff --git a/electron/ipc/recording/shareThumbnail.ts b/electron/ipc/recording/shareThumbnail.ts new file mode 100644 index 000000000..48183fff8 --- /dev/null +++ b/electron/ipc/recording/shareThumbnail.ts @@ -0,0 +1,37 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; + +const run = promisify(execFile); + +/** The caller must validate the local export path before decoding it. */ +export async function createShareThumbnail(filePath: string, signal: AbortSignal): Promise { + const { stdout } = await run( + getFfmpegBinaryPath(), + [ + "-hide_banner", + "-loglevel", + "error", + "-nostdin", + "-threads", + "1", + "-i", + filePath, + "-frames:v", + "1", + "-an", + "-vf", + "scale=640:640:force_original_aspect_ratio=decrease", + "-q:v", + "4", + "-f", + "image2pipe", + "-c:v", + "mjpeg", + "pipe:1", + ], + { encoding: "buffer", maxBuffer: 1024 * 1024, timeout: 15000, windowsHide: true, signal }, + ); + if (!stdout.length) throw new Error("Video has no preview frame"); + return stdout; +} diff --git a/electron/ipc/recording/storagePath.test.ts b/electron/ipc/recording/storagePath.test.ts new file mode 100644 index 000000000..dced4f52e --- /dev/null +++ b/electron/ipc/recording/storagePath.test.ts @@ -0,0 +1,53 @@ +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { resolveRecordedVideoStoragePath } from "./storagePath"; + +describe("resolveRecordedVideoStoragePath", () => { + const recordingsDir = path.resolve("recordings-root"); + + it.each([ + "recording-0.webm", + "recording-1720588800000.mp4", + "recording-1720588800000-webcam.webm", + "recording-1720588800000-webcam.mp4", + ])("accepts an app-generated recording name: %s", (fileName) => { + expect(resolveRecordedVideoStoragePath(recordingsDir, fileName)).toBe( + path.resolve(recordingsDir, fileName), + ); + }); + + it.each([ + "", + "../recording-1.webm", + "..\\recording-1.webm", + "nested/recording-1.webm", + "nested\\recording-1.webm", + "/tmp/recording-1.webm", + "C:\\temp\\recording-1.webm", + "\\\\server\\share\\recording-1.webm", + "recording-1.webm:payload", + "recording-1.webm\n", + "recording-1.webm\0", + "recording--1.webm", + "recording-1.5.webm", + "recording-1.mov", + "other-1.webm", + "recording-1-WEBCAM.webm", + ])("rejects an untrusted recording name: %s", (fileName) => { + expect(() => resolveRecordedVideoStoragePath(recordingsDir, fileName)).toThrow( + "Invalid recording file name", + ); + }); + + it.each([ + { label: "undefined", value: undefined }, + { label: "null", value: null }, + { label: "number", value: 1 }, + { label: "object", value: {} }, + { label: "array", value: [] }, + ])("rejects a non-string recording name: $label", ({ value }) => { + expect(() => resolveRecordedVideoStoragePath(recordingsDir, value)).toThrow( + "Invalid recording file name", + ); + }); +}); diff --git a/electron/ipc/recording/storagePath.ts b/electron/ipc/recording/storagePath.ts new file mode 100644 index 000000000..12c3484f6 --- /dev/null +++ b/electron/ipc/recording/storagePath.ts @@ -0,0 +1,24 @@ +import path from "node:path"; + +const RECORDED_VIDEO_FILE_NAME = /^recording-[0-9]+(?:-webcam)?\.(?:webm|mp4)$/; + +export function resolveRecordedVideoStoragePath(recordingsDir: string, fileName: unknown): string { + if (typeof fileName !== "string" || RECORDED_VIDEO_FILE_NAME.exec(fileName)?.[0] !== fileName) { + throw new Error("Invalid recording file name"); + } + + const resolvedRecordingsDir = path.resolve(recordingsDir); + const candidatePath = path.resolve(resolvedRecordingsDir, fileName); + const relativePath = path.relative(resolvedRecordingsDir, candidatePath); + + if ( + relativePath.length === 0 || + relativePath === ".." || + relativePath.startsWith(`..${path.sep}`) || + path.isAbsolute(relativePath) + ) { + throw new Error("Invalid recording file name"); + } + + return candidatePath; +} diff --git a/electron/ipc/recording/thumbnail.ts b/electron/ipc/recording/thumbnail.ts new file mode 100644 index 000000000..5397b3b37 --- /dev/null +++ b/electron/ipc/recording/thumbnail.ts @@ -0,0 +1,76 @@ +import { createHash } from "node:crypto"; +import { execFile } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { app } from "electron"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; +import { resolveApprovedLocalMediaPath } from "../project/manager"; +import { getRecordingsDir } from "../utils"; + +const run = promisify(execFile); +const pending = new Map>(); +let queue = Promise.resolve(); + +/** Decode one small still, never a video player per library row. */ +export async function getRecordingThumbnail(candidate: string): Promise { + const root = await fs.realpath(await getRecordingsDir()); + const file = await resolveApprovedLocalMediaPath(candidate); + if (!file || path.dirname(file) !== root || !/\.(mp4|mov|webm|mkv|m4v)$/i.test(file)) + throw new Error("Recording is outside the Videos library"); + const stat = await fs.stat(file); + if (!stat.isFile()) throw new Error("Recording is not a file"); + const key = createHash("sha256").update(`${file}:${stat.mtimeMs}:${stat.size}`).digest("hex"); + const existing = pending.get(key); + if (existing) return existing; + const cache = path.join(app.getPath("userData"), "recording-thumbnails", `${key}.jpg`); + const task = queue.then(async () => { + let jpeg: Buffer; + try { + jpeg = await fs.readFile(cache); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + const result = await run( + getFfmpegBinaryPath(), + [ + "-hide_banner", + "-loglevel", + "error", + "-nostdin", + "-threads", + "1", + "-i", + file, + "-frames:v", + "1", + "-an", + "-vf", + "scale=160:90:force_original_aspect_ratio=increase,crop=160:90", + "-q:v", + "5", + "-f", + "image2pipe", + "-c:v", + "mjpeg", + "pipe:1", + ], + { encoding: "buffer", maxBuffer: 512 * 1024, timeout: 15000, windowsHide: true }, + ); + jpeg = result.stdout; + if (!jpeg.length) throw new Error("Recording has no preview frame"); + await fs.mkdir(path.dirname(cache), { recursive: true }); + await fs.writeFile(cache, jpeg); + } + return `data:image/jpeg;base64,${jpeg.toString("base64")}`; + }); + pending.set(key, task); + queue = task.then( + () => undefined, + () => undefined, + ); + try { + return await task; + } finally { + pending.delete(key); + } +} diff --git a/electron/ipc/recording/windows.test.ts b/electron/ipc/recording/windows.test.ts new file mode 100644 index 000000000..0d5613392 --- /dev/null +++ b/electron/ipc/recording/windows.test.ts @@ -0,0 +1,130 @@ +import { EventEmitter } from "node:events"; +import { readFileSync } from "node:fs"; +import { PassThrough } from "node:stream"; +import { fileURLToPath } from "node:url"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { setWindowsCaptureOutputBuffer, setWindowsCaptureTargetPath } from "../state"; +import { waitForWindowsCaptureStop } from "./windows"; + +const windowsCaptureSource = readFileSync( + fileURLToPath(new URL("../../native/wgc-capture/src/wgc_session.cpp", import.meta.url)), + "utf8", +); + +vi.mock("electron", () => ({ + app: { + getPath: () => "C:\\RecordlyTest", + }, + BrowserWindow: { + getAllWindows: () => [], + }, +})); + +class FakeCaptureProcess extends EventEmitter { + stdout = new PassThrough(); + stderr = new PassThrough(); + stdin = new PassThrough(); + killed = false; + + kill = vi.fn(() => { + this.killed = true; + return true; + }); +} + +describe("waitForWindowsCaptureStop", () => { + beforeEach(() => { + setWindowsCaptureOutputBuffer(""); + setWindowsCaptureTargetPath(null); + }); + + it("resolves the helper output path when the process closes cleanly", async () => { + const proc = new FakeCaptureProcess(); + setWindowsCaptureOutputBuffer("Recording stopped. Output path: C:\\Recordly\\capture.mp4"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("close", 0); + + await expect(stopped).resolves.toBe("C:\\Recordly\\capture.mp4"); + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it("resolves the fallback target path when the helper closes cleanly without output path", async () => { + const proc = new FakeCaptureProcess(); + setWindowsCaptureOutputBuffer("Recording stopped without output path"); + setWindowsCaptureTargetPath("C:\\Recordly\\fallback.mp4"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("close", 0); + + await expect(stopped).resolves.toBe("C:\\Recordly\\fallback.mp4"); + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it("rejects with helper output when the helper exits with a non-zero code", async () => { + const proc = new FakeCaptureProcess(); + setWindowsCaptureOutputBuffer("Encoder error: insufficient memory"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("close", 1); + + await expect(stopped).rejects.toThrow("Encoder error: insufficient memory"); + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it("rejects when the helper emits an error", async () => { + const proc = new FakeCaptureProcess(); + const error = new Error("spawn failed"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("error", error); + + await expect(stopped).rejects.toBe(error); + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it("kills the helper and rejects when stop never completes", async () => { + const proc = new FakeCaptureProcess(); + + await expect( + waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 5, + ), + ).rejects.toThrow("Timed out waiting for native Windows capture to stop"); + expect(proc.kill).toHaveBeenCalledTimes(1); + }); +}); + +describe("native Windows window capture", () => { + it("crops monitor frames to the selected window bounds", () => { + expect(windowsCaptureSource).not.toContain("CreateForWindow("); + expect(windowsCaptureSource).toContain("DwmGetWindowAttribute("); + expect(windowsCaptureSource).toContain("CopySubresourceRegion(cropTexture_"); + }); + + it("maps desktop bounds into WGC texture coordinates and keeps the encoder size fixed", () => { + expect(windowsCaptureSource).toContain("normalized * framePoolWidth_"); + expect(windowsCaptureSource).toContain( + "d3dDevice_->CreateTexture2D(&desc, nullptr, &resizedTexture)", + ); + expect(windowsCaptureSource).not.toContain("nextWidth != captureWidth_"); + }); + + it("does not expose desktop pixels when the selected window shrinks", () => { + expect(windowsCaptureSource).toContain("(std::min)(mappedWidth"); + expect(windowsCaptureSource).toContain("ClearRenderTargetView"); + }); +}); diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts new file mode 100644 index 000000000..650cdca98 --- /dev/null +++ b/electron/ipc/recording/windows.ts @@ -0,0 +1,267 @@ +import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import { constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import { BrowserWindow } from "electron"; +import { getWindowsCaptureExePath } from "../paths/binaries"; +import { + selectedSource, + setWindowsCaptureProcess, + setWindowsCaptureStopRequested, + setWindowsNativeCaptureActive, + windowsCaptureOutputBuffer, + windowsCaptureStopRequested, + windowsCaptureTargetPath, + windowsNativeCaptureActive, +} from "../state"; +import { AudioSyncAdjustment } from "../types"; +import { moveFileWithOverwrite } from "../utils"; +import { emitRecordingInterrupted } from "./events"; + +const WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 45_000; + +export type NativeWindowsVideoPaddingResult = { + padded: boolean; + durationSeconds: number; + containerDurationSeconds: number; + targetDurationSeconds: number; + padDurationSeconds: number; +}; + +export type NativeWindowsAudioMuxResult = { + muxed: boolean; + videoDurationSeconds: number; + muxTimeoutMs: number; + audioInputs: string[]; + audio: Record< + string, + { + path: string; + sizeBytes: number; + durationSeconds: number; + startDelayMs: number | null; + adjustment: AudioSyncAdjustment; + } + >; + outputPath?: string; + keptAudioSidecars?: boolean; +}; + +export async function isNativeWindowsCaptureAvailable(): Promise { + if (process.platform !== "win32") return false; + + const os = await import("node:os"); + const [major, , build] = os.release().split(".").map(Number); + const supported = major >= 10 && build >= 19041; + if (!supported) return false; + + try { + await fs.access(getWindowsCaptureExePath(), fsConstants.X_OK); + } catch { + return false; + } + + return true; +} + +export function waitForWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup(); + reject(new Error("Timed out waiting for native Windows capture to start")); + }, 12000); + + let stdoutBuffer = ""; + const onStdout = (chunk: Buffer) => { + stdoutBuffer += chunk.toString(); + if (stdoutBuffer.includes("Recording started")) { + cleanup(); + resolve(); + } + }; + + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + + const onExit = (code: number | null) => { + cleanup(); + reject( + new Error( + windowsCaptureOutputBuffer.trim() || + `Native Windows capture exited before recording started (code ${code ?? "unknown"})`, + ), + ); + }; + + const cleanup = () => { + clearTimeout(timer); + proc.stdout.off("data", onStdout); + proc.off("error", onError); + proc.off("exit", onExit); + }; + + proc.stdout.on("data", onStdout); + proc.once("error", onError); + proc.once("exit", onExit); + }); +} + +export function waitForWindowsCaptureStop( + proc: ChildProcessWithoutNullStreams, + timeoutMs = WINDOWS_CAPTURE_STOP_TIMEOUT_MS, +) { + return new Promise((resolve, reject) => { + let settled = false; + const finish = (callback: () => void) => { + if (settled) return; + settled = true; + cleanup(); + callback(); + }; + + const timer = setTimeout(() => { + finish(() => { + try { + if (!proc.killed) proc.kill(); + } catch { + // The process may already be gone; the caller only needs the timeout error. + } + reject(new Error("Timed out waiting for native Windows capture to stop")); + }); + }, timeoutMs); + + const onClose = (code: number | null) => { + finish(() => { + const match = windowsCaptureOutputBuffer.match( + /Recording stopped\. Output path: (.+)/, + ); + if (match?.[1]) { + resolve(match[1].trim()); + return; + } + if (code === 0 && windowsCaptureTargetPath) { + resolve(windowsCaptureTargetPath); + return; + } + reject( + new Error( + windowsCaptureOutputBuffer.trim() || + `Native Windows capture exited with code ${code ?? "unknown"}`, + ), + ); + }); + }; + + const onError = (error: Error) => { + finish(() => { + reject(error); + }); + }; + + const cleanup = () => { + clearTimeout(timer); + proc.off("close", onClose); + proc.off("error", onError); + }; + + proc.once("close", onClose); + proc.once("error", onError); + }); +} + +export function attachWindowsCaptureLifecycle(proc: ChildProcessWithoutNullStreams) { + proc.once("close", () => { + const wasActive = windowsNativeCaptureActive; + setWindowsCaptureProcess(null); + + if (!wasActive || windowsCaptureStopRequested) { + return; + } + + setWindowsNativeCaptureActive(false); + setWindowsCaptureStopRequested(false); + + const sourceName = selectedSource?.name ?? "Screen"; + BrowserWindow.getAllWindows().forEach((window) => { + if (!window.isDestroyed()) { + window.webContents.send("recording-state-changed", { + recording: false, + sourceName, + }); + } + }); + + emitRecordingInterrupted("capture-stopped", "Recording stopped unexpectedly."); + }); +} + +export async function muxNativeWindowsVideoWithAudio( + videoPath: string, + systemAudioPath: string | null, + micAudioPath: string | null, +): Promise { + const start = Date.now(); + console.log("[PERF:MAIN] muxNativeWindowsVideoWithAudio: STARTED"); + const audio: NativeWindowsAudioMuxResult["audio"] = {}; + const audioInputs: string[] = []; + + const videoPathWithoutExt = videoPath.replace(/\.[^.]+$/u, ""); + + // Optimization: instead of heavy FFmpeg muxing, we just move the audio sidecars + // to their final companion paths so the editor can find them as separate tracks. + if (systemAudioPath) { + const finalSystemPath = `${videoPathWithoutExt}.system.wav`; + try { + const stat = await fs.stat(systemAudioPath); + if (stat.size > 0) { + if (systemAudioPath !== finalSystemPath) { + await moveFileWithOverwrite(systemAudioPath, finalSystemPath); + } + audioInputs.push("system"); + audio.system = { + path: finalSystemPath, + sizeBytes: stat.size, + durationSeconds: 0, + startDelayMs: null, + adjustment: { mode: "none", delayMs: 0, tempoRatio: 1, durationDeltaMs: 0 }, + }; + } + } catch (err) { + console.error(`[mux-win] Failed to handle system audio:`, err); + } + } + + if (micAudioPath) { + const finalMicPath = `${videoPathWithoutExt}.mic.wav`; + try { + const stat = await fs.stat(micAudioPath); + if (stat.size > 0) { + if (micAudioPath !== finalMicPath) { + await moveFileWithOverwrite(micAudioPath, finalMicPath); + } + audioInputs.push("mic"); + audio.mic = { + path: finalMicPath, + sizeBytes: stat.size, + durationSeconds: 0, + startDelayMs: null, + adjustment: { mode: "none", delayMs: 0, tempoRatio: 1, durationDeltaMs: 0 }, + }; + } + } catch (err) { + console.error(`[mux-win] Failed to handle mic audio:`, err); + } + } + + console.log(`[PERF:MAIN] muxNativeWindowsVideoWithAudio: COMPLETED in ${Date.now() - start}ms`); + + return { + muxed: false, + videoDurationSeconds: 0, // No longer needed here + muxTimeoutMs: 0, + audioInputs, + audio, + keptAudioSidecars: true, + }; +} diff --git a/electron/ipc/recording/windowsFallbacks.test.ts b/electron/ipc/recording/windowsFallbacks.test.ts new file mode 100644 index 000000000..512a81bef --- /dev/null +++ b/electron/ipc/recording/windowsFallbacks.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; + +import { + shouldStartWindowsBrowserMicrophoneFallback, + shouldUseWindowsBrowserMicrophoneFallback, + WINDOWS_MIC_CAPTURE_MODE_ENV, +} from "./windowsFallbacks"; + +describe("shouldUseWindowsBrowserMicrophoneFallback", () => { + it("defaults Windows microphone capture to native WASAPI", () => { + expect(shouldStartWindowsBrowserMicrophoneFallback({ capturesMicrophone: true }, {})).toBe( + false, + ); + }); + + it("can be forced before native capture starts", () => { + expect( + shouldStartWindowsBrowserMicrophoneFallback( + { capturesMicrophone: true }, + { [WINDOWS_MIC_CAPTURE_MODE_ENV]: "browser" }, + ), + ).toBe(true); + }); + + it("keeps native WASAPI enabled when explicitly requested", () => { + expect( + shouldStartWindowsBrowserMicrophoneFallback( + { capturesMicrophone: true }, + { [WINDOWS_MIC_CAPTURE_MODE_ENV]: "native" }, + ), + ).toBe(false); + expect( + shouldStartWindowsBrowserMicrophoneFallback( + { capturesMicrophone: true }, + { [WINDOWS_MIC_CAPTURE_MODE_ENV]: "wasapi" }, + ), + ).toBe(false); + }); + + it("uses native WASAPI for unknown mode values", () => { + expect( + shouldStartWindowsBrowserMicrophoneFallback( + { capturesMicrophone: true }, + { [WINDOWS_MIC_CAPTURE_MODE_ENV]: "typo" }, + ), + ).toBe(false); + }); + + it("does not force fallback when microphone capture was not requested", () => { + expect( + shouldStartWindowsBrowserMicrophoneFallback( + { capturesMicrophone: false }, + { [WINDOWS_MIC_CAPTURE_MODE_ENV]: "browser" }, + ), + ).toBe(false); + }); + + it("returns true when native Windows mic initialization fails", () => { + expect( + shouldUseWindowsBrowserMicrophoneFallback( + "WARNING: Failed to initialize WASAPI mic capture\nRecording started", + { capturesMicrophone: true }, + ), + ).toBe(true); + }); + + it("returns true when the native helper reports its stable fallback marker", () => { + expect( + shouldUseWindowsBrowserMicrophoneFallback( + "MICROPHONE_CAPTURE_UNAVAILABLE\nRecording started", + { capturesMicrophone: true }, + ), + ).toBe(true); + }); + + it("returns false when microphone capture was not requested", () => { + expect( + shouldUseWindowsBrowserMicrophoneFallback( + "WARNING: Failed to initialize WASAPI mic capture\nRecording started", + { capturesMicrophone: false }, + ), + ).toBe(false); + }); + + it("returns false for a healthy native mic by default", () => { + expect( + shouldUseWindowsBrowserMicrophoneFallback( + "Recording started", + { capturesMicrophone: true }, + {}, + ), + ).toBe(false); + }); + + it("returns true when browser fallback is forced", () => { + expect( + shouldUseWindowsBrowserMicrophoneFallback( + "Recording started", + { capturesMicrophone: true }, + { [WINDOWS_MIC_CAPTURE_MODE_ENV]: "fallback" }, + ), + ).toBe(true); + }); +}); diff --git a/electron/ipc/recording/windowsFallbacks.ts b/electron/ipc/recording/windowsFallbacks.ts new file mode 100644 index 000000000..f579e0527 --- /dev/null +++ b/electron/ipc/recording/windowsFallbacks.ts @@ -0,0 +1,33 @@ +const WINDOWS_MIC_CAPTURE_UNAVAILABLE_MARKERS = [ + "MICROPHONE_CAPTURE_UNAVAILABLE", + "WARNING: Failed to initialize WASAPI mic capture", +]; +export const WINDOWS_MIC_CAPTURE_MODE_ENV = "RECORDLY_WINDOWS_MIC_CAPTURE"; + +export function shouldStartWindowsBrowserMicrophoneFallback( + options?: { capturesMicrophone?: boolean }, + env: NodeJS.ProcessEnv = process.env, +) { + if (!options?.capturesMicrophone) { + return false; + } + + const mode = env[WINDOWS_MIC_CAPTURE_MODE_ENV]?.trim().toLowerCase(); + // Native WASAPI is the normal Windows path. Keep the renderer path as an + // explicit escape hatch and as an automatic fallback when WASAPI cannot start. + return mode === "browser" || mode === "fallback" || mode === "renderer"; +} + +export function shouldUseWindowsBrowserMicrophoneFallback( + captureOutput: string, + options?: { capturesMicrophone?: boolean }, + env: NodeJS.ProcessEnv = process.env, +) { + return ( + Boolean(options?.capturesMicrophone) && + (shouldStartWindowsBrowserMicrophoneFallback(options, env) || + WINDOWS_MIC_CAPTURE_UNAVAILABLE_MARKERS.some((marker) => + captureOutput.includes(marker), + )) + ); +} diff --git a/electron/ipc/register/announcements.ts b/electron/ipc/register/announcements.ts new file mode 100644 index 000000000..c5791e99f --- /dev/null +++ b/electron/ipc/register/announcements.ts @@ -0,0 +1,91 @@ +import { ipcMain } from "electron"; + +const DEFAULT_ANNOUNCEMENT_FEED_URL = + "https://raw.githubusercontent.com/webadderallorg/Recordly/main/announcements.json"; +const ANNOUNCEMENT_FETCH_TIMEOUT_MS = 5_000; +const ANNOUNCEMENT_CACHE_TTL_MS = 6 * 60 * 60 * 1_000; +const MAX_ANNOUNCEMENT_FEED_BYTES = 1_000_000; + +let cachedFeed: unknown = null; +let cachedAt = 0; +let hasCachedResult = false; +let pendingFetch: Promise | null = null; + +function getAnnouncementFeedUrl(): string | null { + const configuredUrl = process.env.RECORDLY_ANNOUNCEMENTS_URL?.trim(); + if (configuredUrl?.toLowerCase() === "off") { + return null; + } + + const candidate = configuredUrl || DEFAULT_ANNOUNCEMENT_FEED_URL; + try { + const parsed = new URL(candidate); + return parsed.protocol === "https:" && !parsed.username && !parsed.password + ? parsed.href + : null; + } catch { + return null; + } +} + +async function requestAnnouncementFeed(feedUrl: string): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), ANNOUNCEMENT_FETCH_TIMEOUT_MS); + + try { + const response = await fetch(feedUrl, { + signal: controller.signal, + headers: { Accept: "application/json" }, + redirect: "follow", + }); + if (!response.ok) { + throw new Error(`Announcement feed returned HTTP ${response.status}`); + } + if (new URL(response.url).protocol !== "https:") { + throw new Error("Announcement feed redirected to an unsafe URL"); + } + + const declaredLength = Number(response.headers.get("content-length")); + if (Number.isFinite(declaredLength) && declaredLength > MAX_ANNOUNCEMENT_FEED_BYTES) { + throw new Error("Announcement feed is too large"); + } + + const text = await response.text(); + if (Buffer.byteLength(text, "utf8") > MAX_ANNOUNCEMENT_FEED_BYTES) { + throw new Error("Announcement feed is too large"); + } + + cachedFeed = JSON.parse(text) as unknown; + cachedAt = Date.now(); + hasCachedResult = true; + return cachedFeed; + } catch (error) { + console.warn("Failed to load announcement feed:", error); + return cachedFeed; + } finally { + clearTimeout(timeout); + } +} + +function fetchAnnouncementFeed(): Promise { + const feedUrl = getAnnouncementFeedUrl(); + if (!feedUrl) { + return Promise.resolve(null); + } + + if (hasCachedResult && Date.now() - cachedAt < ANNOUNCEMENT_CACHE_TTL_MS) { + return Promise.resolve(cachedFeed); + } + + if (!pendingFetch) { + pendingFetch = requestAnnouncementFeed(feedUrl).finally(() => { + pendingFetch = null; + }); + } + + return pendingFetch; +} + +export function registerAnnouncementHandlers() { + ipcMain.handle("announcements:get", fetchAnnouncementFeed); +} diff --git a/electron/ipc/register/assets.ts b/electron/ipc/register/assets.ts new file mode 100644 index 000000000..e871cc170 --- /dev/null +++ b/electron/ipc/register/assets.ts @@ -0,0 +1,151 @@ +import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { app, ipcMain } from "electron"; +import { USER_DATA_PATH } from "../../appPaths"; +import { getAssetRootPath } from "../project/manager"; +import { normalizePath } from "../utils"; + +export function registerAssetHandlers() { + async function resolveReadableLocalFilePath(filePath: string) { + const normalizedPath = normalizePath(filePath); + const resolvedPath = await fs.realpath(normalizedPath).catch(() => normalizedPath); + const stats = await fs.stat(resolvedPath); + if (!stats.isFile()) { + throw new Error("Path is not a readable file"); + } + return normalizePath(resolvedPath); + } + + // Generate a tiny thumbnail for a wallpaper image and cache it in userData. + // Returns the cached thumbnail as raw JPEG bytes for fast grid rendering. + // Serialized to prevent concurrent nativeImage operations from eating memory. + const THUMB_SIZE = 96; + const thumbCacheDir = path.join(USER_DATA_PATH, "wallpaper-thumbs"); + let thumbGenerationQueue: Promise = Promise.resolve(); + + ipcMain.handle("generate-wallpaper-thumbnail", async (_, filePath: string) => { + try { + const bundled = filePath.startsWith("/wallpapers/"); + const wallpaperRoot = path.resolve(getAssetRootPath(), "wallpapers"); + const candidate = bundled + ? path.resolve( + wallpaperRoot, + decodeURIComponent(filePath.slice("/wallpapers/".length)), + ) + : filePath; + if ( + bundled && + (path.relative(wallpaperRoot, candidate).startsWith("..") || + path.isAbsolute(path.relative(wallpaperRoot, candidate))) + ) { + throw new Error("Wallpaper path is outside the bundled wallpapers"); + } + const resolved = await resolveReadableLocalFilePath(candidate); + + // Deterministic cache key from file path + mtime + const stat = await fs.stat(resolved); + const cacheKey = createHash("sha256") + .update(`${resolved}:${stat.mtimeMs}`) + .digest("hex"); + const thumbPath = path.join(thumbCacheDir, `${cacheKey}.jpg`); + + // Return cached thumbnail if it exists (no queue needed) + if (existsSync(thumbPath)) { + const data = await fs.readFile(thumbPath); + return { success: true, data }; + } + + // Serialize nativeImage operations to avoid OOM from concurrent full-res decodes + let jpegData: Buffer; + const generation = thumbGenerationQueue.then(async () => { + const { nativeImage } = await import("electron"); + const img = nativeImage.createFromPath(resolved); + if (img.isEmpty()) { + throw new Error("Failed to load image"); + } + const { width, height } = img.getSize(); + const scale = THUMB_SIZE / Math.min(width, height); + const resized = img.resize({ + width: Math.round(width * scale), + height: Math.round(height * scale), + quality: "good", + }); + jpegData = resized.toJPEG(70); + + // Cache to disk + await fs.mkdir(thumbCacheDir, { recursive: true }); + await fs.writeFile(thumbPath, jpegData); + }); + // Keep the queue moving even if one fails + thumbGenerationQueue = generation.catch(() => undefined); + await generation; + + return { success: true, data: jpegData! }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + + // Return base path for assets so renderer can resolve file:// paths in production + ipcMain.handle("get-asset-base-path", () => { + if (!app.isPackaged) { + return null; + } + + try { + const assetPath = getAssetRootPath(); + return pathToFileURL(`${assetPath}${path.sep}`).toString(); + } catch (err) { + console.error("Failed to resolve asset base path:", err); + return null; + } + }); + + ipcMain.handle("list-asset-directory", async (_, relativeDir: string) => { + try { + const normalizedRelativeDir = String(relativeDir ?? "") + .replace(/\\/g, "/") + .replace(/^\/+/, ""); + + const assetRootPath = path.resolve(getAssetRootPath()); + const targetDirPath = path.resolve(assetRootPath, normalizedRelativeDir); + if ( + targetDirPath !== assetRootPath && + !targetDirPath.startsWith(`${assetRootPath}${path.sep}`) + ) { + return { success: false, error: "Invalid asset directory" }; + } + + const entries = await fs.readdir(targetDirPath, { withFileTypes: true }); + const files = entries + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .sort(new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare); + + return { success: true, files }; + } catch (error) { + console.error("Failed to list asset directory:", error); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("read-local-file", async (_, filePath: string) => { + try { + // Intentionally more permissive than the media-server allowlist: this IPC + // is used for direct renderer-side local file reads after the app has + // already accepted a path, while URL-based media serving must stay scoped + // to approved/app-managed locations. We still canonicalize the path and + // require a real on-disk file so this cannot be used to read directories. + const resolved = await resolveReadableLocalFilePath(filePath); + + const data = await fs.readFile(resolved); + return { success: true, data }; + } catch (error) { + console.error("Failed to read local file:", error); + return { success: false, error: String(error) }; + } + }); +} diff --git a/electron/ipc/register/captions.ts b/electron/ipc/register/captions.ts new file mode 100644 index 000000000..7dfe5d671 --- /dev/null +++ b/electron/ipc/register/captions.ts @@ -0,0 +1,256 @@ +import path from "node:path"; +import { dialog, ipcMain } from "electron"; +import { generateAutoCaptionsFromVideo } from "../captions/generate"; +import { + deleteWhisperSmallModel, + downloadWhisperSmallModel, + getWhisperSmallModelStatus, + sendWhisperModelDownloadProgress, +} from "../captions/whisper"; +import { LEGACY_PROJECT_FILE_EXTENSIONS, PROJECT_FILE_EXTENSION } from "../constants"; +import { hasProjectFileExtension, loadProjectFromPath } from "../project/manager"; +import { setCurrentProjectPath } from "../state"; +import { approveUserPath, getRecordingsDir } from "../utils"; + +const VIDEO_FILE_EXTENSIONS = ["webm", "mp4", "mov", "avi", "mkv"]; +const PROJECT_FILE_EXTENSIONS = [PROJECT_FILE_EXTENSION, ...LEGACY_PROJECT_FILE_EXTENSIONS]; + +type OpenVideoFilePickerOptions = { + includeProjects?: boolean; +}; + +export function registerCaptionHandlers() { + ipcMain.handle("open-video-file-picker", async (_, options?: OpenVideoFilePickerOptions) => { + try { + const includeProjects = Boolean(options?.includeProjects); + const recordingsDir = await getRecordingsDir(); + const result = await dialog.showOpenDialog({ + title: includeProjects ? "Import Media or Recordly Project" : "Select Video File", + defaultPath: recordingsDir, + filters: [ + ...(includeProjects + ? [ + { + name: "Media or Recordly Projects", + extensions: [ + ...VIDEO_FILE_EXTENSIONS, + ...PROJECT_FILE_EXTENSIONS, + ], + }, + ] + : []), + { name: "Video Files", extensions: VIDEO_FILE_EXTENSIONS }, + ...(includeProjects + ? [{ name: "Recordly Projects", extensions: PROJECT_FILE_EXTENSIONS }] + : []), + { name: "All Files", extensions: ["*"] }, + ], + properties: ["openFile"], + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true }; + } + + const selectedPath = result.filePaths[0]; + + if (includeProjects && hasProjectFileExtension(selectedPath)) { + const projectResult = await loadProjectFromPath(selectedPath); + return projectResult.success + ? { ...projectResult, kind: "project" } + : projectResult; + } + + approveUserPath(selectedPath); + setCurrentProjectPath(null); + return { + success: true, + kind: "media", + path: selectedPath, + extension: path.extname(selectedPath).replace(/^\./, "").toLowerCase(), + }; + } catch (error) { + console.error("Failed to open file picker:", error); + return { + success: false, + message: "Failed to open file picker", + error: String(error), + }; + } + }); + + ipcMain.handle("open-audio-file-picker", async () => { + try { + const result = await dialog.showOpenDialog({ + title: "Select Audio File", + filters: [ + { + name: "Audio Files", + extensions: ["mp3", "wav", "aac", "m4a", "flac", "ogg"], + }, + { name: "All Files", extensions: ["*"] }, + ], + properties: ["openFile"], + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true }; + } + + approveUserPath(result.filePaths[0]); + return { + success: true, + path: result.filePaths[0], + }; + } catch (error) { + console.error("Failed to open audio file picker:", error); + return { + success: false, + message: "Failed to open audio file picker", + error: String(error), + }; + } + }); + + ipcMain.handle("open-whisper-executable-picker", async () => { + try { + const result = await dialog.showOpenDialog({ + title: "Select Whisper Executable", + filters: [ + { + name: "Executables", + extensions: process.platform === "win32" ? ["exe", "cmd", "bat"] : ["*"], + }, + { name: "All Files", extensions: ["*"] }, + ], + properties: ["openFile"], + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true }; + } + + approveUserPath(result.filePaths[0]); + return { success: true, path: result.filePaths[0] }; + } catch (error) { + console.error("Failed to open Whisper executable picker:", error); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("open-whisper-model-picker", async () => { + try { + const result = await dialog.showOpenDialog({ + title: "Select Whisper Model", + filters: [ + { name: "Whisper Models", extensions: ["bin"] }, + { name: "All Files", extensions: ["*"] }, + ], + properties: ["openFile"], + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true }; + } + + approveUserPath(result.filePaths[0]); + return { success: true, path: result.filePaths[0] }; + } catch (error) { + console.error("Failed to open Whisper model picker:", error); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("get-whisper-small-model-status", async () => { + try { + return await getWhisperSmallModelStatus(); + } catch (error) { + return { success: false, exists: false, path: null, error: String(error) }; + } + }); + + ipcMain.handle("download-whisper-small-model", async (event) => { + try { + const existing = await getWhisperSmallModelStatus(); + if (existing.exists) { + sendWhisperModelDownloadProgress(event.sender, { + status: "downloaded", + progress: 100, + path: existing.path, + }); + return { success: true, path: existing.path, alreadyDownloaded: true }; + } + + const modelPath = await downloadWhisperSmallModel(event.sender); + return { success: true, path: modelPath }; + } catch (error) { + console.error("Failed to download Whisper small model:", error); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("delete-whisper-small-model", async (event) => { + try { + await deleteWhisperSmallModel(); + sendWhisperModelDownloadProgress(event.sender, { + status: "idle", + progress: 0, + path: null, + }); + return { success: true }; + } catch (error) { + console.error("Failed to delete Whisper small model:", error); + // Verify whether the file was actually removed despite the error + const status = await getWhisperSmallModelStatus(); + if (!status.exists) { + // File is gone — treat as success + sendWhisperModelDownloadProgress(event.sender, { + status: "idle", + progress: 0, + path: null, + }); + return { success: true }; + } + sendWhisperModelDownloadProgress(event.sender, { + status: "error", + progress: 0, + path: null, + error: String(error), + }); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle( + "generate-auto-captions", + async ( + _, + options: { + videoPath: string; + whisperExecutablePath: string; + whisperModelPath: string; + language?: string; + }, + ) => { + try { + const result = await generateAutoCaptionsFromVideo(options); + return { + success: true, + cues: result.cues, + message: + result.audioSourceLabel === "recording" + ? `Generated ${result.cues.length} caption cues.` + : `Generated ${result.cues.length} caption cues from the ${result.audioSourceLabel}.`, + }; + } catch (error) { + console.error("Failed to generate auto captions:", error); + return { + success: false, + error: String(error), + message: + error instanceof Error ? error.message : "Failed to generate auto captions", + }; + } + }, + ); +} diff --git a/electron/ipc/register/capturePicker.test.ts b/electron/ipc/register/capturePicker.test.ts new file mode 100644 index 000000000..6a12558a0 --- /dev/null +++ b/electron/ipc/register/capturePicker.test.ts @@ -0,0 +1,221 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +const mocks = vi.hoisted(() => ({ + handlers: new Map unknown>(), + list: vi.fn(), + pickSourceList: vi.fn(), + validate: vi.fn(), + + selected: vi.fn(), + recording: false, + currentSource: null as Record | null, + create: vi.fn(), + close: vi.fn(), + overlay: {}, + outsider: {}, + windows: [] as object[], +})); +vi.mock("electron", () => ({ + ipcMain: { + handle: (name: string, handler: (...args: unknown[]) => unknown) => + mocks.handlers.set(name, handler), + }, +})); +vi.mock("../../windows", () => ({ + createCapturePickerWindows: mocks.create, + closeCapturePickerWindows: mocks.close, + isCapturePickerWebContents: (sender: object) => mocks.windows.includes(sender), +})); +vi.mock("../captureSelection", () => ({ + validateCaptureSource: mocks.validate, +})); +vi.mock("../capturePickerSources", () => ({ listCapturePickerWindows: mocks.list })); +vi.mock("../cursor/bounds", () => ({ stopWindowBoundsCapture: vi.fn() })); +vi.mock("../state", () => ({ + setSelectedSource: mocks.selected, + get isCursorCaptureActive() { + return mocks.recording; + }, + get selectedSource() { + return mocks.currentSource; + }, +})); +vi.mock("../utils", () => ({ + getScreen: () => ({ + getAllDisplays: () => [{ id: 1, bounds: { x: 0, y: 0, width: 1440, height: 1000 } }], + getPrimaryDisplay: () => ({ id: 1 }), + getCursorScreenPoint: () => ({ x: 0, y: 0 }), + }), +})); +vi.mock("./sources", () => ({ + bringSelectedWindowForward: vi.fn(), + broadcastSelectedSourceChange: vi.fn(), + getDesktopSources: vi.fn().mockResolvedValue([{ id: "screen:1:0", display_id: "1" }]), +})); +import { registerCapturePickerHandlers } from "./capturePicker"; +const platform = process.platform; +afterEach(() => { + Object.defineProperty(process, "platform", { value: platform }); + vi.unstubAllEnvs(); +}); +beforeEach(() => { + vi.clearAllMocks(); + mocks.handlers.clear(); + mocks.currentSource = null; + mocks.recording = false; + mocks.windows = [mocks.overlay]; + Object.defineProperty(process, "platform", { value: "darwin" }); + mocks.list.mockResolvedValue([]); + mocks.validate.mockImplementation(async (source) => source); + registerCapturePickerHandlers({ pickSourceList: mocks.pickSourceList }); +}); +const call = (name: string, sender: object, input?: unknown) => + mocks.handlers.get(name)!({ sender }, input); +describe("selection-only picker IPC", () => { + it("ignores completion from other windows and forces selection-only completion", async () => { + const pending = call("pick-capture-target", mocks.outsider); + await vi.waitFor(() => expect(mocks.create).toHaveBeenCalledOnce()); + await call("complete-capture-pick", mocks.outsider, { + kind: "screen", + displayId: 1, + record: true, + }); + expect(mocks.selected).not.toHaveBeenCalled(); + await call("complete-capture-pick", mocks.overlay, { + kind: "screen", + displayId: 1, + record: true, + }); + expect(await pending).toMatchObject({ success: true, source: { id: "screen:1:0" } }); + expect(await pending).not.toHaveProperty("record"); + }); + it("coalesces simultaneous opens into one picker", async () => { + const first = call("pick-capture-target", mocks.outsider); + const second = call("pick-capture-target", mocks.outsider); + await vi.waitFor(() => expect(mocks.create).toHaveBeenCalledOnce()); + await call("complete-capture-pick", mocks.overlay, null); + expect(await first).toEqual({ success: false, canceled: true }); + expect(await second).toEqual({ success: false, canceled: true }); + }); + it("keeps Wayland on the existing portal path without overlays or geometry probes", async () => { + Object.defineProperty(process, "platform", { value: "linux" }); + vi.stubEnv("XDG_SESSION_TYPE", "wayland"); + expect(await call("pick-capture-target", mocks.outsider)).toMatchObject({ + success: true, + source: { id: "screen:linux-portal" }, + }); + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.list).not.toHaveBeenCalled(); + }); +}); + +it("rejects a window that disappeared while the picker was open", async () => { + mocks.list.mockResolvedValue([ + { + id: "window:101:0", + title: "Document", + appName: "Notes", + x: 0, + y: 0, + width: 400, + height: 300, + }, + ]); + mocks.validate.mockResolvedValue(null); + const pending = call("pick-capture-target", mocks.outsider); + await vi.waitFor(() => expect(mocks.create).toHaveBeenCalledOnce()); + await call("complete-capture-pick", mocks.overlay, { + kind: "window", + windowId: "window:101:0", + displayId: 1, + }); + expect(await pending).toMatchObject({ + success: false, + message: "That window or screen is no longer available.", + }); + expect(mocks.selected).not.toHaveBeenCalled(); +}); +it("surfaces detection failures instead of opening a screen-only overlay", async () => { + mocks.list.mockRejectedValue(new Error("Unable to detect windows")); + expect(await call("pick-capture-target", mocks.outsider)).toMatchObject({ + success: false, + message: "Unable to detect windows", + }); + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.selected).not.toHaveBeenCalled(); +}); + +it("opens a fresh picker even when an area is already selected", async () => { + mocks.currentSource = { + id: "area:1", + display_id: "1", + area: { x: 100, y: 200, width: 400, height: 300 }, + }; + expect(await call("get-capture-picker-context", mocks.overlay, 1)).toMatchObject({ + lastArea: null, + }); + mocks.currentSource = { id: "window:101:0", display_id: "1" }; + expect(await call("get-capture-picker-context", mocks.overlay, 1)).toMatchObject({ + lastArea: null, + }); +}); + +it("does not open any source picker during recording or pause", async () => { + mocks.recording = true; + expect(await call("pick-capture-target", mocks.outsider)).toEqual({ + success: false, + canceled: true, + }); + expect(mocks.list).not.toHaveBeenCalled(); + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.selected).not.toHaveBeenCalled(); +}); + +it("rejects a pending selection if recording starts while the picker is open", async () => { + const pending = call("pick-capture-target", mocks.outsider); + await vi.waitFor(() => expect(mocks.create).toHaveBeenCalledOnce()); + mocks.recording = true; + await call("complete-capture-pick", mocks.overlay, { kind: "screen", displayId: 1 }); + expect(await pending).toEqual({ success: false, canceled: true }); + expect(mocks.selected).not.toHaveBeenCalled(); +}); + +it("does not open overlays if recording starts while listing windows", async () => { + mocks.list.mockImplementation(async () => { + mocks.recording = true; + return []; + }); + expect(await call("pick-capture-target", mocks.outsider)).toEqual({ + success: false, + canceled: true, + }); + expect(mocks.create).not.toHaveBeenCalled(); +}); + +it("keeps X11 on the source list and coalesces concurrent opens", async () => { + Object.defineProperty(process, "platform", { value: "linux" }); + vi.stubEnv("XDG_SESSION_TYPE", "x11"); + let finish!: (result: unknown) => void; + mocks.pickSourceList.mockReturnValue( + new Promise((resolve) => { + finish = resolve; + }), + ); + const first = call("pick-capture-target", mocks.outsider); + const second = call("pick-capture-target", mocks.outsider); + expect(mocks.pickSourceList).toHaveBeenCalledOnce(); + expect(mocks.list).not.toHaveBeenCalled(); + expect(mocks.create).not.toHaveBeenCalled(); + finish({ success: false, canceled: true }); + expect(await first).toEqual({ success: false, canceled: true }); + expect(await second).toEqual({ success: false, canceled: true }); +}); +it("keeps the X11 source list closed during recording", async () => { + Object.defineProperty(process, "platform", { value: "linux" }); + vi.stubEnv("XDG_SESSION_TYPE", "x11"); + mocks.recording = true; + expect(await call("pick-capture-target", mocks.outsider)).toEqual({ + success: false, + canceled: true, + }); + expect(mocks.pickSourceList).not.toHaveBeenCalled(); +}); diff --git a/electron/ipc/register/capturePicker.ts b/electron/ipc/register/capturePicker.ts new file mode 100644 index 000000000..5f471b03c --- /dev/null +++ b/electron/ipc/register/capturePicker.ts @@ -0,0 +1,199 @@ +import { validateCaptureSource } from "../captureSelection"; +import { listCapturePickerWindows } from "../capturePickerSources"; +import { ipcMain } from "electron"; +import type { SourceListPickResult } from "../../sourceListPicker"; +import { + closeCapturePickerWindows, + createCapturePickerWindows, + isCapturePickerWebContents, +} from "../../windows"; +import { stopWindowBoundsCapture } from "../cursor/bounds"; +import { isCursorCaptureActive, setSelectedSource } from "../state"; +import { createAreaSource, createWindowSource, normalizeCapturePick } from "../sourceArea"; +import type { CapturePick, CapturePickerWindow, SelectedSource } from "../types"; +import { getScreen } from "../utils"; +import { getScreenSourceIdForDisplay, isLikelyLinuxWaylandSession } from "./sourceMapping"; +import { broadcastSelectedSourceChange, getDesktopSources } from "./sources"; + +/** The same source the screen list offers for this display. */ +async function createScreenSource(displayId: number): Promise { + const displays = getScreen().getAllDisplays(); + const index = displays.findIndex((display) => display.id === displayId); + if (index < 0) return null; + const sources = await getDesktopSources( + { + types: ["screen"], + thumbnailSize: { width: 0, height: 0 }, + }, + { strict: true }, + ); + const matched = + sources.find((source) => source.display_id === String(displayId)) ?? + (sources.length === displays.length ? sources[index] : undefined); + const isPrimary = getScreen().getPrimaryDisplay().id === displayId; + return { + id: getScreenSourceIdForDisplay({ + displayId: String(displayId), + platform: process.platform, + matchedSourceId: matched?.id, + }), + name: isPrimary ? `Screen ${index + 1} (Primary)` : `Screen ${index + 1}`, + display_id: String(displayId), + sourceType: "screen", + }; +} + +/** + * The capture picker covers every display: hovering highlights the window under + * the cursor, a click picks that window or the whole screen, and a drag draws an + * area. The pick becomes the selected source. + */ +export function registerCapturePickerHandlers({ + pickSourceList, +}: { + pickSourceList: () => Promise; +}) { + let resolvePendingPick: ((pick: CapturePick | null) => void) | null = null; + let pickableWindows: Promise = Promise.resolve([]); + + const finishPick = (pick: CapturePick | null) => { + const resolve = resolvePendingPick; + resolvePendingPick = null; + resolve?.(pick); + }; + + let activePick: Promise | null = null; + const pickTarget = async () => { + if (isCursorCaptureActive) return { success: false, canceled: true }; + if (process.platform === "linux" && isLikelyLinuxWaylandSession(process.env)) { + // Preserve the existing portal recording path: its system picker opens + // when recording starts; overlay selection cannot identify Wayland windows. + const source = { + id: "screen:linux-portal", + name: "System picker", + sourceType: "screen" as const, + }; + setSelectedSource(source); + broadcastSelectedSourceChange(); + return { + success: true, + source, + message: "Choose a screen or window in the system picker when you start recording.", + }; + } + + if (process.platform === "linux") return pickSourceList(); + + finishPick(null); + // List windows before the overlays cover them. + pickableWindows = Promise.resolve(await listCapturePickerWindows()); + if (isCursorCaptureActive) return { success: false, canceled: true }; + const pick = await new Promise((resolve) => { + resolvePendingPick = resolve; + // Closing an overlay without confirming cancels the pick. + createCapturePickerWindows(() => finishPick(null)); + }); + closeCapturePickerWindows(); + if (!pick || isCursorCaptureActive) { + return { success: false, canceled: true }; + } + + let source: SelectedSource | null; + if (pick.kind === "area") { + source = createAreaSource(pick); + } else if (pick.kind === "window") { + const window = (await pickableWindows).find((entry) => entry.id === pick.windowId); + source = window ? createWindowSource(window) : null; + } else { + source = await createScreenSource(pick.displayId); + } + if (source) source = await validateCaptureSource(source); + if (!source) { + return { success: false, message: "That window or screen is no longer available." }; + } + + if (isCursorCaptureActive) return { success: false, canceled: true }; + // Recording brings the selected window forward; selection need not wait for it. + setSelectedSource(source); + broadcastSelectedSourceChange(); + stopWindowBoundsCapture(); + return { success: true, source }; + }; + ipcMain.handle("pick-capture-target", () => { + if (isCursorCaptureActive) return { success: false, canceled: true }; + if (!activePick) { + activePick = pickTarget() + .catch((error) => { + finishPick(null); + closeCapturePickerWindows(); + return { + success: false, + message: + error instanceof Error + ? error.message + : "Unable to open the source picker.", + }; + }) + .finally(() => { + activePick = null; + }); + } + return activePick; + }); + + ipcMain.handle("complete-capture-pick", async (event, input: unknown) => { + if (!isCapturePickerWebContents(event.sender)) return; + if ( + input && + typeof input === "object" && + (input as { kind?: unknown }).kind === "area" && + process.platform !== "darwin" + ) + return; + if (!input || isCursorCaptureActive) { + finishPick(null); + return; + } + const displays = getScreen() + .getAllDisplays() + .map((display) => ({ id: display.id, bounds: display.bounds })); + finishPick( + normalizeCapturePick( + input && typeof input === "object" ? { ...input, record: false } : input, + displays, + await pickableWindows, + ), + ); + }); + + ipcMain.handle("get-capture-picker-context", async (event, displayId: unknown) => { + if (!isCapturePickerWebContents(event.sender)) throw new Error("Unknown picker window."); + const display = getScreen() + .getAllDisplays() + .find((entry) => entry.id === Number(displayId)); + if (!display) { + return { + displayBounds: null, + lastArea: null, + allowArea: process.platform === "darwin", + windows: [], + cursor: null, + }; + } + const { x, y, width, height } = display.bounds; + const windows = (await pickableWindows).filter( + (window) => + window.x < x + width && + window.x + window.width > x && + window.y < y + height && + window.y + window.height > y, + ); + return { + displayBounds: display.bounds, + allowArea: process.platform === "darwin", + lastArea: null, + windows, + cursor: getScreen().getCursorScreenPoint(), + }; + }); +} diff --git a/electron/ipc/register/cloudShare.test.ts b/electron/ipc/register/cloudShare.test.ts new file mode 100644 index 000000000..3346e9b44 --- /dev/null +++ b/electron/ipc/register/cloudShare.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, it } from "vitest"; +import { + normalizeCloudEndpoint, + parseCloudShareTicket, + parseCloudRecordingList, +} from "../cloudShareContract"; + +describe("cloud share contract", () => { + it("accepts only Recordly production, isolated staging, and local development endpoints", () => { + expect( + normalizeCloudEndpoint( + "https://recordly-share-staging.youngchen3442.workers.dev/api/upload", + ), + ).toBe("https://recordly-share-staging.youngchen3442.workers.dev/api/upload"); + expect(normalizeCloudEndpoint("https://videos.recordly.dev/api/upload#ignored")).toBe( + "https://videos.recordly.dev/api/upload", + ); + expect(normalizeCloudEndpoint("http://localhost:8787/api/upload")).toBe( + "http://localhost:8787/api/upload", + ); + }); + + it("normalizes a Voom upload ticket and derives its metadata endpoint", () => { + expect( + parseCloudShareTicket({ + shareCode: "abc123", + uploadURL: "http://localhost:8787/api/upload-data/abc123", + shareURL: "http://localhost:8787/s/abc123", + }), + ).toEqual({ + uploadUrl: "http://localhost:8787/api/upload-data/abc123", + shareUrl: "http://localhost:8787/s/abc123", + shareCode: "abc123", + method: "PUT", + headers: {}, + finalizeUrl: "http://localhost:8787/api/metadata/abc123", + }); + }); + + it("rejects insecure and untrusted remote endpoints", () => { + expect(() => + normalizeCloudEndpoint("https://other.youngchen3442.workers.dev/api/upload"), + ).toThrow("only allowed through the Recordly service"); + expect(() => + normalizeCloudEndpoint( + "https://recordly-share-staging.youngchen3442.workers.dev.attacker.example/api/upload", + ), + ).toThrow("only allowed through the Recordly service"); + expect(() => normalizeCloudEndpoint("https://untrusted.example.test/api/upload")).toThrow( + "only allowed through the Recordly service", + ); + expect(() => + normalizeCloudEndpoint("https://videos.recordly.dev.attacker.example/api/upload"), + ).toThrow("only allowed through the Recordly service"); + expect(() => normalizeCloudEndpoint("http://share.example.com/api/shares")).toThrow( + "only allowed through the Recordly service", + ); + expect(() => normalizeCloudEndpoint("https://share.example.com/api/upload")).toThrow( + "only allowed through the Recordly service", + ); + expect(() => normalizeCloudEndpoint("https://videos.recordly.dev/api/other")).toThrow( + "endpoint is invalid", + ); + }); + + it("normalizes a presigned PUT ticket", () => { + expect( + parseCloudShareTicket({ + uploadUrl: "https://storage.example.com/upload", + shareUrl: "https://share.example.com/s/123", + headers: { "x-test": "value" }, + }), + ).toEqual({ + uploadUrl: "https://storage.example.com/upload", + shareUrl: "https://share.example.com/s/123", + shareCode: undefined, + method: "PUT", + headers: { "x-test": "value" }, + finalizeUrl: undefined, + }); + }); + + it("rejects unsupported methods and malformed headers", () => { + expect(() => + parseCloudShareTicket({ + uploadUrl: "https://storage.example.com/upload", + shareUrl: "https://share.example.com/s/123", + method: "POST", + }), + ).toThrow("presigned PUT"); + expect(() => + parseCloudShareTicket({ + uploadUrl: "https://storage.example.com/upload", + shareUrl: "https://share.example.com/s/123", + headers: { invalid: 42 }, + }), + ).toThrow("must be strings"); + expect(() => + parseCloudShareTicket({ + uploadUrl: "https://storage.example.com/upload", + shareUrl: "https://share.example.com/s/123", + headers: { authorization: "Bearer exfiltrate-me" }, + }), + ).toThrow("not allowed"); + }); + + it("rejects an insecure remote upload URL", () => { + expect(() => + parseCloudShareTicket({ + uploadUrl: "http://storage.example.com/upload", + shareUrl: "https://share.example.com/s/123", + }), + ).toThrow("uploadUrl must use HTTPS"); + }); +}); + +describe("cloud recording library responses", () => { + it.each([ + null, + false, + "invalid", + [], + {}, + { videos: null }, + ])("rejects malformed lists (%j)", (data) => { + expect(() => + parseCloudRecordingList(data, "https://videos.recordly.dev/api/upload"), + ).toThrow("Invalid recordings response."); + }); + it.each([ + null, + false, + "invalid", + [], + {}, + { share_code: "abc", title: 1 }, + ])("rejects malformed rows (%j)", (row) => { + expect(() => + parseCloudRecordingList({ videos: [row] }, "https://videos.recordly.dev/api/upload"), + ).toThrow("Invalid recording response."); + }); + it("uses the validated endpoint for links and normalizes readiness and size", () => { + expect( + parseCloudRecordingList( + { + videos: [ + { + share_code: "abc123", + title: "Fixture", + file_size: 8, + upload_completed: 1, + created_at: "2026-01-01", + }, + ], + }, + "https://videos.recordly.dev/api/upload", + ), + ).toEqual([ + { + code: "abc123", + title: "Fixture", + size: 8, + ready: true, + createdAt: "2026-01-01", + url: "https://videos.recordly.dev/s/abc123", + }, + ]); + }); +}); diff --git a/electron/ipc/register/cloudShare.ts b/electron/ipc/register/cloudShare.ts new file mode 100644 index 000000000..cb9ed7ea4 --- /dev/null +++ b/electron/ipc/register/cloudShare.ts @@ -0,0 +1,528 @@ +import { createShareThumbnail } from "../recording/shareThumbnail"; +import { createReadStream } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { Transform } from "node:stream"; +import { ipcMain } from "electron"; +import { + normalizeCloudEndpoint, + parseCloudRecordingList, + parseCloudShareTicket, +} from "../cloudShareContract"; +import { isOwnedExportPath } from "../export/exportStream"; +import { isAllowedLocalReadPath } from "../project/manager"; + +const MAX_CONTROL_RESPONSE_BYTES = 1024 * 1024; +const MAX_AUTH_TOKEN_BYTES = 8192; +const MULTIPART_THRESHOLD_BYTES = 50 * 1024 * 1024; +const MULTIPART_PART_BYTES = 25 * 1024 * 1024; +const MULTIPART_PART_ATTEMPTS = 3; +const activeUploads = new Map(); + +type UploadedPart = { partNumber: number; etag: string }; + +class NonRetryableUploadError extends Error {} + +function contentTypeFor(filePath: string) { + switch (path.extname(filePath).toLowerCase()) { + case ".mp4": + return "video/mp4"; + case ".webm": + return "video/webm"; + case ".mov": + return "video/quicktime"; + case ".gif": + return "image/gif"; + default: + return "application/octet-stream"; + } +} + +async function readJsonResponse(response: Response) { + const text = await response.text(); + if (Buffer.byteLength(text) > MAX_CONTROL_RESPONSE_BYTES) { + throw new Error("The cloud share server response was too large."); + } + try { + return JSON.parse(text) as unknown; + } catch { + throw new Error(`The cloud share server returned invalid JSON (${response.status}).`); + } +} + +async function responseError(response: Response, fallback: string) { + try { + const body = await readJsonResponse(response); + if (body && typeof body === "object" && "error" in body) { + return String((body as { error: unknown }).error); + } + } catch { + // Cloudflare may return an HTML/plain-text edge error instead of Worker JSON. + } + return `${fallback} (${response.status}).`; +} + +function waitForRetry(delayMs: number, signal: AbortSignal) { + return new Promise((resolve, reject) => { + if (signal.aborted) { + reject(signal.reason ?? new Error("Upload aborted")); + return; + } + const handleAbort = () => { + clearTimeout(timeout); + reject(signal.reason ?? new Error("Upload aborted")); + }; + const timeout = setTimeout(() => { + signal.removeEventListener("abort", handleAbort); + resolve(); + }, delayMs); + signal.addEventListener("abort", handleAbort, { once: true }); + }); +} + +function isRetryableUploadStatus(status: number) { + return status === 408 || status === 425 || status === 429 || status >= 500; +} + +async function uploadMultipart(options: { + endpoint: string; + shareCode: string; + filePath: string; + fileSize: number; + contentType: string; + token: string; + signal: AbortSignal; + onProgress: (uploadedBytes: number) => void; +}) { + const endpointOrigin = new URL(options.endpoint).origin; + const authHeaders = { authorization: `Bearer ${options.token}` }; + const startResponse = await fetch( + new URL(`/api/upload-multipart/${options.shareCode}`, endpointOrigin), + { + method: "POST", + headers: authHeaders, + redirect: "error", + signal: options.signal, + }, + ); + if (!startResponse.ok) { + throw new Error(await responseError(startResponse, "Could not start video upload")); + } + const startBody = await readJsonResponse(startResponse); + const multipartUploadId = + startBody && + typeof startBody === "object" && + "uploadId" in startBody && + typeof (startBody as { uploadId: unknown }).uploadId === "string" + ? (startBody as { uploadId: string }).uploadId + : ""; + if (!multipartUploadId || Buffer.byteLength(multipartUploadId) > 2048) { + throw new Error("The cloud share server returned an invalid multipart upload ID."); + } + + const encodedUploadId = encodeURIComponent(multipartUploadId); + const parts: UploadedPart[] = []; + let confirmedBytes = 0; + let completed = false; + + try { + const partCount = Math.ceil(options.fileSize / MULTIPART_PART_BYTES); + for (let partIndex = 0; partIndex < partCount; partIndex += 1) { + const partNumber = partIndex + 1; + const start = partIndex * MULTIPART_PART_BYTES; + const end = Math.min(options.fileSize, start + MULTIPART_PART_BYTES) - 1; + const partSize = end - start + 1; + let uploadedPart: UploadedPart | undefined; + + for (let attempt = 1; attempt <= MULTIPART_PART_ATTEMPTS; attempt += 1) { + let attemptBytes = 0; + const progress = new Transform({ + transform(chunk, _encoding, callback) { + attemptBytes += chunk.length; + options.onProgress( + Math.min(options.fileSize, confirmedBytes + attemptBytes), + ); + callback(null, chunk); + }, + }); + const source = createReadStream(options.filePath, { start, end }); + const body = source.pipe(progress); + source.on("error", (error) => body.destroy(error)); + try { + const partResponse = await fetch( + new URL( + `/api/upload-part/${options.shareCode}/${encodedUploadId}/${partNumber}`, + endpointOrigin, + ), + { + method: "PUT", + headers: { + ...authHeaders, + "content-type": options.contentType, + "content-length": String(partSize), + }, + body: body as unknown as BodyInit, + duplex: "half", + redirect: "error", + signal: options.signal, + } as RequestInit & { duplex: "half" }, + ); + if (!partResponse.ok) { + const message = await responseError( + partResponse, + `Video part ${partNumber} failed`, + ); + if (!isRetryableUploadStatus(partResponse.status)) { + throw new NonRetryableUploadError(message); + } + throw new Error(message); + } else { + const partBody = await readJsonResponse(partResponse); + const etag = + partBody && typeof partBody === "object" && "etag" in partBody + ? (partBody as { etag: unknown }).etag + : undefined; + const returnedPartNumber = + partBody && typeof partBody === "object" && "partNumber" in partBody + ? (partBody as { partNumber: unknown }).partNumber + : undefined; + if (typeof etag !== "string" || returnedPartNumber !== partNumber) { + throw new NonRetryableUploadError( + `The cloud share server returned invalid data for video part ${partNumber}.`, + ); + } + uploadedPart = { partNumber, etag }; + break; + } + } catch (error) { + if ( + options.signal.aborted || + error instanceof NonRetryableUploadError || + attempt === MULTIPART_PART_ATTEMPTS + ) { + throw error; + } + } finally { + body.destroy(); + source.destroy(); + } + + options.onProgress(confirmedBytes); + await waitForRetry(300 * 3 ** (attempt - 1), options.signal); + } + + if (!uploadedPart) throw new Error(`Video part ${partNumber} could not be uploaded.`); + parts.push(uploadedPart); + confirmedBytes += partSize; + options.onProgress(confirmedBytes); + } + + const completeResponse = await fetch( + new URL(`/api/upload-complete/${options.shareCode}/${encodedUploadId}`, endpointOrigin), + { + method: "POST", + headers: { ...authHeaders, "content-type": "application/json" }, + body: JSON.stringify({ parts }), + redirect: "error", + signal: options.signal, + }, + ); + if (!completeResponse.ok) { + throw new Error(await responseError(completeResponse, "Could not finish video upload")); + } + completed = true; + } finally { + if (!completed) { + try { + await fetch( + new URL( + `/api/upload-abort/${options.shareCode}/${encodedUploadId}`, + endpointOrigin, + ), + { + method: "POST", + headers: authHeaders, + redirect: "error", + signal: AbortSignal.timeout(5000), + }, + ); + } catch { + // R2 also reaps abandoned multipart uploads, so cleanup is best-effort. + } + } + } +} + +export function registerCloudShareHandlers() { + ipcMain.handle( + "cloud-share-manage", + async ( + _event, + input: { endpoint?: unknown; token?: unknown; action?: unknown; shareCode?: unknown }, + ) => { + try { + const endpoint = normalizeCloudEndpoint(input?.endpoint); + if ( + typeof input.token !== "string" || + !input.token || + Buffer.byteLength(input.token) > MAX_AUTH_TOKEN_BYTES + ) + throw new Error("Sign in to manage shared recordings."); + if (input.action !== "list" && input.action !== "delete") + throw new Error("Invalid recording action."); + if ( + input.action === "delete" && + (typeof input.shareCode !== "string" || + !/^[a-z0-9]{1,128}$/.test(input.shareCode)) + ) + throw new Error("Invalid recording ID."); + const response = await fetch( + new URL( + input.action === "list" ? "/api/videos" : `/api/delete/${input.shareCode}`, + endpoint, + ), + { + method: input.action === "list" ? "GET" : "DELETE", + headers: { authorization: `Bearer ${input.token}` }, + redirect: "error", + signal: AbortSignal.timeout(30000), + }, + ); + if (!response.ok) + throw new Error( + await responseError(response, "Could not manage shared recordings"), + ); + if (input.action === "delete") return { success: true }; + return { + success: true, + videos: parseCloudRecordingList(await readJsonResponse(response), endpoint), + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }, + ); + + ipcMain.handle( + "cloud-share-upload", + async ( + event, + input: { + filePath?: unknown; + endpoint?: unknown; + token?: unknown; + title?: unknown; + notes?: unknown; + uploadId?: unknown; + }, + ) => { + let uploadId: string | undefined; + try { + const endpoint = normalizeCloudEndpoint(input?.endpoint); + if (typeof input?.filePath !== "string" || input.filePath.trim().length === 0) { + throw new Error("Export a video before sharing it."); + } + uploadId = + typeof input.uploadId === "string" && /^[a-f0-9-]{36}$/i.test(input.uploadId) + ? input.uploadId + : crypto.randomUUID(); + const controller = new AbortController(); + activeUploads.set(uploadId, controller); + const requestedPath = path.resolve(input.filePath); + const resolvedPath = await fs.realpath(requestedPath); + const isRecordlyExport = + isOwnedExportPath(requestedPath) || isOwnedExportPath(resolvedPath); + if (!isRecordlyExport && !isAllowedLocalReadPath(resolvedPath)) { + throw new Error("This file is outside Recordly's approved media locations."); + } + const stat = await fs.stat(resolvedPath); + if (!stat.isFile()) throw new Error("The exported video could not be found."); + const title = + (typeof input.title === "string" ? input.title.trim().slice(0, 200) : "") || + path.basename(resolvedPath, path.extname(resolvedPath)); + const notes = + typeof input.notes === "string" ? input.notes.trim().slice(0, 2000) : ""; + + const token = typeof input.token === "string" ? input.token.trim() : ""; + if (!token || Buffer.byteLength(token) > MAX_AUTH_TOKEN_BYTES) { + throw new Error("Sign in to Recordly before creating a shared link."); + } + const controlHeaders: Record = { + "content-type": "application/json", + }; + if (token) controlHeaders.authorization = `Bearer ${token}`; + const ticketResponse = await fetch(endpoint, { + method: "POST", + headers: controlHeaders, + body: JSON.stringify({ + name: path.basename(resolvedPath), + title, + fileSize: stat.size, + notes, + size: stat.size, + contentType: contentTypeFor(resolvedPath), + }), + redirect: "error", + signal: controller.signal, + }); + const ticketBody = await readJsonResponse(ticketResponse); + if (!ticketResponse.ok) { + const message = + ticketBody && typeof ticketBody === "object" && "error" in ticketBody + ? String((ticketBody as { error: unknown }).error) + : `Cloud share request failed (${ticketResponse.status}).`; + throw new Error(message); + } + const ticket = parseCloudShareTicket(ticketBody); + const endpointOrigin = new URL(endpoint).origin; + if (new URL(ticket.shareUrl).origin !== endpointOrigin) { + throw new Error("The share service returned an untrusted viewing URL."); + } + if (ticket.finalizeUrl && new URL(ticket.finalizeUrl).origin !== endpointOrigin) { + throw new Error("The share service returned an untrusted finalization URL."); + } + const contentType = contentTypeFor(resolvedPath); + const sendProgress = (uploadedBytes: number) => { + if (event.sender.isDestroyed()) return; + event.sender.send("cloud-share-progress", { + uploadId, + uploadedBytes, + totalBytes: stat.size, + }); + }; + const canUseMultipart = + stat.size > MULTIPART_THRESHOLD_BYTES && + ticket.shareCode !== undefined && + new URL(ticket.uploadUrl).origin === endpointOrigin; + + if (canUseMultipart) { + await uploadMultipart({ + endpoint, + shareCode: ticket.shareCode as string, + filePath: resolvedPath, + fileSize: stat.size, + contentType, + token, + signal: controller.signal, + onProgress: sendProgress, + }); + } else { + let uploaded = 0; + const progress = new Transform({ + transform(chunk, _encoding, callback) { + uploaded += chunk.length; + sendProgress(uploaded); + callback(null, chunk); + }, + }); + const source = createReadStream(resolvedPath); + const body = source.pipe(progress); + source.on("error", (error) => body.destroy(error)); + const uploadHeaders: Record = { + "content-type": contentType, + "content-length": String(stat.size), + ...ticket.headers, + }; + if (new URL(ticket.uploadUrl).origin === endpointOrigin) { + uploadHeaders.authorization = `Bearer ${token}`; + } + try { + const uploadResponse = await fetch(ticket.uploadUrl, { + method: "PUT", + headers: uploadHeaders, + body: body as unknown as BodyInit, + duplex: "half", + redirect: "error", + signal: controller.signal, + } as RequestInit & { duplex: "half" }); + if (!uploadResponse.ok) { + throw new Error( + await responseError(uploadResponse, "Video upload failed"), + ); + } + } finally { + body.destroy(); + source.destroy(); + } + } + + if (ticket.finalizeUrl) { + const finalizeHeaders: Record = { + "content-type": "application/json", + }; + if (token && new URL(ticket.finalizeUrl).origin === new URL(endpoint).origin) { + finalizeHeaders.authorization = `Bearer ${token}`; + } + const finalizeResponse = await fetch(ticket.finalizeUrl, { + method: "POST", + headers: finalizeHeaders, + body: JSON.stringify({ + title, + summary: notes, + }), + redirect: "error", + signal: controller.signal, + }); + if (!finalizeResponse.ok) { + throw new Error( + `Cloud share finalization failed (${finalizeResponse.status}).`, + ); + } + } + if (ticket.shareCode) { + try { + const thumbnail = await createShareThumbnail( + resolvedPath, + controller.signal, + ); + const response = await fetch( + new URL(`/api/upload-thumbnail/${ticket.shareCode}`, endpointOrigin), + { + method: "PUT", + headers: { + authorization: `Bearer ${token}`, + "content-type": "image/jpeg", + "content-length": String(thumbnail.length), + }, + body: new Uint8Array(thumbnail), + redirect: "error", + signal: controller.signal, + }, + ); + if (!response.ok) + throw new Error(`Thumbnail upload failed (${response.status})`); + } catch { + // The recording is already published. Older/missing posters use the + // video's first frame in the library instead of failing the share. + console.warn( + "Shared video poster unavailable; using video preview fallback.", + ); + } + } + + return { success: true, uploadId, shareUrl: ticket.shareUrl }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + success: false, + uploadId, + canceled: message.includes("aborted"), + error: message, + }; + } finally { + if (uploadId) activeUploads.delete(uploadId); + } + }, + ); + + ipcMain.handle("cloud-share-cancel", (_event, uploadId: unknown) => { + if (typeof uploadId !== "string") return { success: false }; + const controller = activeUploads.get(uploadId); + if (!controller) return { success: false }; + controller.abort(); + return { success: true }; + }); +} diff --git a/electron/ipc/register/export.test.ts b/electron/ipc/register/export.test.ts new file mode 100644 index 000000000..df1e9d857 --- /dev/null +++ b/electron/ipc/register/export.test.ts @@ -0,0 +1,86 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("electron", () => ({ + app: { + getAppPath: () => process.cwd(), + getPath: () => process.env.TEMP ?? process.cwd(), + isPackaged: false, + }, + BrowserWindow: { + fromWebContents: () => null, + }, + dialog: { + showSaveDialog: vi.fn(), + }, + ipcMain: { + handle: vi.fn(), + }, + powerSaveBlocker: { + isStarted: () => true, + start: () => 1, + stop: vi.fn(), + }, +})); + +vi.mock("../ffmpeg/binary", () => ({ + getFfmpegBinaryPath: () => "ffmpeg", +})); + +import { moveExportedTempFile } from "./export"; + +const tempDirs: string[] = []; + +async function makeTempDir() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-export-move-")); + tempDirs.push(dir); + return dir; +} + +afterEach(async () => { + vi.restoreAllMocks(); + await Promise.allSettled( + tempDirs.splice(0).map((dir) => fs.rm(dir, { force: true, recursive: true })), + ); +}); + +describe("moveExportedTempFile", () => { + it("moves an app-managed export temp file to the selected destination", async () => { + const dir = await makeTempDir(); + const tempPath = path.join(dir, "export-temp.mp4"); + const destinationPath = path.join(dir, "export-final.mp4"); + await fs.writeFile(tempPath, "recordly-export"); + + await moveExportedTempFile(tempPath, destinationPath); + + await expect(fs.readFile(destinationPath, "utf8")).resolves.toBe("recordly-export"); + await expect(fs.access(tempPath)).rejects.toThrow(); + }); + + it("falls back when Windows reports the destination already exists during initial rename", async () => { + const dir = await makeTempDir(); + const tempPath = path.join(dir, "export-temp.mp4"); + const destinationPath = path.join(dir, "export-final.mp4"); + await fs.writeFile(tempPath, "new-export"); + await fs.writeFile(destinationPath, "previous-export"); + + const originalRename = fs.rename.bind(fs); + const renameSpy = vi.spyOn(fs, "rename"); + renameSpy.mockImplementation(async (from, to) => { + if (from === tempPath && to === destinationPath) { + const error = new Error("destination exists") as NodeJS.ErrnoException; + error.code = "EEXIST"; + throw error; + } + + return originalRename(from, to); + }); + + await moveExportedTempFile(tempPath, destinationPath); + + await expect(fs.readFile(destinationPath, "utf8")).resolves.toBe("new-export"); + await expect(fs.access(tempPath)).rejects.toThrow(); + }); +}); diff --git a/electron/ipc/register/export.ts b/electron/ipc/register/export.ts new file mode 100644 index 000000000..82b523de7 --- /dev/null +++ b/electron/ipc/register/export.ts @@ -0,0 +1,1093 @@ +import type { ChildProcessByStdio } from "node:child_process"; +import { spawn } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import type { Readable, Writable } from "node:stream"; +import type { SaveDialogOptions } from "electron"; +import { app, BrowserWindow, dialog, ipcMain } from "electron"; +import { + closeExportStream, + isOwnedExportPath, + openExportStream, + registerOwnedExportPath, + releaseOwnedExportPath, + writeToExportStream, +} from "../export/exportStream"; +import { + enqueueNativeVideoExportFrameWrite, + enqueueNativeVideoExportFrameWrites, + exportNativeStaticLayoutVideo, + flushNativeVideoExportPendingWriteRequests, + getExportHardwareInfo, + getNativeExportCapabilities, + getNativeVideoExportMaxQueuedWriteBytes, + getNativeVideoExportSessionError, + isHardwareAcceleratedVideoEncoder, + isIgnorableNativeVideoExportStreamError, + muxExportedVideoAudioBuffer, + muxNativeVideoExportAudio, + type NativeStaticLayoutExportOptions, + type NativeVideoExportSession, + nativeStaticLayoutExportSessions, + nativeVideoExportSessions, + probeNativeVideoMetadata, + removeTemporaryExportFile, + resolveNativeVideoEncoder, + sendNativeVideoExportWriteFrameResult, + settleNativeVideoExportWriteFrameRequest, +} from "../export/native-video"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; +import { + buildNativeH264StreamExportArgs, + buildNativeVideoExportArgs, + getNativeVideoInputByteSize, + type NativeExportEncodingMode, + type NativeVideoExportFinishOptions, +} from "../nativeVideoExport"; +import { isAllowedLocalReadPath, resolveApprovedLocalMediaPath } from "../project/manager"; +import { approveUserPath } from "../utils"; +import { + type CaptionSidecarPayload, + parseCaptionSidecarPayload, + withCaptionSidecarMessage, + writeCaptionSidecarsBestEffort, +} from "./exportCaptionSidecars"; + +function getPartialExportDestinationPath(destinationPath: string) { + const parsed = path.parse(destinationPath); + const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + return path.join(parsed.dir, `.recordly-partial-${parsed.name}-${suffix}${parsed.ext}`); +} + +const MAX_IN_MEMORY_EXPORT_BYTES = 0x7fffffff; + +function getInMemoryExportTooLargeMessage(byteLength: number) { + if (byteLength <= MAX_IN_MEMORY_EXPORT_BYTES) { + return null; + } + + return "Export is too large for the legacy in-memory save path. Please retry with temp-file streaming enabled."; +} + +export async function moveExportedTempFile(tempPath: string, destinationPath: string) { + await fs.mkdir(path.dirname(destinationPath), { recursive: true }); + try { + await fs.rename(tempPath, destinationPath); + return; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "EXDEV" && code !== "EPERM" && code !== "ENOTEMPTY" && code !== "EEXIST") { + throw error; + } + // Cross-device or Windows permission quirks — fall back to copy + unlink so + // exporting to a different volume still works. + } + + const partialDestinationPath = getPartialExportDestinationPath(destinationPath); + try { + await fs.copyFile(tempPath, partialDestinationPath); + try { + await fs.rename(partialDestinationPath, destinationPath); + } catch (renameError) { + const code = (renameError as NodeJS.ErrnoException).code; + if (code !== "EEXIST" && code !== "EPERM") { + throw renameError; + } + + const backupDestinationPath = getPartialExportDestinationPath(destinationPath); + let movedExistingDestination = false; + try { + await fs.rename(destinationPath, backupDestinationPath); + movedExistingDestination = true; + } catch (backupError) { + if ((backupError as NodeJS.ErrnoException).code !== "ENOENT") { + throw backupError; + } + } + + try { + await fs.rename(partialDestinationPath, destinationPath); + } catch (replaceError) { + if (movedExistingDestination) { + await fs.rename(backupDestinationPath, destinationPath).catch(() => undefined); + } + throw replaceError; + } + + if (movedExistingDestination) { + await fs.rm(backupDestinationPath, { force: true }).catch((cleanupError) => { + console.warn( + `[export] Failed to remove backup file after replace (${backupDestinationPath}):`, + cleanupError, + ); + }); + } + } + try { + await fs.rm(tempPath, { force: true }); + } catch (unlinkError) { + // Copy succeeded, so the export itself is safe; surface the leaked temp + // path instead of silently swallowing the failure so operators can + // reclaim disk space manually if the OS temp reaper misses it. + console.warn( + `[export] Failed to remove temp file after cross-volume copy (${tempPath}):`, + unlinkError, + ); + } + } catch (error) { + await fs.rm(partialDestinationPath, { force: true }).catch(() => undefined); + throw error; + } +} + +async function resolveAllowedReadableFilePath( + filePath: string, + label: string, + options: { mediaOnly?: boolean } = {}, +) { + if (typeof filePath !== "string" || filePath.trim().length === 0) { + throw new Error(`${label} requires a file path`); + } + + if (options.mediaOnly ?? true) { + const resolvedMediaPath = await resolveApprovedLocalMediaPath(filePath); + if (!resolvedMediaPath) { + throw new Error(`${label} is not an approved readable media file`); + } + + return resolvedMediaPath; + } + + const resolvedPath = path.resolve(filePath); + if (!isAllowedLocalReadPath(resolvedPath)) { + throw new Error(`${label} is not approved for local reads`); + } + + const realPath = await fs.realpath(resolvedPath).catch(() => null); + if (!realPath) { + throw new Error(`${label} does not exist`); + } + + const stat = await fs.stat(realPath).catch(() => null); + if (!stat?.isFile()) { + throw new Error(`${label} is not a readable file`); + } + + if (!isAllowedLocalReadPath(realPath)) { + throw new Error(`${label} is not approved for local reads`); + } + + return realPath; +} + +async function sanitizeNativeStaticLayoutExportOptions( + options: NativeStaticLayoutExportOptions, +): Promise { + const sanitized: NativeStaticLayoutExportOptions = { + ...options, + inputPath: await resolveAllowedReadableFilePath(options.inputPath, "Native input"), + }; + const mutableOptions = sanitized as unknown as Record; + + for (const [field, label] of [ + ["backgroundImagePath", "Native background image"], + ["webcamInputPath", "Native webcam input"], + ["cursorAtlasPath", "Native cursor atlas"], + ] as const) { + const value = mutableOptions[field]; + if (typeof value === "string" && value.trim().length > 0) { + mutableOptions[field] = await resolveAllowedReadableFilePath(value, label); + } else if (value === "" || value === undefined) { + mutableOptions[field] = null; + } else if (value !== null) { + throw new Error(`${label} must be a file path`); + } + } + + for (const [field, label] of [ + ["cursorTelemetryPath", "Native cursor telemetry"], + ["cursorAtlasMetadataPath", "Native cursor atlas metadata"], + ["zoomTelemetryPath", "Native zoom telemetry"], + ["timelineMapPath", "Native timeline map"], + ] as const) { + const value = mutableOptions[field]; + if (typeof value === "string" && value.trim().length > 0) { + mutableOptions[field] = await resolveAllowedReadableFilePath(value, label, { + mediaOnly: false, + }); + } else if (value === "" || value === undefined) { + mutableOptions[field] = null; + } else if (value !== null) { + throw new Error(`${label} must be a file path`); + } + } + + const audioOptions = sanitized.audioOptions; + if (audioOptions?.audioSourcePath) { + sanitized.audioOptions = { + ...audioOptions, + audioSourcePath: await resolveAllowedReadableFilePath( + audioOptions.audioSourcePath, + "Native audio source", + ), + }; + } + + return sanitized; +} + +function isTempPathSafe(tempPath: string): boolean { + const tempRoot = path.resolve(app.getPath("temp")); + const candidate = path.resolve(tempPath); + if (candidate === tempRoot) { + return false; + } + const withSep = tempRoot.endsWith(path.sep) ? tempRoot : tempRoot + path.sep; + return candidate.startsWith(withSep); +} + +export function registerExportHandlers() { + ipcMain.handle( + "native-video-export-start", + async ( + event, + options: { + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: NativeExportEncodingMode; + inputMode?: "rawvideo" | "h264-stream"; + }, + ) => { + try { + if (options.width % 2 !== 0 || options.height % 2 !== 0) { + throw new Error("Native export requires even output dimensions"); + } + + const ffmpegPath = getFfmpegBinaryPath(); + const inputMode = options.inputMode ?? "rawvideo"; + const sessionId = `recordly-export-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const outputPath = path.join(app.getPath("temp"), `${sessionId}.mp4`); + + let encoderName: string; + let ffmpegArgs: string[]; + + if (inputMode === "h264-stream") { + // Pre-encoded H.264 Annex B from browser VideoEncoder — just stream-copy into MP4 + encoderName = "h264-stream-copy"; + ffmpegArgs = buildNativeH264StreamExportArgs({ + frameRate: options.frameRate, + outputPath, + }); + } else { + encoderName = await resolveNativeVideoEncoder(ffmpegPath, options.encodingMode); + ffmpegArgs = buildNativeVideoExportArgs(encoderName, options, outputPath); + } + + const ffmpegProcess = spawn(ffmpegPath, ffmpegArgs, { + stdio: ["pipe", "ignore", "pipe"], + }) as ChildProcessByStdio; + // For rawvideo, frames are a fixed RGBA size. For h264-stream, chunks are variable. + const inputByteSize = + inputMode === "rawvideo" + ? getNativeVideoInputByteSize(options.width, options.height) + : 0; + + const session: NativeVideoExportSession = { + ffmpegProcess, + outputPath, + inputByteSize, + inputMode, + maxQueuedWriteBytes: + inputMode === "h264-stream" + ? 32 * 1024 * 1024 + : getNativeVideoExportMaxQueuedWriteBytes(inputByteSize), + stderrOutput: "", + encoderName, + processError: null, + stdinError: null, + terminating: false, + writeSequence: Promise.resolve(), + sender: event.sender, + pendingWriteRequestIds: new Set(), + completionPromise: new Promise((resolve, reject) => { + ffmpegProcess.once("error", (error) => { + const processError = + error instanceof Error ? error : new Error(String(error)); + if (session.terminating) { + resolve(); + return; + } + + session.processError = processError; + reject(processError); + }); + ffmpegProcess.stdin.once("error", (error) => { + const stdinError = + error instanceof Error ? error : new Error(String(error)); + if ( + session.terminating && + isIgnorableNativeVideoExportStreamError(stdinError) + ) { + return; + } + + session.stdinError = stdinError; + }); + ffmpegProcess.once("close", (code, signal) => { + if (session.terminating) { + resolve(); + return; + } + + if (code === 0) { + resolve(); + return; + } + + reject( + new Error( + getNativeVideoExportSessionError( + session, + `FFmpeg exited with code ${code ?? "unknown"}${signal ? ` (signal ${signal})` : ""}`, + ), + ), + ); + }); + }), + }; + void session.completionPromise.catch(() => undefined); + + ffmpegProcess.stderr.on("data", (chunk: Buffer) => { + session.stderrOutput += chunk.toString(); + }); + + nativeVideoExportSessions.set(sessionId, session); + + console.log( + `[native-export] Started ${isHardwareAcceleratedVideoEncoder(encoderName) ? "hardware" : "software"} session ${sessionId} with ${encoderName}`, + ); + + return { + success: true, + sessionId, + encoderName, + }; + } catch (error) { + console.error( + "[native-export] Failed to start native video export session:", + error, + ); + return { + success: false, + error: String(error), + }; + } + }, + ); + + ipcMain.handle("probe-native-video-metadata", async (_, filePath: string) => { + try { + if (typeof filePath !== "string" || filePath.trim().length === 0) { + throw new Error("Native metadata probe requires a file path"); + } + + const resolvedFilePath = await resolveAllowedReadableFilePath( + filePath, + "Native metadata probe", + ); + const metadata = await probeNativeVideoMetadata( + getFfmpegBinaryPath(), + resolvedFilePath, + ); + return { + success: true, + metadata, + }; + } catch (error) { + console.warn("[probe-native-video-metadata] Failed:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }); + + ipcMain.handle("get-native-export-capabilities", async () => { + try { + return { + success: true, + capabilities: await getNativeExportCapabilities(), + }; + } catch (error) { + console.warn("[native-export-capabilities] Failed:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }); + + ipcMain.handle("get-export-hardware-info", async () => { + try { + return { + success: true, + hardware: await getExportHardwareInfo(), + }; + } catch (error) { + console.warn("[export-hardware-info] Failed:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }); + + ipcMain.handle( + "native-static-layout-export", + async (event, options: NativeStaticLayoutExportOptions) => { + try { + if (!options || typeof options.inputPath !== "string") { + throw new Error("Native static layout export requires an input path"); + } + const sanitizedOptions = await sanitizeNativeStaticLayoutExportOptions(options); + + const result = await exportNativeStaticLayoutVideo( + getFfmpegBinaryPath(), + sanitizedOptions, + (progress) => { + if (event.sender.isDestroyed()) { + return; + } + + event.sender.send("native-static-layout-export-progress", progress); + }, + ); + registerOwnedExportPath(result.outputPath); + const primaryBackend = result.metrics.chunks[0]?.backend; + return { + success: true, + tempPath: result.outputPath, + encoderName: + primaryBackend === "nvidia-cuda-compositor" + ? "nvidia-cuda-compositor" + : primaryBackend === "windows-d3d11-compositor" + ? "windows-d3d11-compositor" + : result.metrics.chunkCount > 1 + ? "chunked-h264-nvenc" + : "static-layout-h264-nvenc", + metrics: result.metrics, + }; + } catch (error) { + console.warn("[native-static-layout-export] Failed:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }, + ); + + ipcMain.handle("native-static-layout-export-cancel", async (_, sessionId: string) => { + const session = nativeStaticLayoutExportSessions.get(sessionId); + if (!session) { + return { success: true }; + } + + session.terminating = true; + try { + session.currentProcess?.kill("SIGKILL"); + } catch { + // Process may already be closed. + } + + return { success: true }; + }); + + ipcMain.on( + "native-video-export-write-frames-async", + ( + event, + payload: { + sessionId: string; + requestId: number; + frameDataList: Uint8Array[]; + }, + ) => { + const sessionId = payload?.sessionId; + const requestId = payload?.requestId; + const frameDataList = payload?.frameDataList; + + if ( + typeof sessionId !== "string" || + typeof requestId !== "number" || + !Array.isArray(frameDataList) || + frameDataList.length === 0 + ) { + return; + } + + const session = nativeVideoExportSessions.get(sessionId); + if (!session) { + sendNativeVideoExportWriteFrameResult(event.sender, sessionId, requestId, { + success: false, + error: "Invalid native export session", + }); + return; + } + + session.sender = event.sender; + session.pendingWriteRequestIds.add(requestId); + + if (session.terminating) { + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: false, + error: "Native video export session was cancelled", + }); + return; + } + + if ( + session.inputMode !== "h264-stream" && + frameDataList.some((frameData) => frameData.byteLength !== session.inputByteSize) + ) { + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: false, + error: "Native video export batch contained invalid frame sizes", + }); + return; + } + + void enqueueNativeVideoExportFrameWrites(session, frameDataList) + .then(() => { + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: true, + }); + }) + .catch((error) => { + session.stdinError = error instanceof Error ? error : new Error(String(error)); + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: false, + error: getNativeVideoExportSessionError( + session, + session.stdinError.message, + ), + }); + }); + }, + ); + + ipcMain.on( + "native-video-export-write-frame-async", + ( + event, + payload: { + sessionId: string; + requestId: number; + frameData: Uint8Array; + }, + ) => { + const sessionId = payload?.sessionId; + const requestId = payload?.requestId; + const frameData = payload?.frameData; + + if (typeof sessionId !== "string" || typeof requestId !== "number" || !frameData) { + return; + } + + const session = nativeVideoExportSessions.get(sessionId); + if (!session) { + sendNativeVideoExportWriteFrameResult(event.sender, sessionId, requestId, { + success: false, + error: "Invalid native export session", + }); + return; + } + + session.sender = event.sender; + session.pendingWriteRequestIds.add(requestId); + + if (session.terminating) { + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: false, + error: "Native video export session was cancelled", + }); + return; + } + + if ( + session.inputMode !== "h264-stream" && + frameData.byteLength !== session.inputByteSize + ) { + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: false, + error: `Native video export expected ${session.inputByteSize} bytes per frame but received ${frameData.byteLength}`, + }); + return; + } + + void enqueueNativeVideoExportFrameWrite(session, frameData) + .then(() => { + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: true, + }); + }) + .catch((error) => { + session.stdinError = error instanceof Error ? error : new Error(String(error)); + settleNativeVideoExportWriteFrameRequest(sessionId, session, requestId, { + success: false, + error: getNativeVideoExportSessionError( + session, + session.stdinError.message, + ), + }); + }); + }, + ); + + ipcMain.handle( + "native-video-export-finish", + async (_, sessionId: string, options?: NativeVideoExportFinishOptions) => { + const session = nativeVideoExportSessions.get(sessionId); + if (!session) { + return { success: false, error: "Invalid native export session" }; + } + + try { + await session.writeSequence; + if ( + !session.ffmpegProcess.stdin.destroyed && + !session.ffmpegProcess.stdin.writableEnded + ) { + session.ffmpegProcess.stdin.end(); + } + await session.completionPromise; + + const finalized = await muxNativeVideoExportAudio( + session.outputPath, + options ?? {}, + ); + nativeVideoExportSessions.delete(sessionId); + // Register the finalized path so only app-produced paths can flow back + // through finalize-exported-video / discard-exported-temp. + registerOwnedExportPath(finalized.outputPath); + if (finalized.outputPath !== session.outputPath) { + // muxNativeVideoExportAudio removes the intermediate on success, but + // clear our registry entry defensively in case a future refactor + // changes that contract. + releaseOwnedExportPath(session.outputPath); + } + + // Return a temp path instead of reading the file back into memory so we + // never hit V8's per-ArrayBuffer limit on >2 GiB exports. The renderer + // uses finalize-exported-video to move the file to its final path. + return { + success: true, + tempPath: finalized.outputPath, + encoderName: session.encoderName, + metrics: finalized.metrics, + }; + } catch (error) { + flushNativeVideoExportPendingWriteRequests(sessionId, session, String(error)); + nativeVideoExportSessions.delete(sessionId); + await removeTemporaryExportFile(session.outputPath); + const finalizedSuffix = session.outputPath.replace(/\.mp4$/, "-final.mp4"); + await removeTemporaryExportFile(finalizedSuffix); + return { + success: false, + error: String(error), + }; + } + }, + ); + + ipcMain.handle( + "mux-exported-video-audio-from-path", + async (_, videoPath: string, options?: NativeVideoExportFinishOptions) => { + if (typeof videoPath !== "string" || !isOwnedExportPath(videoPath)) { + return { + success: false, + error: "Video path is not an app-managed export temp", + }; + } + try { + const finalized = await muxNativeVideoExportAudio(videoPath, options ?? {}); + if (finalized.outputPath !== videoPath) { + registerOwnedExportPath(finalized.outputPath); + // muxNativeVideoExportAudio removes the intermediate on success, so + // the input is no longer owned by the registry after the call + // returns. + releaseOwnedExportPath(videoPath); + } + return { + success: true, + tempPath: finalized.outputPath, + metrics: finalized.metrics, + }; + } catch (error) { + // Only clean up the input path if it is still an owned temp (i.e. + // muxNativeVideoExportAudio failed before consuming it). + if (isOwnedExportPath(videoPath)) { + await removeTemporaryExportFile(videoPath); + releaseOwnedExportPath(videoPath); + } + return { success: false, error: String(error) }; + } + }, + ); + + ipcMain.handle( + "mux-exported-video-audio", + async (_, videoData: ArrayBuffer, options?: NativeVideoExportFinishOptions) => { + try { + const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength); + if (sizeError) { + return { + success: false, + error: sizeError, + }; + } + + const result = await muxExportedVideoAudioBuffer(videoData, options ?? {}); + // Register the muxed output so finalize-exported-video / discard- + // exported-temp accept it. Returning a temp path (instead of the + // muxed bytes) keeps us off Node's >2 GiB fs.readFile cap and + // avoids a redundant copy through the renderer. + registerOwnedExportPath(result.outputPath); + return { + success: true, + tempPath: result.outputPath, + metrics: result.metrics, + }; + } catch (error) { + return { + success: false, + error: String(error), + }; + } + }, + ); + + ipcMain.handle("export-stream-open", async (_event, options?: { extension?: string }) => { + try { + const result = await openExportStream(options); + return { success: true, streamId: result.streamId, tempPath: result.tempPath }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle( + "export-stream-write", + async (_event, streamId: string, position: number, chunk: Uint8Array) => { + try { + await writeToExportStream(streamId, position, chunk); + return { success: true }; + } catch (error) { + return { success: false, error: String(error) }; + } + }, + ); + + ipcMain.handle( + "export-stream-close", + async (_event, streamId: string, options?: { abort?: boolean }) => { + try { + const result = await closeExportStream(streamId, options); + return { + success: true, + tempPath: result.tempPath, + bytesWritten: result.bytesWritten, + }; + } catch (error) { + return { success: false, error: String(error) }; + } + }, + ); + + ipcMain.handle("native-video-export-cancel", async (_, sessionId: string) => { + const session = nativeVideoExportSessions.get(sessionId); + if (!session) { + return { success: true }; + } + + session.terminating = true; + nativeVideoExportSessions.delete(sessionId); + flushNativeVideoExportPendingWriteRequests( + sessionId, + session, + "Native video export session was cancelled", + ); + + try { + if ( + !session.ffmpegProcess.stdin.destroyed && + !session.ffmpegProcess.stdin.writableEnded + ) { + session.ffmpegProcess.stdin.destroy(); + } + } catch { + // Stream may already be closed. + } + + try { + session.ffmpegProcess.kill("SIGKILL"); + } catch { + // Process may already be closed. + } + + await session.completionPromise.catch(() => undefined); + await removeTemporaryExportFile(session.outputPath); + return { success: true }; + }); + + ipcMain.handle( + "save-exported-video", + async ( + event, + videoData: ArrayBuffer, + fileName: string, + captionSidecar?: CaptionSidecarPayload, + ) => { + try { + const sidecarPayload = parseCaptionSidecarPayload(captionSidecar); + const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength); + if (sizeError) { + return { + success: false, + message: sizeError, + error: sizeError, + }; + } + + // Determine file type from extension + const isGif = fileName.toLowerCase().endsWith(".gif"); + const filters = isGif + ? [{ name: "GIF Image", extensions: ["gif"] }] + : [{ name: "MP4 Video", extensions: ["mp4"] }]; + const parentWindow = BrowserWindow.fromWebContents(event.sender); + const saveDialogOptions: SaveDialogOptions = { + title: isGif ? "Save Exported GIF" : "Save Exported Video", + defaultPath: path.join(app.getPath("downloads"), fileName), + filters, + properties: ["createDirectory", "showOverwriteConfirmation"], + }; + + const result = parentWindow + ? await dialog.showSaveDialog(parentWindow, saveDialogOptions) + : await dialog.showSaveDialog(saveDialogOptions); + + if (result.canceled || !result.filePath) { + return { + success: false, + canceled: true, + message: "Export canceled", + }; + } + + await fs.writeFile(result.filePath, Buffer.from(videoData)); + const captionSidecarResult = await writeCaptionSidecarsBestEffort( + result.filePath, + sidecarPayload, + ); + approveUserPath(result.filePath); + + return { + success: true, + path: result.filePath, + message: withCaptionSidecarMessage( + "Video exported successfully", + captionSidecarResult, + ), + }; + } catch (error) { + console.error("Failed to save exported video:", error); + return { + success: false, + message: "Failed to save exported video", + error: String(error), + }; + } + }, + ); + + ipcMain.handle( + "write-exported-video-to-path", + async ( + _event, + videoData: ArrayBuffer, + outputPath: string, + captionSidecar?: CaptionSidecarPayload, + ) => { + try { + const sidecarPayload = parseCaptionSidecarPayload(captionSidecar); + const sizeError = getInMemoryExportTooLargeMessage(videoData.byteLength); + if (sizeError) { + return { + success: false, + message: sizeError, + canceled: false, + error: sizeError, + }; + } + + const resolvedPath = path.resolve(outputPath); + await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); + await fs.writeFile(resolvedPath, Buffer.from(videoData)); + const captionSidecarResult = await writeCaptionSidecarsBestEffort( + resolvedPath, + sidecarPayload, + ); + approveUserPath(resolvedPath); + + return { + success: true, + path: resolvedPath, + message: withCaptionSidecarMessage( + "Video exported successfully", + captionSidecarResult, + ), + canceled: false, + }; + } catch (error) { + console.error("Failed to write exported video to path:", error); + return { + success: false, + message: "Failed to write exported video", + canceled: false, + error: String(error), + }; + } + }, + ); + + ipcMain.handle( + "finalize-exported-video", + async ( + event, + payload: { + tempPath: string; + fileName: string; + outputPath?: string | null; + captionSidecar?: CaptionSidecarPayload; + }, + ) => { + const tempPath = payload?.tempPath; + const fileName = payload?.fileName; + if (typeof tempPath !== "string" || typeof fileName !== "string") { + return { success: false, error: "Invalid finalize-exported-video payload" }; + } + + if (!isTempPathSafe(tempPath) || !isOwnedExportPath(tempPath)) { + return { + success: false, + error: "Temp path is not an app-managed export temp", + }; + } + + try { + await fs.access(tempPath); + } catch { + return { + success: false, + error: `Exported video temp file is missing: ${tempPath}`, + }; + } + + try { + const sidecarPayload = parseCaptionSidecarPayload(payload.captionSidecar); + if (payload.outputPath) { + const resolvedPath = path.resolve(payload.outputPath); + await moveExportedTempFile(tempPath, resolvedPath); + releaseOwnedExportPath(tempPath); + const captionSidecarResult = await writeCaptionSidecarsBestEffort( + resolvedPath, + sidecarPayload, + ); + approveUserPath(resolvedPath); + return { + success: true, + path: resolvedPath, + canceled: false, + message: withCaptionSidecarMessage( + "Video exported successfully", + captionSidecarResult, + ), + }; + } + + const isGif = fileName.toLowerCase().endsWith(".gif"); + const filters = isGif + ? [{ name: "GIF Image", extensions: ["gif"] }] + : [{ name: "MP4 Video", extensions: ["mp4"] }]; + const parentWindow = BrowserWindow.fromWebContents(event.sender); + const saveDialogOptions: SaveDialogOptions = { + title: isGif ? "Save Exported GIF" : "Save Exported Video", + defaultPath: path.join(app.getPath("downloads"), fileName), + filters, + properties: ["createDirectory", "showOverwriteConfirmation"], + }; + + const result = parentWindow + ? await dialog.showSaveDialog(parentWindow, saveDialogOptions) + : await dialog.showSaveDialog(saveDialogOptions); + + if (result.canceled || !result.filePath) { + // Leave the temp file in place so the renderer can offer "Save Again" + // without re-rendering. The renderer owns cleanup on discard. + return { + success: false, + canceled: true, + message: "Export canceled", + }; + } + + await moveExportedTempFile(tempPath, result.filePath); + releaseOwnedExportPath(tempPath); + const captionSidecarResult = await writeCaptionSidecarsBestEffort( + result.filePath, + sidecarPayload, + ); + approveUserPath(result.filePath); + + return { + success: true, + path: result.filePath, + canceled: false, + message: withCaptionSidecarMessage( + "Video exported successfully", + captionSidecarResult, + ), + }; + } catch (error) { + console.error("Failed to finalize exported video:", error); + return { + success: false, + canceled: false, + message: "Failed to save exported video", + error: String(error), + }; + } + }, + ); + + ipcMain.handle("discard-exported-temp", async (_event, tempPath: string) => { + if (typeof tempPath !== "string" || tempPath.length === 0) { + return { success: false, error: "Invalid temp path" }; + } + if (!isTempPathSafe(tempPath) || !isOwnedExportPath(tempPath)) { + return { + success: false, + error: "Temp path is not an app-managed export temp", + }; + } + try { + await removeTemporaryExportFile(tempPath); + releaseOwnedExportPath(tempPath); + return { success: true }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); +} diff --git a/electron/ipc/register/exportCaptionSidecars.test.ts b/electron/ipc/register/exportCaptionSidecars.test.ts new file mode 100644 index 000000000..2fc571e13 --- /dev/null +++ b/electron/ipc/register/exportCaptionSidecars.test.ts @@ -0,0 +1,114 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + parseCaptionSidecarPayload, + serializeSrt, + serializeVtt, + withCaptionSidecarMessage, + writeCaptionSidecarsBestEffort, +} from "./exportCaptionSidecars"; + +describe("exportCaptionSidecars", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("serializes SRT cues with stable numbering and timestamps", () => { + expect( + serializeSrt([ + { + startMs: 1234, + endMs: 5678, + text: "Hello\nworld", + }, + ]), + ).toBe("1\n00:00:01,234 --> 00:00:05,678\nHello\nworld"); + }); + + it("serializes VTT cues with header and dot timestamps", () => { + expect( + serializeVtt([ + { + startMs: 0, + endMs: 2000, + text: "Caption", + }, + ]), + ).toBe("WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nCaption"); + }); + + it("drops malformed cues when parsing sidecar payloads", () => { + expect( + parseCaptionSidecarPayload({ + format: "both", + cues: [ + { startMs: 0, endMs: 1000, text: "ok" }, + { startMs: 2000, endMs: 1000, text: "bad range" }, + { startMs: 1000, endMs: 2000, text: " " }, + ], + }), + ).toEqual({ + format: "both", + cues: [{ startMs: 0, endMs: 1000, text: "ok" }], + }); + }); + + it("returns a warning result instead of throwing when sidecar writes fail", async () => { + const writeFileSpy = vi + .spyOn(fs, "writeFile") + .mockRejectedValueOnce(new Error("disk full")); + + await expect( + writeCaptionSidecarsBestEffort("/tmp/export.mp4", { + format: "srt", + cues: [{ startMs: 0, endMs: 1000, text: "Caption" }], + }), + ).resolves.toEqual({ + wroteAny: false, + error: "disk full", + }); + + expect(writeFileSpy).toHaveBeenCalledTimes(1); + }); + + it("writes requested caption sidecars when the filesystem succeeds", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-sidecar-test-")); + const videoPath = path.join(tempDir, "clip.mp4"); + + try { + await expect( + writeCaptionSidecarsBestEffort(videoPath, { + format: "both", + cues: [{ startMs: 0, endMs: 1000, text: "Caption" }], + }), + ).resolves.toEqual({ wroteAny: true, error: null }); + + await expect(fs.readFile(path.join(tempDir, "clip.srt"), "utf8")).resolves.toContain( + "00:00:00,000 --> 00:00:01,000", + ); + await expect(fs.readFile(path.join(tempDir, "clip.vtt"), "utf8")).resolves.toContain( + "WEBVTT", + ); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("appends a non-fatal caption warning only when sidecar writes fail", () => { + expect( + withCaptionSidecarMessage("Video exported successfully", { + wroteAny: false, + error: "disk full", + }), + ).toBe("Video exported successfully Captions could not be saved alongside the video."); + + expect( + withCaptionSidecarMessage("Video exported successfully", { + wroteAny: true, + error: null, + }), + ).toBe("Video exported successfully"); + }); +}); diff --git a/electron/ipc/register/exportCaptionSidecars.ts b/electron/ipc/register/exportCaptionSidecars.ts new file mode 100644 index 000000000..0560a9a96 --- /dev/null +++ b/electron/ipc/register/exportCaptionSidecars.ts @@ -0,0 +1,157 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +export type CaptionSidecarCue = { + startMs: number; + endMs: number; + text: string; +}; + +export type CaptionSidecarPayload = { + format: "srt" | "vtt" | "both"; + cues: CaptionSidecarCue[]; +}; + +export type CaptionSidecarWriteResult = { + wroteAny: boolean; + error: string | null; +}; + +function toSrtTimestamp(totalMs: number): string { + const ms = Math.max(0, Math.round(totalMs)); + const hours = Math.floor(ms / 3_600_000); + const minutes = Math.floor((ms % 3_600_000) / 60_000); + const seconds = Math.floor((ms % 60_000) / 1000); + const millis = ms % 1000; + return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")},${String(millis).padStart(3, "0")}`; +} + +function toVttTimestamp(totalMs: number): string { + const ms = Math.max(0, Math.round(totalMs)); + const hours = Math.floor(ms / 3_600_000); + const minutes = Math.floor((ms % 3_600_000) / 60_000); + const seconds = Math.floor((ms % 60_000) / 1000); + const millis = ms % 1000; + return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(millis).padStart(3, "0")}`; +} + +function normalizeCaptionSidecarCues(cues: unknown): CaptionSidecarCue[] { + if (!Array.isArray(cues)) { + return []; + } + + return cues + .filter((cue): cue is CaptionSidecarCue => { + return ( + typeof cue === "object" && + cue !== null && + typeof cue.startMs === "number" && + typeof cue.endMs === "number" && + typeof cue.text === "string" && + Number.isFinite(cue.startMs) && + Number.isFinite(cue.endMs) && + cue.endMs > cue.startMs && + cue.text.trim().length > 0 + ); + }) + .map((cue) => ({ + startMs: cue.startMs, + endMs: cue.endMs, + text: cue.text.replace(/\r\n/g, "\n").trim(), + })); +} + +export function parseCaptionSidecarPayload(payload: unknown): CaptionSidecarPayload | null { + if (typeof payload !== "object" || payload === null) { + return null; + } + + const candidate = payload as { + format?: unknown; + cues?: unknown; + }; + + const format = + candidate.format === "srt" || candidate.format === "vtt" || candidate.format === "both" + ? candidate.format + : null; + if (!format) { + return null; + } + + const cues = normalizeCaptionSidecarCues(candidate.cues); + if (cues.length === 0) { + return null; + } + + return { format, cues }; +} + +export function serializeSrt(cues: CaptionSidecarCue[]): string { + return cues + .map((cue, index) => { + return `${index + 1}\n${toSrtTimestamp(cue.startMs)} --> ${toSrtTimestamp(cue.endMs)}\n${cue.text}`; + }) + .join("\n\n"); +} + +export function serializeVtt(cues: CaptionSidecarCue[]): string { + const body = cues + .map((cue) => { + return `${toVttTimestamp(cue.startMs)} --> ${toVttTimestamp(cue.endMs)}\n${cue.text}`; + }) + .join("\n\n"); + return `WEBVTT\n\n${body}`; +} + +export async function writeCaptionSidecars( + videoPath: string, + payload: CaptionSidecarPayload | null, +) { + if (!payload) { + return; + } + + const parsed = path.parse(videoPath); + const basePath = path.join(parsed.dir, parsed.name); + + if (payload.format === "srt" || payload.format === "both") { + await fs.writeFile(`${basePath}.srt`, serializeSrt(payload.cues), "utf8"); + } + + if (payload.format === "vtt" || payload.format === "both") { + await fs.writeFile(`${basePath}.vtt`, serializeVtt(payload.cues), "utf8"); + } +} + +export async function writeCaptionSidecarsBestEffort( + videoPath: string, + payload: CaptionSidecarPayload | null, +): Promise { + if (!payload) { + return { wroteAny: false, error: null }; + } + + try { + await writeCaptionSidecars(videoPath, payload); + return { wroteAny: true, error: null }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn("[export] Failed to write caption sidecars:", { + videoPath, + message, + }); + return { wroteAny: false, error: message }; + } +} + +export function withCaptionSidecarMessage( + baseMessage: string, + captionSidecarResult: CaptionSidecarWriteResult, +) { + if (!captionSidecarResult.error) { + return baseMessage; + } + + return `${baseMessage} Captions could not be saved alongside the video.`; +} diff --git a/electron/ipc/register/permissions.test.ts b/electron/ipc/register/permissions.test.ts new file mode 100644 index 000000000..4491267b8 --- /dev/null +++ b/electron/ipc/register/permissions.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +const mocks = vi.hoisted(() => ({ + handlers: new Map unknown>(), + status: vi.fn(), + sources: vi.fn(), + open: vi.fn(), +})); +vi.mock("electron", () => ({ + ipcMain: { + handle: (key: string, handler: (...args: unknown[]) => unknown) => + mocks.handlers.set(key, handler), + }, + desktopCapturer: { getSources: mocks.sources }, + systemPreferences: { getMediaAccessStatus: mocks.status }, + shell: { openExternal: mocks.open }, +})); +vi.mock("../utils", () => ({ getMacPrivacySettingsUrl: (pane: string) => `settings:${pane}` })); +import { registerPermissionHandlers } from "./permissions"; +const platform = process.platform; +afterEach(() => Object.defineProperty(process, "platform", { value: platform })); +beforeEach(() => { + vi.resetAllMocks(); + Object.defineProperty(process, "platform", { value: "darwin" }); + mocks.handlers.clear(); + registerPermissionHandlers(); +}); +it("does not prompt or open settings merely by registering permission handlers", () => { + expect(mocks.sources).not.toHaveBeenCalled(); + expect(mocks.open).not.toHaveBeenCalled(); +}); +it("registers first-time Screen Recording access before opening its settings", async () => { + mocks.status.mockReturnValue("not-determined"); + mocks.sources.mockResolvedValue([]); + expect(await mocks.handlers.get("open-screen-recording-preferences")!()).toEqual({ + success: true, + }); + expect(mocks.sources).toHaveBeenCalledWith({ + types: ["screen"], + thumbnailSize: { width: 0, height: 0 }, + }); + expect(mocks.sources.mock.invocationCallOrder[0]).toBeLessThan( + mocks.open.mock.invocationCallOrder[0], + ); + expect(mocks.open).toHaveBeenCalledWith("settings:screen"); +}); +it("opens settings even when the first-time capture request is rejected", async () => { + mocks.status.mockReturnValue("not-determined"); + mocks.sources.mockRejectedValue(new Error("Denied")); + expect(await mocks.handlers.get("open-screen-recording-preferences")!()).toEqual({ + success: true, + }); + expect(mocks.open).toHaveBeenCalledWith("settings:screen"); +}); +it("does not enumerate screens for an already determined permission", async () => { + mocks.status.mockReturnValue("denied"); + await mocks.handlers.get("open-screen-recording-preferences")!(); + expect(mocks.sources).not.toHaveBeenCalled(); + expect(mocks.open).toHaveBeenCalledWith("settings:screen"); +}); +it("leaves native macOS permission controls unused on other platforms", async () => { + Object.defineProperty(process, "platform", { value: "win32" }); + expect(await mocks.handlers.get("open-screen-recording-preferences")!()).toEqual({ + success: true, + }); + expect(mocks.sources).not.toHaveBeenCalled(); + expect(mocks.open).not.toHaveBeenCalled(); +}); diff --git a/electron/ipc/register/permissions.ts b/electron/ipc/register/permissions.ts new file mode 100644 index 000000000..b529c739d --- /dev/null +++ b/electron/ipc/register/permissions.ts @@ -0,0 +1,98 @@ +import { desktopCapturer, ipcMain, shell, systemPreferences } from "electron"; +import { getMacPrivacySettingsUrl } from "../utils"; + +export function registerPermissionHandlers() { + ipcMain.handle("open-external-url", async (_, url: string) => { + try { + // Security: only allow http/https URLs to prevent file:// or custom protocol abuse + const parsed = new URL(url); + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + return { success: false, error: `Blocked non-HTTP URL: ${parsed.protocol}` }; + } + await shell.openExternal(url); + return { success: true }; + } catch (error) { + console.error("Failed to open URL:", error); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("get-accessibility-permission-status", () => { + if (process.platform !== "darwin") { + return { success: true, trusted: true, prompted: false }; + } + + return { + success: true, + trusted: systemPreferences.isTrustedAccessibilityClient(false), + prompted: false, + }; + }); + + ipcMain.handle("request-accessibility-permission", () => { + if (process.platform !== "darwin") { + return { success: true, trusted: true, prompted: false }; + } + + return { + success: true, + trusted: systemPreferences.isTrustedAccessibilityClient(true), + prompted: true, + }; + }); + + ipcMain.handle("get-screen-recording-permission-status", () => { + if (process.platform !== "darwin") { + return { success: true, status: "granted" }; + } + + try { + return { + success: true, + status: systemPreferences.getMediaAccessStatus("screen"), + }; + } catch (error) { + console.error("Failed to get screen recording permission status:", error); + return { success: false, status: "unknown", error: String(error) }; + } + }); + + ipcMain.handle("open-screen-recording-preferences", async () => { + if (process.platform !== "darwin") { + return { success: true }; + } + + try { + // First-time access must register Recordly in macOS before its settings row can be enabled. + if (systemPreferences.getMediaAccessStatus("screen") === "not-determined") { + try { + await desktopCapturer.getSources({ + types: ["screen"], + thumbnailSize: { width: 0, height: 0 }, + }); + } catch { + // A denied prompt still needs the settings page so access can be enabled later. + } + } + await shell.openExternal(getMacPrivacySettingsUrl("screen")); + return { success: true }; + } catch (error) { + console.error("Failed to open Screen Recording preferences:", error); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("open-accessibility-preferences", async () => { + if (process.platform !== "darwin") { + return { success: true }; + } + + try { + await shell.openExternal(getMacPrivacySettingsUrl("accessibility")); + return { success: true }; + } catch (error) { + console.error("Failed to open Accessibility preferences:", error); + return { success: false, error: String(error) }; + } + }); +} diff --git a/electron/ipc/register/project.ts b/electron/ipc/register/project.ts new file mode 100644 index 000000000..88dfb71d9 --- /dev/null +++ b/electron/ipc/register/project.ts @@ -0,0 +1,958 @@ +import { persistRecentMetadata } from "../project/recentMetadata"; +import { renameLibraryProject } from "../project/renameLibraryProject"; +import { createUntitledProject } from "../project/createUntitledProject"; +import { trashLibraryProjects } from "../project/trashProjects"; +import { getRecordingThumbnail } from "../recording/thumbnail"; +import { listRecordings, setRecordingsRemoved } from "../recording/library"; +import { importRecording, discardRecordingImport } from "../recording/importRecording"; +import { randomUUID } from "node:crypto"; +import { constants as fsConstants } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { BrowserWindow, dialog, ipcMain, shell } from "electron"; +import { RECORDINGS_DIR } from "../../appPaths"; +import { buildMediaUrl, getMediaServerBaseUrl } from "../../mediaServer"; +import { LEGACY_PROJECT_FILE_EXTENSIONS, PROJECT_FILE_EXTENSION } from "../constants"; +import { getProjectBackupPath, writeProjectFileAtomically } from "../project/atomicSave"; +import { + getProjectsDir, + getProjectThumbnailPath, + isPathInsideDirectory, + isTrustedProjectPath, + listProjectLibraryEntries, + loadProjectFromPath, + readProjectPreview, + loadRecentProjectPaths, + persistRecordingsDirectorySetting, + rememberApprovedLocalReadPath, + rememberRecentProject, + replaceApprovedSessionLocalReadPaths, + resolveApprovedLocalMediaPath, + saveProjectThumbnail, + saveRecentProjectPaths, +} from "../project/manager"; +import { persistRecordingSessionManifest, resolveRecordingSession } from "../project/session"; +import { + currentProjectPath, + currentRecordingSession, + currentVideoPath, + setCurrentProjectPath, + setCurrentRecordingSession, + setCurrentVideoPath, +} from "../state"; +import { + approveUserPath, + getRecordingsDir, + getTelemetryPathForVideo, + isAutoRecordingPath, + normalizeVideoSourcePath, + normalizePath, + parseJsonWithByteOrderMark, +} from "../utils"; + +function normalizeRecordingTimeOffsetMs(value: unknown): number { + return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0; +} + +function normalizeBoolean(value: unknown, fallback = false): boolean { + return typeof value === "boolean" ? value : fallback; +} + +/** + * Produces a filesystem-safe project base name without the project extension. + */ +function normalizeProjectSaveName(projectName?: string | null) { + if (typeof projectName !== "string") { + return null; + } + + const trimmedName = projectName.trim(); + if (!trimmedName) { + return null; + } + + const withoutExtension = trimmedName.replace( + new RegExp(`\\.${PROJECT_FILE_EXTENSION}$`, "i"), + "", + ); + const withoutInvalidFilesystemChars = withoutExtension.replace(/[<>:"/\\|?*]/g, ""); + const withoutControlChars = Array.from(withoutInvalidFilesystemChars) + .filter((character) => character.charCodeAt(0) > 31) + .join(""); + const sanitizedName = withoutControlChars + .replace(/\s+/g, " ") + .replace(/[. ]+$/g, "") + .trim(); + + return sanitizedName || null; +} + +type NamedProjectSaveMode = "rename" | "copy"; + +function normalizeNamedProjectSaveMode(value: unknown): NamedProjectSaveMode { + return value === "copy" ? "copy" : "rename"; +} + +/** + * Extracts the persisted source video path from a saved project payload. + */ +function getProjectVideoPath(projectData: unknown) { + if (!projectData || typeof projectData !== "object") { + return null; + } + + const candidate = projectData as { videoPath?: unknown }; + return typeof candidate.videoPath === "string" ? candidate.videoPath : null; +} + +function getProjectId(projectData: unknown) { + if (!projectData || typeof projectData !== "object") { + return null; + } + + const candidate = projectData as { projectId?: unknown }; + return typeof candidate.projectId === "string" && candidate.projectId.trim().length > 0 + ? candidate.projectId + : null; +} + +function withProjectId(projectData: unknown, projectId: string) { + if (!projectData || typeof projectData !== "object" || Array.isArray(projectData)) { + return projectData; + } + + return { + ...projectData, + projectId, + }; +} + +function ensureProjectDataHasProjectId(projectData: unknown) { + const existingProjectId = getProjectId(projectData); + if (existingProjectId) { + return { + projectId: existingProjectId, + projectData, + }; + } + + const projectId = randomUUID(); + return { + projectId, + projectData: withProjectId(projectData, projectId), + }; +} + +async function resolveComparablePath(filePath: string) { + return fs.realpath(filePath).catch(() => path.resolve(filePath)); +} + +/** + * Prevents a named save from silently overwriting a different project file. + */ +async function ensureNamedProjectSaveDoesNotOverwriteDifferentProject( + targetProjectPath: string, + projectData: unknown, + activeProjectPath?: string | null, +) { + try { + await fs.stat(targetProjectPath); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { + return { success: true }; + } + throw error; + } + + const targetResolvedPath = await resolveComparablePath(targetProjectPath); + if (activeProjectPath) { + const activeResolvedPath = await resolveComparablePath(activeProjectPath); + if (activeResolvedPath === targetResolvedPath) { + return { success: true }; + } + } + + const incomingProjectId = getProjectId(projectData); + const incomingVideoPath = getProjectVideoPath(projectData); + + try { + const existingProjectRaw = await fs.readFile(targetProjectPath, "utf-8"); + const existingProjectData = parseJsonWithByteOrderMark(existingProjectRaw); + const existingProjectId = getProjectId(existingProjectData); + const existingVideoPath = getProjectVideoPath(existingProjectData); + + if (existingProjectId && incomingProjectId) { + if (existingProjectId === incomingProjectId) { + return { success: true }; + } + + return { + success: false, + message: "A different project already uses this name", + }; + } + + if (existingVideoPath && incomingVideoPath && existingVideoPath !== incomingVideoPath) { + return { + success: false, + message: "A different project already uses this name", + }; + } + + if (!existingProjectId && !incomingProjectId && existingVideoPath && incomingVideoPath) { + return { + success: false, + message: "Unable to verify project identity for the chosen name", + }; + } + + return { + success: false, + message: "Unable to verify project identity for the chosen name", + }; + } catch (error) { + console.error("Failed to verify existing named project before overwrite:", error); + return { + success: false, + message: "Unable to verify project identity for the chosen name", + }; + } +} + +export function registerProjectHandlers() { + ipcMain.handle("rename-library-project", async (_, source: string, name: string) => { + try { + const entries = await listProjectLibraryEntries(); + const target = await renameLibraryProject( + source, + name, + entries.entries.map((entry) => entry.path), + (value) => [getProjectThumbnailPath(value), getProjectBackupPath(value)], + ); + if (currentProjectPath && normalizePath(currentProjectPath) === normalizePath(source)) + setCurrentProjectPath(target); + const warning = await persistRecentMetadata(() => rememberRecentProject(target)); + return { success: true, path: target, warning }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + const imports = new Map(); + const pendingImports = new Map>(); + const watchedImportSenders = new WeakSet(); + const abandonImports = (owner: number) => { + imports.get(owner)?.abort(); + const outputs = pendingImports.get(owner); + pendingImports.delete(owner); + void Promise.allSettled([...(outputs ?? [])].map(discardRecordingImport)).then( + (results) => { + for (const result of results) + if (result.status === "rejected") + console.warn("Could not discard abandoned import", result.reason); + }, + ); + }; + ipcMain.handle("finish-recording-import", async (event, keepPath: string, commit = false) => { + if (imports.has(event.sender.id)) + return { success: false, error: "Import is still running" }; + const outputs = pendingImports.get(event.sender.id); + // Commit only after the renderer confirms that its project is still active. + if (commit) outputs?.delete(keepPath); + try { + for (const output of outputs ?? []) { + if (output === keepPath) continue; + await discardRecordingImport(output); + outputs?.delete(output); + } + if (!outputs?.size) pendingImports.delete(event.sender.id); + return { success: true }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + ipcMain.handle("cancel-recording-import", (event) => { + imports.get(event.sender.id)?.abort(); + return { success: true }; + }); + ipcMain.handle("get-recording-thumbnail", async (_, file: string) => { + try { + return { success: true, value: await getRecordingThumbnail(file) }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + ipcMain.handle("get-project-preview", async (_, projectPath: string) => { + try { + return { success: true, value: await readProjectPreview(projectPath) }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + ipcMain.handle("list-recordings", async (_, includeSources?: boolean) => { + try { + return { success: true, value: await listRecordings(includeSources === true) }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + ipcMain.handle("set-recordings-removed", async (_, paths: string[], removed: boolean) => { + try { + await setRecordingsRemoved(paths, removed); + return { success: true, value: null }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + ipcMain.handle( + "import-recording", + async ( + event, + currentPath: string, + recordingPath: string, + webcam?: import("../../../src/types/recordingLibrary").RecordingWebcamSource, + ) => { + const owner = event.sender.id; + if (!watchedImportSenders.has(event.sender)) { + watchedImportSenders.add(event.sender); + event.sender.once("destroyed", () => abandonImports(owner)); + event.sender.on("render-process-gone", () => abandonImports(owner)); + event.sender.on("did-start-navigation", (_event, _url, isInPlace, isMainFrame) => { + if (isMainFrame && !isInPlace) abandonImports(owner); + }); + } + if (imports.has(owner)) + return { success: false, error: "An import is already running" }; + const controller = new AbortController(); + imports.set(owner, controller); + try { + const value = await importRecording( + currentPath, + recordingPath, + webcam, + controller.signal, + ); + if (controller.signal.aborted || event.sender.isDestroyed()) { + await discardRecordingImport(value.path); + throw new Error("Import cancelled because its editor closed or reloaded"); + } + const outputs = pendingImports.get(owner) ?? new Set(); + outputs.add(value.path); + pendingImports.set(owner, outputs); + return { success: true, value }; + } catch (error) { + return { success: false, error: String(error) }; + } finally { + imports.delete(owner); + } + }, + ); + ipcMain.handle("reveal-in-folder", async (_, filePath: string) => { + try { + // shell.showItemInFolder doesn't return a value, it throws on error + shell.showItemInFolder(filePath); + return { success: true }; + } catch (error) { + console.error(`Error revealing item in folder: ${filePath}`, error); + // Fallback to open the directory if revealing the item fails + // This might happen if the file was moved or deleted after export, + // or if the path is somehow invalid for showItemInFolder + try { + const openPathResult = await shell.openPath(path.dirname(filePath)); + if (openPathResult) { + // openPath returned an error message + return { success: false, error: openPathResult }; + } + return { success: true, message: "Could not reveal item, but opened directory." }; + } catch (openError) { + console.error(`Error opening directory: ${path.dirname(filePath)}`, openError); + return { success: false, error: String(error) }; + } + } + }); + + ipcMain.handle("open-recordings-folder", async () => { + try { + const recordingsDir = await getRecordingsDir(); + const openPathResult = await shell.openPath(recordingsDir); + if (openPathResult) { + return { + success: false, + error: openPathResult, + message: "Failed to open recordings folder.", + }; + } + + return { success: true }; + } catch (error) { + console.error("Failed to open recordings folder:", error); + return { + success: false, + error: String(error), + message: "Failed to open recordings folder.", + }; + } + }); + + ipcMain.handle("get-recordings-directory", async () => { + try { + const recordingsDir = await getRecordingsDir(); + return { + success: true, + path: recordingsDir, + isDefault: recordingsDir === RECORDINGS_DIR, + }; + } catch (error) { + return { + success: false, + path: RECORDINGS_DIR, + isDefault: true, + error: String(error), + }; + } + }); + + ipcMain.handle("choose-recordings-directory", async () => { + try { + const current = await getRecordingsDir(); + const result = await dialog.showOpenDialog({ + title: "Choose recordings folder", + defaultPath: current, + properties: ["openDirectory", "createDirectory", "promptToCreate"], + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true, path: current }; + } + + const selectedPath = path.resolve(result.filePaths[0]); + await fs.mkdir(selectedPath, { recursive: true }); + await fs.access(selectedPath, fsConstants.W_OK); + await persistRecordingsDirectorySetting(selectedPath); + + return { + success: true, + path: selectedPath, + isDefault: selectedPath === RECORDINGS_DIR, + }; + } catch (error) { + return { + success: false, + error: String(error), + message: "Failed to set recordings folder", + }; + } + }); + + ipcMain.handle( + "save-project-file", + async ( + _, + projectData: unknown, + suggestedName?: string, + existingProjectPath?: string, + thumbnailDataUrl?: string | null, + ) => { + try { + const projectsDir = await getProjectsDir(); + const preparedProject = ensureProjectDataHasProjectId(projectData); + const trustedExistingProjectPath = + existingProjectPath && + path.extname(existingProjectPath).toLowerCase() === + `.${PROJECT_FILE_EXTENSION}` && + (isTrustedProjectPath(existingProjectPath) || + isPathInsideDirectory(existingProjectPath, projectsDir)) + ? path.resolve(existingProjectPath) + : null; + + if (trustedExistingProjectPath) { + await writeProjectFileAtomically( + trustedExistingProjectPath, + JSON.stringify(preparedProject.projectData, null, 2), + ); + setCurrentProjectPath(trustedExistingProjectPath); + await saveProjectThumbnail(trustedExistingProjectPath, thumbnailDataUrl); + await rememberRecentProject(trustedExistingProjectPath); + return { + success: true, + path: trustedExistingProjectPath, + projectId: preparedProject.projectId, + message: "Project saved successfully", + }; + } + + if (existingProjectPath) { + return { + success: false, + message: + "Project path is no longer trusted. Use Save As to choose a project file.", + }; + } + + const safeName = normalizeProjectSaveName(suggestedName) || `project-${Date.now()}`; + const defaultName = `${safeName}.${PROJECT_FILE_EXTENSION}`; + + const result = await dialog.showSaveDialog({ + title: "Save Recordly Project", + defaultPath: path.join(projectsDir, defaultName), + filters: [ + { name: "Recordly Project", extensions: [PROJECT_FILE_EXTENSION] }, + { name: "JSON", extensions: ["json"] }, + ], + properties: ["createDirectory", "showOverwriteConfirmation"], + }); + + if (result.canceled || !result.filePath) { + return { + success: false, + canceled: true, + message: "Save project canceled", + }; + } + + await writeProjectFileAtomically( + result.filePath, + JSON.stringify(preparedProject.projectData, null, 2), + ); + setCurrentProjectPath(result.filePath); + await saveProjectThumbnail(result.filePath, thumbnailDataUrl); + await rememberRecentProject(result.filePath); + + return { + success: true, + path: result.filePath, + projectId: preparedProject.projectId, + message: "Project saved successfully", + }; + } catch (error) { + console.error("Failed to save project file:", error); + return { + success: false, + message: "Failed to save project file", + error: String(error), + }; + } + }, + ); + + ipcMain.handle( + "save-project-file-named", + async ( + _, + projectData: unknown, + projectName: string, + thumbnailDataUrl?: string | null, + mode?: unknown, + ) => { + try { + const normalizedProjectName = normalizeProjectSaveName(projectName); + if (!normalizedProjectName) { + return { + success: false, + message: "Project name is required", + }; + } + + const projectsDir = await getProjectsDir(); + const namedSaveMode = normalizeNamedProjectSaveMode(mode); + const activeProjectPath = isTrustedProjectPath(currentProjectPath) + ? currentProjectPath + : null; + const targetProjectPath = path.join( + projectsDir, + `${normalizedProjectName}.${PROJECT_FILE_EXTENSION}`, + ); + const [activeResolvedPath, targetResolvedPath] = await Promise.all([ + activeProjectPath + ? resolveComparablePath(activeProjectPath) + : Promise.resolve(null), + resolveComparablePath(targetProjectPath), + ]); + const isSavingToDifferentPath = + !activeResolvedPath || activeResolvedPath !== targetResolvedPath; + const preparedProject = + namedSaveMode === "copy" && isSavingToDifferentPath + ? (() => { + const projectId = randomUUID(); + return { + projectId, + projectData: withProjectId(projectData, projectId), + }; + })() + : ensureProjectDataHasProjectId(projectData); + + const overwriteCheck = await ensureNamedProjectSaveDoesNotOverwriteDifferentProject( + targetProjectPath, + preparedProject.projectData, + activeProjectPath, + ); + if (!overwriteCheck.success) { + return overwriteCheck; + } + + await writeProjectFileAtomically( + targetProjectPath, + JSON.stringify(preparedProject.projectData, null, 2), + ); + await saveProjectThumbnail(targetProjectPath, thumbnailDataUrl); + await rememberRecentProject(targetProjectPath); + + if (namedSaveMode === "rename" && activeProjectPath && isSavingToDifferentPath) { + await fs + .unlink(activeProjectPath) + .catch((unlinkError: NodeJS.ErrnoException) => { + if (unlinkError.code !== "ENOENT") { + throw unlinkError; + } + }); + await fs + .rm(getProjectThumbnailPath(activeProjectPath), { force: true }) + .catch(() => undefined); + await fs + .rm(getProjectBackupPath(activeProjectPath), { force: true }) + .catch(() => undefined); + + const recentProjectPaths = await loadRecentProjectPaths(); + const filteredRecentProjectPaths: string[] = []; + for (const recentProjectPath of recentProjectPaths) { + const recentResolvedPath = await resolveComparablePath(recentProjectPath); + if (recentResolvedPath !== activeResolvedPath) { + filteredRecentProjectPaths.push(recentProjectPath); + } + } + await saveRecentProjectPaths(filteredRecentProjectPaths); + } + + setCurrentProjectPath(targetProjectPath); + + return { + success: true, + path: targetProjectPath, + projectId: preparedProject.projectId, + message: "Project saved successfully", + }; + } catch (error) { + console.error("Failed to save named project file:", error); + return { + success: false, + message: "Failed to save project file", + error: String(error), + }; + } + }, + ); + + ipcMain.handle("load-project-file", async () => { + try { + const projectsDir = await getProjectsDir(); + const result = await dialog.showOpenDialog({ + title: "Open Recordly Project", + defaultPath: projectsDir, + filters: [ + { + name: "Recordly Project", + extensions: [PROJECT_FILE_EXTENSION, ...LEGACY_PROJECT_FILE_EXTENSIONS], + }, + { name: "JSON", extensions: ["json"] }, + { name: "All Files", extensions: ["*"] }, + ], + properties: ["openFile"], + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true, message: "Open project canceled" }; + } + + return await loadProjectFromPath(result.filePaths[0]); + } catch (error) { + console.error("Failed to load project file:", error); + return { + success: false, + message: "Failed to load project file", + error: String(error), + }; + } + }); + + ipcMain.handle("load-current-project-file", async () => { + try { + if (!currentProjectPath) { + return { success: false, message: "No active project" }; + } + + return await loadProjectFromPath(currentProjectPath); + } catch (error) { + console.error("Failed to load current project file:", error); + return { + success: false, + message: "Failed to load current project file", + error: String(error), + }; + } + }); + + ipcMain.handle("get-projects-directory", async () => { + try { + return { + success: true, + path: await getProjectsDir(), + }; + } catch (error) { + return { + success: false, + error: String(error), + }; + } + }); + + ipcMain.handle( + "create-project-file", + async (_, projectData: unknown, thumbnailDataUrl?: string | null) => { + try { + const prepared = ensureProjectDataHasProjectId(projectData); + const target = await createUntitledProject( + await getProjectsDir(), + JSON.stringify(prepared.projectData, null, 2), + ); + setCurrentProjectPath(target); + const warning = await persistRecentMetadata(() => rememberRecentProject(target)); + try { + await saveProjectThumbnail(target, thumbnailDataUrl); + } catch (error) { + console.warn("Could not save project thumbnail", error); + } + return { success: true, path: target, projectId: prepared.projectId, warning }; + } catch (error) { + return { success: false, message: String(error) }; + } + }, + ); + ipcMain.handle("trash-project-files", async (_, paths: unknown) => { + try { + const result = await trashLibraryProjects(paths, { + list: listProjectLibraryEntries, + trash: (filePath) => shell.trashItem(filePath), + thumbnailPath: getProjectThumbnailPath, + }); + if (currentProjectPath && result.deleted.includes(path.resolve(currentProjectPath))) + setCurrentProjectPath(null); + const warning = await persistRecentMetadata(async () => + saveRecentProjectPaths( + (await loadRecentProjectPaths()).filter( + (p) => !result.deleted.includes(path.resolve(p)), + ), + ), + ); + return { success: result.errors.length === 0, ...result, warning }; + } catch (error) { + return { success: false, deleted: [], errors: [String(error)] }; + } + }); + + ipcMain.handle("list-project-files", async (event) => { + try { + const library = await listProjectLibraryEntries((ready) => { + if (!event.sender.isDestroyed()) + event.sender.send("project-library-thumbnail-ready", ready); + }); + return { + success: true, + projectsDir: library.projectsDir, + entries: library.entries, + }; + } catch (error) { + return { + success: false, + projectsDir: null, + entries: [], + error: String(error), + }; + } + }); + + ipcMain.handle("open-project-file-at-path", async (_, filePath: string) => { + try { + return await loadProjectFromPath(filePath); + } catch (error) { + console.error("Failed to open project file at path:", error); + return { + success: false, + message: "Failed to open project file", + error: String(error), + }; + } + }); + + ipcMain.handle("open-projects-directory", async () => { + try { + const projectsDir = await getProjectsDir(); + const openPathResult = await shell.openPath(projectsDir); + if (openPathResult) { + return { + success: false, + error: openPathResult, + message: "Failed to open projects folder.", + }; + } + + return { success: true, path: projectsDir }; + } catch (error) { + console.error("Failed to open projects folder:", error); + return { + success: false, + error: String(error), + message: "Failed to open projects folder.", + }; + } + }); + ipcMain.handle( + "set-current-video-path", + async ( + _, + path: string, + options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean }, + ) => { + setCurrentVideoPath(normalizeVideoSourcePath(path) ?? path); + approveUserPath(currentVideoPath); + const resolvedSession = (await resolveRecordingSession(currentVideoPath)) ?? { + videoPath: currentVideoPath!, + webcamPath: null, + timeOffsetMs: 0, + }; + + const nextSession = { + ...resolvedSession, + hideOverlayCursorByDefault: + normalizeBoolean(options?.hideOverlayCursorByDefault) || + normalizeBoolean(resolvedSession.hideOverlayCursorByDefault), + }; + + setCurrentRecordingSession(nextSession); + await replaceApprovedSessionLocalReadPaths([ + resolvedSession.videoPath, + resolvedSession.webcamPath, + ]); + + if (nextSession.webcamPath) { + await persistRecordingSessionManifest(nextSession); + } + + if (!options?.preserveProjectPath) { + setCurrentProjectPath(null); + } + + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed()) { + window.webContents.send("recording-session-changed", nextSession); + } + } + + return { success: true, webcamPath: nextSession.webcamPath ?? null }; + }, + ); + + ipcMain.handle( + "set-current-recording-session", + async ( + _, + session: { + videoPath: string; + webcamPath?: string | null; + timeOffsetMs?: number; + hideOverlayCursorByDefault?: boolean; + }, + options?: { preserveProjectPath?: boolean }, + ) => { + const normalizedVideoPath = + normalizeVideoSourcePath(session.videoPath) ?? session.videoPath; + setCurrentVideoPath(normalizedVideoPath); + setCurrentRecordingSession({ + videoPath: normalizedVideoPath, + webcamPath: normalizeVideoSourcePath(session.webcamPath ?? null), + timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs), + hideOverlayCursorByDefault: normalizeBoolean(session.hideOverlayCursorByDefault), + }); + await rememberApprovedLocalReadPath(currentRecordingSession!.videoPath); + await rememberApprovedLocalReadPath(currentRecordingSession!.webcamPath); + if (!options?.preserveProjectPath) { + setCurrentProjectPath(null); + } + await persistRecordingSessionManifest(currentRecordingSession!); + + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed()) { + window.webContents.send("recording-session-changed", currentRecordingSession); + } + } + + return { success: true }; + }, + ); + + ipcMain.handle("get-current-recording-session", () => { + if (!currentRecordingSession) { + return { success: false }; + } + + return { + success: true, + session: currentRecordingSession, + }; + }); + + ipcMain.handle("get-current-video-path", () => { + return currentVideoPath ? { success: true, path: currentVideoPath } : { success: false }; + }); + + ipcMain.handle("clear-current-video-path", () => { + setCurrentVideoPath(null); + setCurrentRecordingSession(null); + return { success: true }; + }); + + ipcMain.handle("delete-recording-file", async (_, filePath: string) => { + try { + if (!filePath) { + return { success: false, error: "Only auto-generated recordings can be deleted" }; + } + const resolvedPath = await fs.realpath(filePath).catch(() => path.resolve(filePath)); + const recordingsDirRaw = await getRecordingsDir(); + const recordingsDir = await fs + .realpath(recordingsDirRaw) + .catch(() => path.resolve(recordingsDirRaw)); + if ( + !isPathInsideDirectory(resolvedPath, recordingsDir) || + !isAutoRecordingPath(resolvedPath) + ) { + return { success: false, error: "Only auto-generated recordings can be deleted" }; + } + await fs.unlink(resolvedPath); + // Also delete the cursor telemetry sidecar if it exists + const telemetryPath = getTelemetryPathForVideo(resolvedPath); + await fs.unlink(telemetryPath).catch(() => undefined); + const currentResolved = currentVideoPath + ? await fs.realpath(currentVideoPath).catch(() => currentVideoPath) + : null; + if (currentResolved === resolvedPath) { + setCurrentVideoPath(null); + setCurrentRecordingSession(null); + } + return { success: true }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("get-local-media-url", async (_, filePath: string) => { + const baseUrl = getMediaServerBaseUrl(); + if (!baseUrl || !filePath) { + return { success: false as const }; + } + const resolved = await resolveApprovedLocalMediaPath(filePath); + if (!resolved) { + const normalized = path.resolve(filePath); + console.warn(`[get-local-media-url] Blocked disallowed path: ${normalized}`); + return { success: false as const }; + } + return { success: true as const, url: buildMediaUrl(baseUrl, resolved) }; + }); +} diff --git a/electron/ipc/register/recording.ts b/electron/ipc/register/recording.ts new file mode 100644 index 000000000..faef32d6e --- /dev/null +++ b/electron/ipc/register/recording.ts @@ -0,0 +1,2090 @@ +import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import { execFile, spawn } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { + app, + BrowserWindow, + desktopCapturer, + dialog, + ipcMain, + shell, + systemPreferences, +} from "electron"; +import { getHudCaptureExcludedProcessIds } from "../../../src/lib/hudCaptureProtection"; +import { showCursor } from "../../cursorHider"; +import { + beginHudCaptureProtection, + getHudOverlayCaptureProtectionEnabled, + hideAreaRecordingBorder, + showAreaRecordingBorder, +} from "../../windows"; +import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants"; +import { startWindowBoundsCapture, stopWindowBoundsCapture } from "../cursor/bounds"; +import { startInteractionCapture, stopInteractionCapture } from "../cursor/interaction"; +import { startNativeCursorMonitor, stopNativeCursorMonitor } from "../cursor/monitor"; +import { + normalizeCursorTelemetrySamples, + pauseCursorCaptureAtBoundary, + persistPendingCursorTelemetry, + resetCursorCaptureClock, + resumeCursorCapture, + sampleCursorPoint, + snapshotCursorTelemetryForPersistence, + startCursorSampling, + stopCursorCapture, + writeCursorTelemetry, +} from "../cursor/telemetry"; +import { getFfmpegBinaryPath } from "../ffmpeg/binary"; +import { getMonitorHandles } from "../monitorResolver"; +import { + ensureNativeCaptureHelperBinary, + ensureSwiftHelperBinary, + getNativeCaptureHelperBinaryPath, + getSystemCursorHelperBinaryPath, + getSystemCursorHelperSourcePath, + getWindowsCaptureExePath, +} from "../paths/binaries"; +import { rememberApprovedLocalReadPath } from "../project/manager"; +import { + getBrowserMicSidecarFilters, + shouldKeepRecordingAudioSidecars, +} from "../recording/audioFilters"; +import { + getCompanionAudioFallbackInfo, + getFileSizeIfPresent, + getRecordingDiagnosticsPath, + type MicrophoneChunkTimingEvent, + type MicrophonePauseInterval, + type RecordingDiagnosticsSnapshot, + recordNativeCaptureDiagnostics, + summarizeMicrophoneChunkTiming, + validateRecordedVideo, + writeRecordingDiagnosticsSnapshot, +} from "../recording/diagnostics"; +import { + buildFfmpegCaptureArgs, + waitForFfmpegCaptureStart, + waitForFfmpegCaptureStop, +} from "../recording/ffmpeg"; +import { + attachNativeCaptureLifecycle, + finalizeStoredVideo, + muxNativeMacRecordingWithAudio, + recoverNativeMacCaptureOutput, + waitForNativeCaptureCommand, + waitForNativeCaptureStart, + waitForNativeCaptureStop, +} from "../recording/mac"; +import { + createHudCrashRecoveryDeps, + createStartTracker, + stopRecordingWhenHudGoes, +} from "../recording/hudCrashRecovery"; +import { resolveRecordedVideoStoragePath } from "../recording/storagePath"; +import { + attachWindowsCaptureLifecycle, + isNativeWindowsCaptureAvailable, + muxNativeWindowsVideoWithAudio, + waitForWindowsCaptureStart, + waitForWindowsCaptureStop, +} from "../recording/windows"; +import { + shouldStartWindowsBrowserMicrophoneFallback, + shouldUseWindowsBrowserMicrophoneFallback, +} from "../recording/windowsFallbacks"; +import { + cachedSystemCursorAssets, + cachedSystemCursorAssetsSourceMtimeMs, + currentVideoPath, + ffmpegCaptureOutputBuffer, + ffmpegCaptureProcess, + ffmpegCaptureTargetPath, + ffmpegScreenRecordingActive, + lastNativeCaptureDiagnostics, + nativeCaptureMicrophonePath, + nativeCaptureOutputBuffer, + nativeCapturePaused, + nativeCaptureProcess, + nativeCaptureStopRequested, + nativeCaptureSystemAudioPath, + nativeCaptureTargetPath, + nativeScreenRecordingActive, + selectedSource, + setActiveCursorSamples, + setCachedSystemCursorAssets, + setCachedSystemCursorAssetsSourceMtimeMs, + setCursorCaptureStartTimeMs, + setFfmpegCaptureOutputBuffer, + setFfmpegCaptureProcess, + setFfmpegCaptureTargetPath, + setFfmpegScreenRecordingActive, + setIsCursorCaptureActive, + setLastLeftClick, + setLinuxCursorScreenPoint, + setNativeCaptureMicrophonePath, + setNativeCaptureOutputBuffer, + setNativeCapturePaused, + setNativeCaptureProcess, + setNativeCaptureStopRequested, + setNativeCaptureSystemAudioPath, + setNativeCaptureTargetPath, + setNativeScreenRecordingActive, + setPendingCursorSamples, + setWindowsCaptureOutputBuffer, + setWindowsCapturePaused, + setWindowsCaptureProcess, + setWindowsCaptureStopRequested, + setWindowsCaptureTargetPath, + setWindowsMicAudioPath, + setWindowsNativeCaptureActive, + setWindowsOrphanedMicAudioPath, + setWindowsPendingVideoPath, + setWindowsSystemAudioPath, + windowsCaptureOutputBuffer, + windowsCapturePaused, + windowsCaptureProcess, + windowsCaptureStopRequested, + windowsCaptureTargetPath, + windowsMicAudioPath, + windowsNativeCaptureActive, + windowsOrphanedMicAudioPath, + windowsPendingVideoPath, + windowsSystemAudioPath, +} from "../state"; +import type { CursorTelemetryPoint, NativeMacRecordingOptions, SelectedSource } from "../types"; +import { + getMacPrivacySettingsUrl, + getRecordingsDir, + getScreen, + getTelemetryPathForVideo, + moveFileWithOverwrite, + normalizeVideoSourcePath, + parseJsonWithByteOrderMark, + parseWindowId, +} from "../utils"; +import { resolveWindowsCaptureTarget } from "../windowsCaptureSelection"; +import { getSourceArea } from "../sourceArea"; +import { bringSelectedWindowForward } from "./sources"; + +const execFileAsync = promisify(execFile); + +async function writeWindowsRecordingDiagnostics( + videoPath: string | null | undefined, + snapshot: Omit, +) { + if (!videoPath) { + return null; + } + + try { + return await writeRecordingDiagnosticsSnapshot(videoPath, { + backend: "windows-wgc", + ...snapshot, + }); + } catch (error) { + console.warn("Failed to write Windows recording diagnostics:", error); + return null; + } +} + +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +function pickPrimitiveRecord(value: unknown) { + if (!isRecord(value)) { + return null; + } + + const entries = Object.entries(value).filter( + (entry): entry is [string, boolean | number | string] => { + const primitive = entry[1]; + return ( + typeof primitive === "boolean" || + typeof primitive === "number" || + typeof primitive === "string" + ); + }, + ); + + return entries.length > 0 ? Object.fromEntries(entries) : null; +} + +function normalizeRendererTimestampMs(value: unknown) { + const nowMs = Date.now(); + if (typeof value !== "number" || !Number.isFinite(value)) { + return nowMs; + } + + return Math.min(Math.max(0, Math.round(value)), nowMs); +} + +function pickMicrophoneChunkEvents(value: unknown): MicrophoneChunkTimingEvent[] | null { + if (!Array.isArray(value)) { + return null; + } + + const events = value + .map((event) => { + if (!isRecord(event)) { + return null; + } + + const { index, size, elapsedMs, deltaMs, recordedElapsedMs, recordedDeltaMs } = event; + if ( + typeof index !== "number" || + !Number.isFinite(index) || + index < 0 || + typeof size !== "number" || + !Number.isFinite(size) || + size < 0 || + typeof elapsedMs !== "number" || + !Number.isFinite(elapsedMs) || + elapsedMs < 0 + ) { + return null; + } + + return { + index: Math.round(index), + size: Math.round(size), + elapsedMs: Math.round(elapsedMs), + deltaMs: + typeof deltaMs === "number" && Number.isFinite(deltaMs) + ? Math.max(0, Math.round(deltaMs)) + : null, + ...(typeof recordedElapsedMs === "number" && + Number.isFinite(recordedElapsedMs) && + recordedElapsedMs >= 0 + ? { recordedElapsedMs: Math.round(recordedElapsedMs) } + : {}), + recordedDeltaMs: + typeof recordedDeltaMs === "number" && Number.isFinite(recordedDeltaMs) + ? Math.max(0, Math.round(recordedDeltaMs)) + : null, + }; + }) + .filter((event): event is NonNullable => event !== null); + + return events.length > 0 ? events : null; +} + +function pickMicrophonePauseIntervals(value: unknown): MicrophonePauseInterval[] | null { + if (!Array.isArray(value)) { + return null; + } + + const intervals = value + .map((interval) => { + if ( + !isRecord(interval) || + typeof interval.startElapsedMs !== "number" || + !Number.isFinite(interval.startElapsedMs) || + interval.startElapsedMs < 0 + ) { + return null; + } + + const startElapsedMs = Math.max(0, Math.round(interval.startElapsedMs)); + return { + startElapsedMs, + ...(typeof interval.endElapsedMs === "number" && + Number.isFinite(interval.endElapsedMs) && + interval.endElapsedMs >= startElapsedMs + ? { endElapsedMs: Math.round(interval.endElapsedMs) } + : {}), + ...(typeof interval.durationMs === "number" && + Number.isFinite(interval.durationMs) && + interval.durationMs >= 0 + ? { durationMs: Math.round(interval.durationMs) } + : {}), + }; + }) + .filter((interval): interval is NonNullable => interval !== null); + + return intervals.length > 0 ? intervals : null; +} + +function pickAudioInputDevices(value: unknown) { + if (!Array.isArray(value)) { + return null; + } + + const devices = value + .map((device) => { + if (!isRecord(device) || typeof device.deviceId !== "string") { + return null; + } + + return { + deviceId: device.deviceId, + ...(typeof device.groupId === "string" ? { groupId: device.groupId } : {}), + label: typeof device.label === "string" ? device.label : "", + }; + }) + .filter((device): device is NonNullable => device !== null); + + return devices.length > 0 ? devices : null; +} + +async function getSystemCursorAssets() { + if (process.platform !== "darwin") { + setCachedSystemCursorAssets({}); + setCachedSystemCursorAssetsSourceMtimeMs(null); + return cachedSystemCursorAssets ?? {}; + } + const sourcePath = getSystemCursorHelperSourcePath(); + const sourceStat = await fs.stat(sourcePath); + if (cachedSystemCursorAssets && cachedSystemCursorAssetsSourceMtimeMs === sourceStat.mtimeMs) { + return cachedSystemCursorAssets; + } + const binaryPath = await ensureSwiftHelperBinary( + sourcePath, + getSystemCursorHelperBinaryPath(), + "system cursor helper", + "recordly-system-cursors", + ); + const { stdout } = await execFileAsync(binaryPath, [], { + timeout: 15000, + maxBuffer: 20 * 1024 * 1024, + }); + const parsed = JSON.parse(stdout) as Record< + string, + Partial + >; + const result = Object.fromEntries( + Object.entries(parsed).filter( + ([, asset]) => + typeof asset?.dataUrl === "string" && + typeof asset?.hotspotX === "number" && + typeof asset?.hotspotY === "number" && + typeof asset?.width === "number" && + typeof asset?.height === "number", + ), + ) as Record; + setCachedSystemCursorAssets(result); + setCachedSystemCursorAssetsSourceMtimeMs(sourceStat.mtimeMs); + return result; +} + +function normalizeDesktopSourceName(value: string) { + return value.trim().replace(/\s+/g, " ").toLowerCase(); +} + +async function cleanupWindowsOrphanedMicAudioPath(filePath: string | null) { + if (!filePath) { + return; + } + + if (shouldKeepRecordingAudioSidecars()) { + console.log(`[recording] Keeping orphaned native mic sidecar for diagnostics: ${filePath}`); + return; + } + + await fs.rm(filePath, { force: true }).catch(() => undefined); +} + +async function pathExists(filePath: string | null | undefined) { + if (!filePath) { + return false; + } + + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function resolveExistingPath(...candidates: Array) { + for (const candidate of candidates) { + if (await pathExists(candidate)) { + return candidate ?? null; + } + } + + return null; +} + +async function reportRecordingRecoveredAfterHudCrash( + result: { success: boolean; path?: string; message?: string; error?: string }, + reason: string, +) { + if (result.success && result.path) { + const { response } = await dialog.showMessageBox({ + type: "warning", + title: "Recording Stopped", + message: "The recording controls stopped working, so Recordly saved the recording.", + detail: `The recording was stopped and saved to:\n${result.path}\n\n(Renderer exit reason: ${reason})`, + buttons: [process.platform === "darwin" ? "Show in Finder" : "Show in Folder", "OK"], + defaultId: 0, + cancelId: 1, + }); + if (response === 0) { + shell.showItemInFolder(result.path); + } + return; + } + await dialog.showMessageBox({ + type: "error", + title: "Recording Stopped", + message: + "The recording controls stopped working, and Recordly could not save the recording.", + detail: `${result.error ?? result.message ?? "Unknown error"}\n\n(Renderer exit reason: ${reason})`, + }); +} + +export function registerRecordingHandlers( + onRecordingStateChange?: (recording: boolean, sourceName: string) => void, +) { + // True after set-recording-state(true): a recording the user sees, not a countdown. + let userRecordingActive = false; + let lastStopEndedUserRecording = false; + let stoppedCountdownPath: string | null = null; + // Every start-native-screen-recording call still running, so recovery can wait for them. + const starts = createStartTracker(); + const trackStart = starts.track; + const hudCrashRecoveryDeps = createHudCrashRecoveryDeps({ + captureState: () => ({ + nativeActive: nativeScreenRecordingActive, + nativeStopRequested: nativeCaptureStopRequested, + windowsActive: windowsNativeCaptureActive, + windowsStopRequested: windowsCaptureStopRequested, + userRecording: userRecordingActive, + lastStopEndedUserRecording, + }), + startSettled: starts.settled, + stopNativeScreenRecording, + muxNativeWindowsRecording: () => muxNativeWindowsRecording(), + clearWindowsPendingVideo: () => setWindowsPendingVideoPath(null), + windowsPendingVideoPath: () => windowsPendingVideoPath, + stoppedCountdownPath: () => stoppedCountdownPath, + clearStoppedCountdownPath: () => { + stoppedCountdownPath = null; + }, + fileExists: (filePath) => + fs.access(filePath).then( + () => true, + () => false, + ), + deleteFile: (filePath) => fs.rm(filePath, { force: true }), + telemetryPathFor: getTelemetryPathForVideo, + diagnosticsPathFor: getRecordingDiagnosticsPath, + setRecordingState, + reportRecovered: reportRecordingRecoveredAfterHudCrash, + }); + + ipcMain.handle( + "start-native-screen-recording", + trackStart(async (event, source: SelectedSource, options?: NativeMacRecordingOptions) => { + // If this renderer dies mid-recording, nothing else can press Stop (#1139). + stopRecordingWhenHudGoes(event.sender, hudCrashRecoveryDeps); + stoppedCountdownPath = null; + // Protect the HUD at the capture boundary, before the renderer publishes recording state. + beginHudCaptureProtection(); + const visibleWindowBounds = source.id?.startsWith("window:") + ? await bringSelectedWindowForward(source) + : null; + + // Windows native capture path + if (process.platform === "win32") { + const windowsCaptureAvailable = await isNativeWindowsCaptureAvailable(); + if (!windowsCaptureAvailable) { + return { + success: false, + message: "Native Windows capture is not available on this system.", + }; + } + + if (windowsCaptureProcess && !windowsNativeCaptureActive) { + try { + windowsCaptureProcess.kill(); + } catch { + /* ignore */ + } + setWindowsCaptureProcess(null); + setWindowsCaptureTargetPath(null); + setWindowsCaptureStopRequested(false); + } + + if (windowsCaptureProcess) { + return { + success: false, + message: "A native Windows screen recording is already active.", + }; + } + + let wcProc: ChildProcessWithoutNullStreams | null = null; + let tempVideoPath: string | null = null; + let tempSystemAudioPath: string | null = null; + let tempMicPath: string | null = null; + try { + const exePath = getWindowsCaptureExePath(); + const recordingsDir = await getRecordingsDir(); + const timestamp = Date.now(); + const outputPath = path.join(recordingsDir, `recording-${timestamp}.mp4`); + tempVideoPath = path.join( + app.getPath("temp"), + `recordly-native-${timestamp}.mp4`, + ); + + let captureOutput = ""; + let systemAudioPath: string | null = null; + let microphonePath: string | null = null; + let orphanedMicAudioPath: string | null = null; + + const browserMicFallbackRequested = + shouldStartWindowsBrowserMicrophoneFallback(options); + const captureTarget = resolveWindowsCaptureTarget( + source, + getScreen().getAllDisplays(), + getScreen().getPrimaryDisplay(), + ); + const displayBounds = + captureTarget.kind === "display" ? captureTarget.bounds : null; + setWindowsOrphanedMicAudioPath(null); + + const config: Record = { + outputPath: tempVideoPath, + fps: 60, + }; + + if (captureTarget.kind === "invalid-window") { + return { + success: false, + message: + "Selected window is no longer available. Please choose the window again.", + }; + } + + if (captureTarget.kind === "window") { + config.windowHandle = captureTarget.windowHandle; + } else { + // Windows Graphics Capture (WGC) requires a raw HMONITOR handle. + // We attempt to resolve the handle by matching the physical coordinates of the target display. + const monitors = getMonitorHandles(); + const matchedMonitor = monitors.find( + (monitor) => + monitor.x === Math.round(captureTarget.bounds.x) && + monitor.y === Math.round(captureTarget.bounds.y), + ); + + if (matchedMonitor) { + config.displayId = matchedMonitor.handle; + } else { + // Fallback to coordinate-based matching if handle resolution fails + config.displayId = captureTarget.displayId; + } + + config.displayX = Math.round(captureTarget.bounds.x); + config.displayY = Math.round(captureTarget.bounds.y); + config.displayW = Math.round(captureTarget.bounds.width); + config.displayH = Math.round(captureTarget.bounds.height); + } + + if (options?.capturesSystemAudio) { + systemAudioPath = path.join( + recordingsDir, + `recording-${timestamp}.system.wav`, + ); + tempSystemAudioPath = path.join( + app.getPath("temp"), + `recordly-native-${timestamp}.system.wav`, + ); + config.captureSystemAudio = true; + config.audioOutputPath = tempSystemAudioPath; + setWindowsSystemAudioPath(systemAudioPath); + } else { + setWindowsSystemAudioPath(null); + } + + if (options?.capturesMicrophone && !browserMicFallbackRequested) { + microphonePath = path.join(recordingsDir, `recording-${timestamp}.mic.wav`); + tempMicPath = path.join( + app.getPath("temp"), + `recordly-native-${timestamp}.mic.wav`, + ); + config.captureMic = true; + config.micOutputPath = tempMicPath; + if (options.microphoneDeviceId) { + config.micDeviceId = options.microphoneDeviceId; + } + if (options.microphoneLabel) { + config.micDeviceName = options.microphoneLabel; + } + setWindowsMicAudioPath(microphonePath); + } else if (browserMicFallbackRequested) { + config.captureMic = false; + setWindowsMicAudioPath(null); + } else { + setWindowsMicAudioPath(null); + } + + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "start", + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? "unknown", + displayId: typeof config.displayId === "number" ? config.displayId : null, + displayBounds, + windowHandle: + typeof config.windowHandle === "number" ? config.windowHandle : null, + helperPath: exePath, + outputPath, + systemAudioPath, + microphonePath, + }); + + setWindowsCaptureOutputBuffer(""); + setWindowsCaptureTargetPath(outputPath); + setWindowsCaptureStopRequested(false); + setWindowsCapturePaused(false); + + wcProc = spawn(exePath, [JSON.stringify(config)], { + cwd: recordingsDir, + stdio: ["pipe", "pipe", "pipe"], + env: process.env, + }); + setWindowsCaptureProcess(wcProc); + attachWindowsCaptureLifecycle(wcProc); + + wcProc.stdout.on("data", (chunk: Buffer) => { + const msg = chunk.toString(); + captureOutput += msg; + setWindowsCaptureOutputBuffer(captureOutput); + }); + wcProc.stderr.on("data", (chunk: Buffer) => { + const msg = chunk.toString(); + captureOutput += msg; + setWindowsCaptureOutputBuffer(captureOutput); + }); + + await waitForWindowsCaptureStart(wcProc); + const microphoneFallbackRequired = + browserMicFallbackRequested || + shouldUseWindowsBrowserMicrophoneFallback(captureOutput, options); + if (microphoneFallbackRequired) { + orphanedMicAudioPath = tempMicPath ?? microphonePath; + setWindowsOrphanedMicAudioPath(orphanedMicAudioPath); + microphonePath = null; + setWindowsMicAudioPath(null); + } + setWindowsNativeCaptureActive(true); + setNativeScreenRecordingActive(true); + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "start", + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? "unknown", + displayId: typeof config.displayId === "number" ? config.displayId : null, + displayBounds, + windowHandle: + typeof config.windowHandle === "number" ? config.windowHandle : null, + helperPath: exePath, + outputPath, + systemAudioPath, + microphonePath, + processOutput: captureOutput.trim() || undefined, + }); + return { success: true, microphoneFallbackRequired }; + } catch (error) { + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "start", + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? "unknown", + helperPath: windowsCaptureTargetPath ? getWindowsCaptureExePath() : null, + outputPath: windowsCaptureTargetPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + error: String(error), + }); + console.error("Failed to start native Windows capture:", error); + try { + if (wcProc) wcProc.kill(); + } catch { + /* ignore */ + } + await Promise.allSettled([ + tempVideoPath + ? fs.rm(tempVideoPath, { force: true }).catch(() => undefined) + : Promise.resolve(), + tempSystemAudioPath + ? fs.rm(tempSystemAudioPath, { force: true }).catch(() => undefined) + : Promise.resolve(), + tempMicPath + ? fs.rm(tempMicPath, { force: true }).catch(() => undefined) + : Promise.resolve(), + ]); + setWindowsNativeCaptureActive(false); + setNativeScreenRecordingActive(false); + setWindowsCaptureProcess(null); + setWindowsCaptureTargetPath(null); + setWindowsSystemAudioPath(null); + setWindowsMicAudioPath(null); + setWindowsOrphanedMicAudioPath(null); + setWindowsCaptureStopRequested(false); + setWindowsCapturePaused(false); + return { + success: false, + message: "Failed to start native Windows capture", + error: String(error), + }; + } + } + + if (process.platform !== "darwin") { + return { + success: false, + message: "Native screen recording is only available on macOS.", + }; + } + + if (nativeCaptureProcess && !nativeScreenRecordingActive) { + try { + nativeCaptureProcess.kill(); + } catch { + // ignore stale helper cleanup failures + } + setNativeCaptureProcess(null); + setNativeCaptureTargetPath(null); + setNativeCaptureStopRequested(false); + } + + if (nativeCaptureProcess) { + return { success: false, message: "A native screen recording is already active." }; + } + + let captProc: ChildProcessWithoutNullStreams | null = null; + try { + const recordingsDir = await getRecordingsDir(); + + // Warm up TCC: trigger an Electron-level screen capture API call so macOS + // activates the screen-recording grant for this process tree before the + // native helper binary spawns and calls SCStream.startCapture(). + try { + await desktopCapturer.getSources({ + types: ["screen"], + thumbnailSize: { width: 1, height: 1 }, + }); + } catch { + // non-fatal – the helper will report its own TCC status + } + + // Ensure microphone TCC is granted for this process tree when mic capture + // is requested, so the child helper inherits the grant. + if (options?.capturesMicrophone) { + const micStatus = systemPreferences.getMediaAccessStatus("microphone"); + if (micStatus !== "granted") { + await systemPreferences.askForMediaAccess("microphone"); + } + } + + const appName = normalizeDesktopSourceName(String(source?.appName ?? "")); + const ownAppName = normalizeDesktopSourceName(app.getName()); + if ( + !ALLOW_RECORDLY_WINDOW_CAPTURE && + source?.id?.startsWith("window:") && + appName && + (appName === ownAppName || appName === "recordly") + ) { + return { + success: false, + message: + "Cannot record Recordly windows. Please select another app window.", + }; + } + + const helperPath = await ensureNativeCaptureHelperBinary(); + const timestamp = Date.now(); + const outputPath = path.join(recordingsDir, `recording-${timestamp}.mp4`); + const capturesSystemAudio = Boolean(options?.capturesSystemAudio); + const capturesMicrophone = Boolean(options?.capturesMicrophone); + const systemAudioOutputPath = capturesSystemAudio + ? path.join(recordingsDir, `recording-${timestamp}.system.m4a`) + : null; + const microphoneOutputPath = capturesMicrophone + ? path.join(recordingsDir, `recording-${timestamp}.mic.m4a`) + : null; + const config: Record = { + fps: 60, + outputPath, + capturesSystemAudio, + capturesMicrophone, + }; + + const excludedProcessIds = getHudCaptureExcludedProcessIds( + process.platform, + getHudOverlayCaptureProtectionEnabled(), + process.pid, + ); + if (excludedProcessIds.length > 0) { + config.excludedProcessIds = excludedProcessIds; + } + + if (options?.microphoneDeviceId) { + config.microphoneDeviceId = options.microphoneDeviceId; + } + + if (options?.microphoneLabel) { + config.microphoneLabel = options.microphoneLabel; + } + + if (systemAudioOutputPath) { + config.systemAudioOutputPath = systemAudioOutputPath; + } + + if (microphoneOutputPath) { + config.microphoneOutputPath = microphoneOutputPath; + } + + const windowId = parseWindowId(source?.id); + const screenId = Number(source?.display_id); + const area = getSourceArea(source); + + if (area) { + config.regionX = area.x; + config.regionY = area.y; + config.regionWidth = area.width; + config.regionHeight = area.height; + if (Number.isFinite(screenId) && screenId > 0) { + config.displayId = screenId; + } + } else if ( + Number.isFinite(windowId) && + windowId && + source?.id?.startsWith("window:") + ) { + config.windowId = windowId; + if (visibleWindowBounds) { + config.windowX = visibleWindowBounds.x; + config.windowY = visibleWindowBounds.y; + config.windowWidth = visibleWindowBounds.width; + config.windowHeight = visibleWindowBounds.height; + } + } else if (Number.isFinite(screenId) && screenId > 0) { + config.displayId = screenId; + } else { + config.displayId = Number(getScreen().getPrimaryDisplay().id); + } + + setNativeCaptureOutputBuffer(""); + setNativeCaptureTargetPath(outputPath); + setNativeCaptureSystemAudioPath(systemAudioOutputPath); + setNativeCaptureMicrophonePath(microphoneOutputPath); + setNativeCaptureStopRequested(false); + setNativeCapturePaused(false); + captProc = spawn(helperPath, [JSON.stringify(config)], { + cwd: recordingsDir, + stdio: ["pipe", "pipe", "pipe"], + }); + setNativeCaptureProcess(captProc); + attachNativeCaptureLifecycle(captProc); + + captProc.stdout.on("data", (chunk: Buffer) => { + setNativeCaptureOutputBuffer(nativeCaptureOutputBuffer + chunk.toString()); + }); + captProc.stderr.on("data", (chunk: Buffer) => { + setNativeCaptureOutputBuffer(nativeCaptureOutputBuffer + chunk.toString()); + }); + + await waitForNativeCaptureStart(captProc); + setNativeScreenRecordingActive(true); + + // If the native helper reported MICROPHONE_CAPTURE_UNAVAILABLE, it started + // capture without microphone. Clear the mic path so the renderer can fall + // back to a browser-side sidecar recording for the microphone track. + const micUnavailableNatively = nativeCaptureOutputBuffer.includes( + "MICROPHONE_CAPTURE_UNAVAILABLE", + ); + if (micUnavailableNatively) { + setNativeCaptureMicrophonePath(null); + } + + recordNativeCaptureDiagnostics({ + backend: "mac-screencapturekit", + phase: "start", + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? "unknown", + displayId: typeof config.displayId === "number" ? config.displayId : null, + helperPath, + outputPath, + systemAudioPath: systemAudioOutputPath, + microphonePath: nativeCaptureMicrophonePath, + processOutput: nativeCaptureOutputBuffer.trim() || undefined, + }); + return { + success: true, + microphoneFallbackRequired: micUnavailableNatively, + }; + } catch (error) { + console.error("Failed to start native ScreenCaptureKit recording:", error); + const errorStr = String(error); + + // Detect TCC (screen recording permission) errors and show a helpful dialog + if ( + errorStr.includes("declined TCC") || + errorStr.includes("declined TCCs") || + errorStr.includes("SCREEN_RECORDING_PERMISSION_DENIED") + ) { + const { response } = await dialog.showMessageBox({ + type: "warning", + title: "Screen Recording Permission Required", + message: + "Recordly needs screen recording permission to capture your screen.", + detail: "Please open System Settings > Privacy & Security > Screen Recording, make sure Recordly is toggled ON, then try recording again.", + buttons: ["Open System Settings", "Cancel"], + defaultId: 0, + cancelId: 1, + }); + if (response === 0) { + await shell.openExternal(getMacPrivacySettingsUrl("screen")); + } + try { + if (captProc) captProc.kill(); + } catch { + /* ignore */ + } + setNativeScreenRecordingActive(false); + setNativeCaptureProcess(null); + setNativeCaptureTargetPath(null); + setNativeCaptureSystemAudioPath(null); + setNativeCaptureMicrophonePath(null); + setNativeCaptureStopRequested(false); + setNativeCapturePaused(false); + return { + success: false, + message: + "Screen recording permission not granted. Please allow access in System Settings and restart the app.", + userNotified: true, + }; + } + + if (errorStr.includes("MICROPHONE_PERMISSION_DENIED")) { + const { response } = await dialog.showMessageBox({ + type: "warning", + title: "Microphone Permission Required", + message: "Recordly needs microphone permission to record audio.", + detail: "Please open System Settings > Privacy & Security > Microphone, make sure Recordly is toggled ON, then try recording again.", + buttons: ["Open System Settings", "Cancel"], + defaultId: 0, + cancelId: 1, + }); + if (response === 0) { + await shell.openExternal(getMacPrivacySettingsUrl("microphone")); + } + try { + if (captProc) captProc.kill(); + } catch { + /* ignore */ + } + setNativeScreenRecordingActive(false); + setNativeCaptureProcess(null); + setNativeCaptureTargetPath(null); + setNativeCaptureSystemAudioPath(null); + setNativeCaptureMicrophonePath(null); + setNativeCaptureStopRequested(false); + setNativeCapturePaused(false); + return { + success: false, + message: + "Microphone permission not granted. Please allow access in System Settings.", + userNotified: true, + }; + } + + recordNativeCaptureDiagnostics({ + backend: "mac-screencapturekit", + phase: "start", + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? "unknown", + helperPath: getNativeCaptureHelperBinaryPath(), + outputPath: nativeCaptureTargetPath, + systemAudioPath: nativeCaptureSystemAudioPath, + microphonePath: nativeCaptureMicrophonePath, + processOutput: nativeCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: await getFileSizeIfPresent(nativeCaptureTargetPath), + error: String(error), + }); + try { + if (captProc) captProc.kill(); + } catch { + // ignore cleanup failures + } + setNativeScreenRecordingActive(false); + setNativeCaptureProcess(null); + setNativeCaptureTargetPath(null); + setNativeCaptureSystemAudioPath(null); + setNativeCaptureMicrophonePath(null); + setNativeCaptureStopRequested(false); + setNativeCapturePaused(false); + return { + success: false, + message: "Failed to start native ScreenCaptureKit recording", + error: String(error), + }; + } + }), + ); + + async function stopNativeScreenRecording() { + // The renderer leaves the recording state only after this stop, so this tells a + // recording from a countdown capture if its HUD dies before the Windows mux. + lastStopEndedUserRecording = userRecordingActive; + const start = Date.now(); + console.log("[PERF:MAIN] Handler: stop-native-screen-recording: STARTED"); + try { + // Windows native capture stop path + if (process.platform === "win32" && windowsNativeCaptureActive) { + let stagedTempVideoPath: string | null = null; + let stagedTempSystemAudioPath: string | null = null; + let stagedTempMicAudioPath: string | null = null; + try { + if (!windowsCaptureProcess) { + throw new Error("Native Windows capture process is not running"); + } + + const proc = windowsCaptureProcess; + const preferredVideoPath = windowsCaptureTargetPath; + const preferredOrphanedMicAudioPath = windowsOrphanedMicAudioPath; + const diagnosticsSystemAudioPath = windowsSystemAudioPath; + const diagnosticsMicAudioPath = windowsMicAudioPath; + setWindowsCaptureStopRequested(true); + proc.stdin.write("stop\n"); + const tempVideoPath = await waitForWindowsCaptureStop(proc); + stagedTempVideoPath = tempVideoPath; + const finalVideoPath = preferredVideoPath ?? tempVideoPath; + + // Native Windows capture results are initially written to a safe temporary path + // (to avoid encoding failures with non-ASCII characters). We move them to the final + // destination now using Node.js, which handles Unicode paths correctly. + if (tempVideoPath !== finalVideoPath) { + await moveFileWithOverwrite(tempVideoPath, finalVideoPath); + } + + if (windowsSystemAudioPath && tempVideoPath.endsWith(".mp4")) { + const tempAudioPath = tempVideoPath.replace(".mp4", ".system.wav"); + stagedTempSystemAudioPath = tempAudioPath; + const finalAudioPath = windowsSystemAudioPath; + if (await pathExists(tempAudioPath)) { + await moveFileWithOverwrite(tempAudioPath, finalAudioPath); + const tempJson = tempAudioPath + ".json"; + if (await pathExists(tempJson)) { + await moveFileWithOverwrite(tempJson, finalAudioPath + ".json"); + } + } + } + + if (windowsMicAudioPath && tempVideoPath.endsWith(".mp4")) { + const tempMicPath = tempVideoPath.replace(".mp4", ".mic.wav"); + stagedTempMicAudioPath = tempMicPath; + const finalMicPath = windowsMicAudioPath; + if (await pathExists(tempMicPath)) { + await moveFileWithOverwrite(tempMicPath, finalMicPath); + const tempJson = tempMicPath + ".json"; + if (await pathExists(tempJson)) { + await moveFileWithOverwrite(tempJson, finalMicPath + ".json"); + } + } + } + const validation = await validateRecordedVideo(finalVideoPath); + + setWindowsCaptureProcess(null); + setWindowsNativeCaptureActive(false); + setNativeScreenRecordingActive(false); + setWindowsCaptureTargetPath(null); + setWindowsCaptureStopRequested(false); + setWindowsCapturePaused(false); + setWindowsOrphanedMicAudioPath(null); + await cleanupWindowsOrphanedMicAudioPath(preferredOrphanedMicAudioPath); + setWindowsPendingVideoPath(finalVideoPath); + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "stop", + outputPath: finalVideoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: validation.fileSizeBytes, + }); + await writeWindowsRecordingDiagnostics(finalVideoPath, { + phase: "stop", + outputPath: finalVideoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + details: { + fileSizeBytes: validation.fileSizeBytes, + durationSeconds: validation.durationSeconds, + }, + }); + + // Persist cursor telemetry before returning so the editor can find it immediately + snapshotCursorTelemetryForPersistence(); + try { + await persistPendingCursorTelemetry(finalVideoPath); + } catch (error) { + console.warn( + "Failed to persist cursor telemetry during native stop:", + error, + ); + } + + return { success: true, path: finalVideoPath }; + } catch (error) { + console.error("Failed to stop native Windows capture:", error); + const fallbackPath = await resolveExistingPath( + windowsCaptureTargetPath, + stagedTempVideoPath, + ); + const recoveredSystemAudioPath = await resolveExistingPath( + windowsSystemAudioPath, + stagedTempSystemAudioPath, + ); + const recoveredMicAudioPath = await resolveExistingPath( + windowsMicAudioPath, + stagedTempMicAudioPath, + ); + const fallbackOrphanedMicAudioPath = windowsOrphanedMicAudioPath; + const diagnosticsSystemAudioPath = + recoveredSystemAudioPath ?? windowsSystemAudioPath; + const diagnosticsMicAudioPath = recoveredMicAudioPath ?? windowsMicAudioPath; + setWindowsNativeCaptureActive(false); + setNativeScreenRecordingActive(false); + setWindowsCaptureProcess(null); + setWindowsCaptureTargetPath(null); + setWindowsCaptureStopRequested(false); + setWindowsCapturePaused(false); + setWindowsOrphanedMicAudioPath(null); + + if (fallbackPath) { + try { + const validation = await validateRecordedVideo(fallbackPath); + setWindowsPendingVideoPath(fallbackPath); + setWindowsSystemAudioPath(recoveredSystemAudioPath); + setWindowsMicAudioPath(recoveredMicAudioPath); + await cleanupWindowsOrphanedMicAudioPath(fallbackOrphanedMicAudioPath); + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "stop", + outputPath: fallbackPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: validation.fileSizeBytes, + error: String(error), + }); + await writeWindowsRecordingDiagnostics(fallbackPath, { + phase: "stop", + outputPath: fallbackPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + error: String(error), + details: { + fileSizeBytes: validation.fileSizeBytes, + durationSeconds: validation.durationSeconds, + recoveredAfterStopFailure: true, + }, + }); + return { success: true, path: fallbackPath }; + } catch { + // File is absent or failed validation. + } + } + + setWindowsSystemAudioPath(null); + setWindowsMicAudioPath(null); + setWindowsPendingVideoPath(null); + await cleanupWindowsOrphanedMicAudioPath(fallbackOrphanedMicAudioPath); + + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "stop", + outputPath: fallbackPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: await getFileSizeIfPresent(fallbackPath), + error: String(error), + }); + await writeWindowsRecordingDiagnostics(fallbackPath, { + phase: "stop", + outputPath: fallbackPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + error: String(error), + details: { + fileSizeBytes: await getFileSizeIfPresent(fallbackPath), + }, + }); + + return { + success: false, + message: "Failed to stop native Windows capture", + error: String(error), + }; + } + } + + if (process.platform !== "darwin") { + return { + success: false, + message: "Native screen recording is only available on macOS.", + }; + } + + if (!nativeScreenRecordingActive) { + const recovered = await recoverNativeMacCaptureOutput(); + if (recovered) { + return recovered; + } + + return { success: false, message: "No native screen recording is active." }; + } + + try { + if (!nativeCaptureProcess) { + throw new Error("Native capture helper process is not running"); + } + + const process = nativeCaptureProcess; + const preferredVideoPath = nativeCaptureTargetPath; + const preferredSystemAudioPath = nativeCaptureSystemAudioPath; + const preferredMicrophonePath = nativeCaptureMicrophonePath; + console.log( + "[stop-native] Audio paths — system:", + preferredSystemAudioPath, + "mic:", + preferredMicrophonePath, + ); + setNativeCaptureStopRequested(true); + process.stdin.write("stop\n"); + const tempVideoPath = await waitForNativeCaptureStop(process); + console.log("[stop-native] Helper stopped, tempVideoPath:", tempVideoPath); + setNativeCaptureProcess(null); + setNativeScreenRecordingActive(false); + setNativeCaptureTargetPath(null); + setNativeCaptureSystemAudioPath(null); + setNativeCaptureMicrophonePath(null); + setNativeCaptureStopRequested(false); + setNativeCapturePaused(false); + + const finalVideoPath = preferredVideoPath ?? tempVideoPath; + if (tempVideoPath !== finalVideoPath) { + await moveFileWithOverwrite(tempVideoPath, finalVideoPath); + } + + if (preferredSystemAudioPath || preferredMicrophonePath) { + console.log( + "[stop-native] Attempting audio mux (merging separate tracks) into:", + finalVideoPath, + ); + try { + await muxNativeMacRecordingWithAudio( + finalVideoPath, + preferredSystemAudioPath, + preferredMicrophonePath, + ); + console.log("[stop-native] Audio mux completed successfully"); + } catch (error) { + console.warn( + "[stop-native] Audio mux failed (video still has inline audio):", + error, + ); + } + } else { + console.log("[stop-native] No separate audio tracks to mux"); + } + + return await finalizeStoredVideo(finalVideoPath); + } catch (error) { + console.error("Failed to stop native ScreenCaptureKit recording:", error); + const fallbackPath = nativeCaptureTargetPath; + const fallbackSystemAudioPath = nativeCaptureSystemAudioPath; + const fallbackMicrophonePath = nativeCaptureMicrophonePath; + const fallbackFileSizeBytes = await getFileSizeIfPresent(fallbackPath); + setNativeScreenRecordingActive(false); + setNativeCaptureProcess(null); + setNativeCaptureTargetPath(null); + setNativeCaptureSystemAudioPath(null); + setNativeCaptureMicrophonePath(null); + setNativeCaptureStopRequested(false); + setNativeCapturePaused(false); + + recordNativeCaptureDiagnostics({ + backend: "mac-screencapturekit", + phase: "stop", + sourceId: lastNativeCaptureDiagnostics?.sourceId ?? null, + sourceType: lastNativeCaptureDiagnostics?.sourceType ?? "unknown", + displayId: lastNativeCaptureDiagnostics?.displayId ?? null, + displayBounds: lastNativeCaptureDiagnostics?.displayBounds ?? null, + windowHandle: lastNativeCaptureDiagnostics?.windowHandle ?? null, + helperPath: lastNativeCaptureDiagnostics?.helperPath ?? null, + outputPath: fallbackPath, + systemAudioPath: fallbackSystemAudioPath, + microphonePath: fallbackMicrophonePath, + osRelease: lastNativeCaptureDiagnostics?.osRelease, + supported: lastNativeCaptureDiagnostics?.supported, + helperExists: lastNativeCaptureDiagnostics?.helperExists, + processOutput: nativeCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: fallbackFileSizeBytes, + error: String(error), + }); + + // Try to recover: if the target file exists on disk, finalize with it + if (fallbackPath) { + try { + await fs.access(fallbackPath); + console.log( + "[stop-native-screen-recording] Recovering with fallback path:", + fallbackPath, + ); + if (fallbackSystemAudioPath || fallbackMicrophonePath) { + try { + await muxNativeMacRecordingWithAudio( + fallbackPath, + fallbackSystemAudioPath, + fallbackMicrophonePath, + ); + } catch (muxError) { + console.warn( + "Failed to mux recovered native macOS audio into capture:", + muxError, + ); + } + } + return await finalizeStoredVideo(fallbackPath); + } catch { + // File doesn't exist or isn't accessible + } + } + + const recovered = await recoverNativeMacCaptureOutput(); + if (recovered) { + return recovered; + } + + return { + success: false, + message: "Failed to stop native ScreenCaptureKit recording", + error: String(error), + }; + } + } finally { + console.log( + `[PERF:MAIN] Handler: stop-native-screen-recording: COMPLETED in ${Date.now() - start}ms`, + ); + } + } + ipcMain.handle("stop-native-screen-recording", async () => { + const stopped = await stopNativeScreenRecording(); + // A countdown the renderer stopped is deleted by it next; remember it in case the + // renderer dies before that (HUD crash recovery discards it then). + if (stopped.success && "path" in stopped && stopped.path && !lastStopEndedUserRecording) { + stoppedCountdownPath = stopped.path; + } + return stopped; + }); + + ipcMain.handle("recover-native-screen-recording", async () => { + if (process.platform !== "darwin") { + return { + success: false, + message: "Native screen recording recovery is only available on macOS.", + }; + } + + const recovered = await recoverNativeMacCaptureOutput(); + if (recovered) { + return recovered; + } + + return { + success: false, + message: "No recoverable native macOS recording output was found.", + }; + }); + + ipcMain.handle("pause-native-screen-recording", async () => { + if (process.platform === "win32") { + if (!windowsNativeCaptureActive || !windowsCaptureProcess) { + return { success: false, message: "No native Windows screen recording is active." }; + } + + if (windowsCapturePaused) { + return { success: true }; + } + + try { + windowsCaptureProcess.stdin.write("pause\n"); + setWindowsCapturePaused(true); + return { success: true }; + } catch (error) { + return { + success: false, + message: "Failed to pause native Windows capture", + error: String(error), + }; + } + } + + if (process.platform !== "darwin") { + return { + success: false, + message: "Native screen recording is only available on macOS.", + }; + } + + if (!nativeScreenRecordingActive || !nativeCaptureProcess) { + return { success: false, message: "No native screen recording is active." }; + } + + if (nativeCapturePaused) { + return { success: true }; + } + + try { + const commandApplied = waitForNativeCaptureCommand( + nativeCaptureProcess, + "Recording paused", + ); + nativeCaptureProcess.stdin.write("pause\n"); + await commandApplied; + setNativeCapturePaused(true); + return { success: true }; + } catch (error) { + return { + success: false, + message: "Failed to pause native screen recording", + error: String(error), + }; + } + }); + + ipcMain.handle("resume-native-screen-recording", async () => { + if (process.platform === "win32") { + if (!windowsNativeCaptureActive || !windowsCaptureProcess) { + return { success: false, message: "No native Windows screen recording is active." }; + } + + if (!windowsCapturePaused) { + return { success: true }; + } + + try { + windowsCaptureProcess.stdin.write("resume\n"); + setWindowsCapturePaused(false); + return { success: true }; + } catch (error) { + return { + success: false, + message: "Failed to resume native Windows capture", + error: String(error), + }; + } + } + + if (process.platform !== "darwin") { + return { + success: false, + message: "Native screen recording is only available on macOS.", + }; + } + + if (!nativeScreenRecordingActive || !nativeCaptureProcess) { + return { success: false, message: "No native screen recording is active." }; + } + + if (!nativeCapturePaused) { + return { success: true }; + } + + try { + const commandApplied = waitForNativeCaptureCommand( + nativeCaptureProcess, + "Recording resumed", + ); + nativeCaptureProcess.stdin.write("resume\n"); + await commandApplied; + setNativeCapturePaused(false); + return { success: true }; + } catch (error) { + return { + success: false, + message: "Failed to resume native screen recording", + error: String(error), + }; + } + }); + + ipcMain.handle("get-system-cursor-assets", async () => { + try { + return { success: true, cursors: await getSystemCursorAssets() }; + } catch (error) { + console.error("Failed to load system cursor assets:", error); + return { success: false, cursors: {}, error: String(error) }; + } + }); + + ipcMain.handle("is-native-windows-capture-available", async () => { + return { available: await isNativeWindowsCaptureAvailable() }; + }); + + ipcMain.handle("get-last-native-capture-diagnostics", async () => { + return { success: true, diagnostics: lastNativeCaptureDiagnostics }; + }); + + ipcMain.handle("get-video-audio-fallback-paths", async (_event, videoPath: string) => { + if (!videoPath) { + return { success: true, paths: [], startDelayMsByPath: {} }; + } + + try { + const { paths, startDelayMsByPath } = await getCompanionAudioFallbackInfo(videoPath); + await Promise.all([ + rememberApprovedLocalReadPath(videoPath), + ...paths.map((fallbackPath) => rememberApprovedLocalReadPath(fallbackPath)), + ]); + return { success: true, paths, startDelayMsByPath }; + } catch (error) { + console.error("Failed to resolve companion audio fallback paths:", error); + return { success: false, paths: [], startDelayMsByPath: {}, error: String(error) }; + } + }); + + async function muxNativeWindowsRecording(expectedDurationMs?: number) { + const start = Date.now(); + console.log("[PERF:MAIN] Handler: mux-native-windows-recording: STARTED"); + try { + const videoPath = windowsPendingVideoPath; + const orphanedMicAudioPath = windowsOrphanedMicAudioPath; + const diagnosticsSystemAudioPath = windowsSystemAudioPath; + const diagnosticsMicAudioPath = windowsMicAudioPath; + setWindowsPendingVideoPath(null); + setWindowsOrphanedMicAudioPath(null); + + if (!videoPath) { + return { success: false, message: "No native Windows video pending for mux" }; + } + + try { + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "mux-start", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + details: { + hasSystemAudio: Boolean(diagnosticsSystemAudioPath), + hasMicrophone: Boolean(diagnosticsMicAudioPath), + hasOrphanedMicrophone: Boolean(orphanedMicAudioPath), + }, + }); + console.log("[mux-win] Optimization active: skipping video padding."); + + let muxDetails: unknown = null; + if (diagnosticsSystemAudioPath || diagnosticsMicAudioPath) { + muxDetails = await muxNativeWindowsVideoWithAudio( + videoPath, + diagnosticsSystemAudioPath, + diagnosticsMicAudioPath, + ); + setWindowsSystemAudioPath(null); + setWindowsMicAudioPath(null); + } + + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "mux", + outputPath: videoPath, + fileSizeBytes: await getFileSizeIfPresent(videoPath), + }); + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "mux-complete", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + details: { + fileSizeBytes: await getFileSizeIfPresent(videoPath), + mux: muxDetails, + }, + }); + await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath); + return await finalizeStoredVideo(videoPath); + } catch (error) { + console.error("Failed to mux native Windows recording:", error); + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "mux", + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + fileSizeBytes: await getFileSizeIfPresent(videoPath), + error: String(error), + }); + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "mux-error", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + error: String(error), + details: { + fileSizeBytes: await getFileSizeIfPresent(videoPath), + }, + }); + setWindowsSystemAudioPath(null); + setWindowsMicAudioPath(null); + await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath); + try { + return await finalizeStoredVideo(videoPath); + } catch { + try { + await validateRecordedVideo(videoPath); + return { + success: false, + path: videoPath, + message: "Failed to mux native Windows recording", + error: String(error), + }; + } catch { + // The fallback path is not safely playable; surface the original mux error. + } + + return { + success: false, + message: "Failed to mux native Windows recording", + error: String(error), + }; + } + } + } finally { + console.log( + `[PERF:MAIN] Handler: mux-native-windows-recording: COMPLETED in ${Date.now() - start}ms`, + ); + } + } + ipcMain.handle("mux-native-windows-recording", (_event, expectedDurationMs?: number) => + muxNativeWindowsRecording(expectedDurationMs), + ); + + ipcMain.handle("start-ffmpeg-recording", async (_, source: SelectedSource) => { + if (ffmpegCaptureProcess) { + return { success: false, message: "An FFmpeg recording is already active." }; + } + + try { + const recordingsDir = await getRecordingsDir(); + const ffmpegPath = getFfmpegBinaryPath(); + const outputPath = path.join(recordingsDir, `recording-${Date.now()}.mp4`); + const args = await buildFfmpegCaptureArgs(source, outputPath); + + setFfmpegCaptureOutputBuffer(""); + setFfmpegCaptureTargetPath(outputPath); + const ffProc = spawn(ffmpegPath, args, { + cwd: recordingsDir, + stdio: ["pipe", "pipe", "pipe"], + }); + setFfmpegCaptureProcess(ffProc); + + ffProc.stdout.on("data", (chunk: Buffer) => { + setFfmpegCaptureOutputBuffer(ffmpegCaptureOutputBuffer + chunk.toString()); + }); + ffProc.stderr.on("data", (chunk: Buffer) => { + setFfmpegCaptureOutputBuffer(ffmpegCaptureOutputBuffer + chunk.toString()); + }); + + await waitForFfmpegCaptureStart(ffProc); + setFfmpegScreenRecordingActive(true); + return { success: true }; + } catch (error) { + console.error("Failed to start FFmpeg recording:", error); + setFfmpegScreenRecordingActive(false); + setFfmpegCaptureProcess(null); + setFfmpegCaptureTargetPath(null); + return { + success: false, + message: "Failed to start FFmpeg recording", + error: String(error), + }; + } + }); + + ipcMain.handle("stop-ffmpeg-recording", async () => { + if (!ffmpegScreenRecordingActive) { + return { success: false, message: "No FFmpeg recording is active." }; + } + + try { + if (!ffmpegCaptureProcess || !ffmpegCaptureTargetPath) { + throw new Error("FFmpeg process is not running"); + } + + const process = ffmpegCaptureProcess; + const outputPath = ffmpegCaptureTargetPath; + process.stdin.write("q\n"); + const finalVideoPath = await waitForFfmpegCaptureStop(process, outputPath); + + setFfmpegCaptureProcess(null); + setFfmpegCaptureTargetPath(null); + setFfmpegScreenRecordingActive(false); + + return await finalizeStoredVideo(finalVideoPath); + } catch (error) { + console.error("Failed to stop FFmpeg recording:", error); + try { + ffmpegCaptureProcess?.kill(); + } catch { + // ignore cleanup failures + } + setFfmpegCaptureProcess(null); + setFfmpegCaptureTargetPath(null); + setFfmpegScreenRecordingActive(false); + return { + success: false, + message: "Failed to stop FFmpeg recording", + error: String(error), + }; + } + }); + + ipcMain.handle( + "store-microphone-sidecar", + async ( + _, + audioData: ArrayBuffer, + videoPath: string, + options?: { + startDelayMs?: number; + browserMicrophoneProfile?: string; + requestedBrowserMicrophoneProfile?: string | null; + requestedConstraints?: unknown; + mediaTrackSettings?: Record; + audioInputDevices?: unknown; + mediaRecorder?: unknown; + chunkEvents?: unknown; + pauseIntervals?: unknown; + }, + ) => { + const baseName = videoPath.replace(/\.[^.]+$/, ""); + const sidecarPath = `${baseName}.mic.wav`; + const sourceWebmPath = `${baseName}.mic.source.webm`; + const tempWebmPath = `${sourceWebmPath}.tmp`; + + try { + await fs.writeFile(tempWebmPath, Buffer.from(audioData)); + await execFileAsync( + getFfmpegBinaryPath(), + [ + "-y", + "-hide_banner", + "-nostdin", + "-nostats", + "-i", + tempWebmPath, + "-vn", + "-ac", + "1", + "-ar", + "48000", + "-af", + [ + ...getBrowserMicSidecarFilters(options?.browserMicrophoneProfile), + "aresample=async=1:first_pts=0", + ].join(","), + "-c:a", + "pcm_s16le", + sidecarPath, + ], + { timeout: 120000, maxBuffer: 10 * 1024 * 1024 }, + ); + if (shouldKeepRecordingAudioSidecars()) { + await fs.rename(tempWebmPath, sourceWebmPath).catch(async () => { + await fs.copyFile(tempWebmPath, sourceWebmPath); + await fs.rm(tempWebmPath, { force: true }); + }); + } else { + await fs.rm(tempWebmPath, { force: true }); + } + const startDelayMs = options?.startDelayMs; + const mediaTrackSettings = pickPrimitiveRecord(options?.mediaTrackSettings); + const audioInputDevices = pickAudioInputDevices(options?.audioInputDevices); + const mediaRecorder = isRecord(options?.mediaRecorder) + ? { + ...(typeof options.mediaRecorder.mimeType === "string" + ? { mimeType: options.mediaRecorder.mimeType } + : {}), + ...(typeof options.mediaRecorder.audioBitsPerSecond === "number" + ? { + audioBitsPerSecond: Math.round( + options.mediaRecorder.audioBitsPerSecond, + ), + } + : {}), + ...(typeof options.mediaRecorder.timesliceMs === "number" + ? { timesliceMs: Math.round(options.mediaRecorder.timesliceMs) } + : {}), + } + : null; + const chunkEvents = pickMicrophoneChunkEvents(options?.chunkEvents); + const pauseIntervals = pickMicrophonePauseIntervals(options?.pauseIntervals); + const chunkTiming = + chunkEvents || pauseIntervals + ? summarizeMicrophoneChunkTiming( + chunkEvents, + pauseIntervals, + mediaRecorder?.timesliceMs, + ) + : null; + const metadata = { + ...(Number.isFinite(startDelayMs) && (startDelayMs ?? 0) >= 0 + ? { startDelayMs: Math.round(startDelayMs ?? 0) } + : {}), + ...(typeof options?.browserMicrophoneProfile === "string" + ? { browserMicrophoneProfile: options.browserMicrophoneProfile } + : {}), + ...(typeof options?.requestedBrowserMicrophoneProfile === "string" + ? { + requestedBrowserMicrophoneProfile: + options.requestedBrowserMicrophoneProfile, + } + : {}), + ...(isRecord(options?.requestedConstraints) + ? { requestedConstraints: options.requestedConstraints } + : {}), + ...(mediaTrackSettings ? { mediaTrackSettings } : {}), + ...(audioInputDevices ? { audioInputDevices } : {}), + ...(mediaRecorder && Object.keys(mediaRecorder).length > 0 + ? { mediaRecorder } + : {}), + ...(chunkEvents ? { chunkEvents } : {}), + ...(pauseIntervals ? { pauseIntervals } : {}), + ...(chunkTiming ? { chunkTiming } : {}), + }; + if (Object.keys(metadata).length > 0) { + try { + await fs.writeFile(`${sidecarPath}.json`, JSON.stringify(metadata)); + } catch (metadataError) { + console.warn( + "Failed to store microphone sidecar timing metadata:", + metadataError, + ); + } + } + await writeRecordingDiagnosticsSnapshot(videoPath, { + backend: "browser-store", + phase: "mic-sidecar", + outputPath: videoPath, + microphonePath: sidecarPath, + details: { + sourceBytes: audioData.byteLength, + sourceWebmPath: shouldKeepRecordingAudioSidecars() ? sourceWebmPath : null, + metadata, + }, + }).catch((diagnosticsError) => { + console.warn( + "Failed to write microphone sidecar diagnostics:", + diagnosticsError, + ); + }); + return { success: true, path: sidecarPath }; + } catch (error) { + await Promise.all([ + fs.rm(tempWebmPath, { force: true }).catch(() => undefined), + fs.rm(sidecarPath, { force: true }).catch(() => undefined), + ]); + console.error("Failed to store microphone sidecar:", error); + return { success: false, error: String(error) }; + } + }, + ); + + ipcMain.handle("store-recorded-video", async (_, videoData: ArrayBuffer, fileName: unknown) => { + try { + const recordingsDir = await getRecordingsDir(); + const videoPath = resolveRecordedVideoStoragePath(recordingsDir, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + return await finalizeStoredVideo(videoPath); + } catch (error) { + console.error("Failed to store video:", error); + return { + success: false, + message: "Failed to store video", + error: String(error), + }; + } + }); + + ipcMain.handle("get-recorded-video-path", async () => { + try { + const recordingsDir = await getRecordingsDir(); + const entries = await fs.readdir(recordingsDir, { withFileTypes: true }); + const candidates = await Promise.all( + entries + .filter( + (entry) => + entry.isFile() && /^recording-\d+\.(webm|mov|mp4)$/i.test(entry.name), + ) + .map(async (entry) => { + const fullPath = path.join(recordingsDir, entry.name); + const stat = await fs.stat(fullPath).catch(() => null); + return stat ? { path: fullPath, mtimeMs: stat.mtimeMs } : null; + }), + ); + const sortedCandidates = candidates + .filter( + (candidate): candidate is { path: string; mtimeMs: number } => + candidate !== null, + ) + .sort((left, right) => right.mtimeMs - left.mtimeMs); + + for (const candidate of sortedCandidates) { + try { + await validateRecordedVideo(candidate.path); + return { success: true, path: candidate.path }; + } catch (error) { + console.warn( + "Skipping unusable recovered recording candidate:", + candidate.path, + error, + ); + } + } + + if (sortedCandidates.length === 0) { + return { success: false, message: "No recorded video found" }; + } + + return { success: false, message: "No usable recorded video found" }; + } catch (error) { + console.error("Failed to get video path:", error); + return { success: false, message: "Failed to get video path", error: String(error) }; + } + }); + + function setRecordingState(recording: boolean) { + userRecordingActive = recording; + if (recording) { + stopCursorCapture(); + stopInteractionCapture(); + startWindowBoundsCapture(); + void startNativeCursorMonitor(); + setIsCursorCaptureActive(true); + setActiveCursorSamples([]); + setPendingCursorSamples([]); + setCursorCaptureStartTimeMs(Date.now()); + resetCursorCaptureClock(); + setLinuxCursorScreenPoint(null); + setLastLeftClick(null); + sampleCursorPoint(); + startCursorSampling(); + void startInteractionCapture(); + } else { + setIsCursorCaptureActive(false); + stopCursorCapture(); + stopInteractionCapture(); + stopWindowBoundsCapture(); + stopNativeCursorMonitor(); + showCursor(); + setLinuxCursorScreenPoint(null); + resetCursorCaptureClock(); + snapshotCursorTelemetryForPersistence(); + setActiveCursorSamples([]); + } + + const source = selectedSource || { name: "Screen" }; + const recordingArea = recording ? getSourceArea(selectedSource) : null; + if (recordingArea) { + showAreaRecordingBorder(recordingArea); + } else { + hideAreaRecordingBorder(); + } + BrowserWindow.getAllWindows().forEach((window) => { + if (!window.isDestroyed()) { + window.webContents.send("recording-state-changed", { + recording, + sourceName: source.name, + }); + } + }); + + if (onRecordingStateChange) { + onRecordingStateChange(recording, source.name); + } + } + ipcMain.handle("set-recording-state", (_, recording: boolean) => setRecordingState(recording)); + + ipcMain.handle("pause-cursor-capture", (_, pausedAtMs?: unknown) => { + pauseCursorCaptureAtBoundary(normalizeRendererTimestampMs(pausedAtMs)); + return { success: true }; + }); + + ipcMain.handle("resume-cursor-capture", (_, resumedAtMs?: unknown) => { + resumeCursorCapture(normalizeRendererTimestampMs(resumedAtMs)); + sampleCursorPoint(); + return { success: true }; + }); + + ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => { + const targetVideoPath = normalizeVideoSourcePath(videoPath ?? currentVideoPath); + if (!targetVideoPath) { + return { success: true, samples: [] }; + } + + const telemetryPath = getTelemetryPathForVideo(targetVideoPath); + try { + const content = await fs.readFile(telemetryPath, "utf-8"); + const parsed = parseJsonWithByteOrderMark(content); + const samples = normalizeCursorTelemetrySamples(parsed); + + return { success: true, samples }; + } catch (error) { + const nodeError = error as NodeJS.ErrnoException; + if (nodeError.code === "ENOENT") { + return { success: true, samples: [] }; + } + console.error("Failed to load cursor telemetry:", error); + return { + success: false, + message: "Failed to load cursor telemetry", + error: String(error), + samples: [], + }; + } + }); + + ipcMain.handle( + "set-cursor-telemetry", + async (_, videoPath: string | undefined, samples: CursorTelemetryPoint[]) => { + const targetVideoPath = normalizeVideoSourcePath(videoPath ?? currentVideoPath); + if (!targetVideoPath) { + return { + success: false, + samples: [], + message: "No video path available for cursor telemetry", + error: "Missing video path", + }; + } + + try { + const normalizedSamples = await writeCursorTelemetry(targetVideoPath, samples); + return { success: true, samples: normalizedSamples }; + } catch (error) { + console.error("Failed to save cursor telemetry:", error); + return { + success: false, + samples: [], + message: "Failed to save cursor telemetry", + error: String(error), + }; + } + }, + ); +} diff --git a/electron/ipc/register/settings.ts b/electron/ipc/register/settings.ts new file mode 100644 index 000000000..065ed7070 --- /dev/null +++ b/electron/ipc/register/settings.ts @@ -0,0 +1,213 @@ +import { createCountdownController } from "../../countdownController"; +import fs from "node:fs/promises"; +import { app, BrowserWindow, ipcMain } from "electron"; +import { hasAppSetting, readAppSettingsStore, writeAppSettingsStore } from "../../appSettingsStore"; +import { hideCursor } from "../../cursorHider"; +import { createCountdownWindow } from "../../windows"; +import { COUNTDOWN_SETTINGS_FILE, RECORDINGS_SETTINGS_FILE, SHORTCUTS_FILE } from "../constants"; +import { + createRecordingPreferencesStore, + type RecordingPreferencesPatch, +} from "../settings/recordingPreferencesStore"; +import { + countdownInProgress, + countdownRemaining, + setCountdownInProgress, + setCountdownRemaining, +} from "../state"; +import { parseJsonWithByteOrderMark } from "../utils"; + +const BROWSER_MICROPHONE_PROFILE_ENV = "RECORDLY_BROWSER_MIC_PROFILE"; +const DEFAULT_BROWSER_MICROPHONE_PROFILE = "processed"; +const recordingPreferencesStore = createRecordingPreferencesStore(RECORDINGS_SETTINGS_FILE); +const BROWSER_MICROPHONE_PROFILES = new Set([ + "processed", + "no-agc", + "no-echo", + "no-noise-suppression", + "raw", +]); + +function getBrowserMicrophoneProfileFromEnv() { + const requested = process.env[BROWSER_MICROPHONE_PROFILE_ENV]?.trim() || null; + const normalized = requested?.toLowerCase() ?? DEFAULT_BROWSER_MICROPHONE_PROFILE; + return { + browserMicrophoneProfile: BROWSER_MICROPHONE_PROFILES.has(normalized) + ? normalized + : DEFAULT_BROWSER_MICROPHONE_PROFILE, + requestedBrowserMicrophoneProfile: requested, + }; +} + +export function registerSettingsHandlers() { + ipcMain.handle( + "get-window-fullscreen", + (event) => BrowserWindow.fromWebContents(event.sender)?.isFullScreen() ?? false, + ); + ipcMain.handle("app:getVersion", () => { + return app.getVersion(); + }); + + ipcMain.handle("get-window-chrome", (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + return { + trafficLightsVisible: + process.platform === "darwin" && + !!win && + !win.isFullScreen() && + !win.isSimpleFullScreen(), + }; + }); + + ipcMain.handle("get-platform", () => { + return process.platform; + }); + + ipcMain.on("app-settings:get", (event, key: unknown) => { + try { + if (typeof key !== "string" || key.length === 0) { + event.returnValue = { success: false, value: null }; + return; + } + + const store = readAppSettingsStore(); + event.returnValue = { + success: true, + value: hasAppSetting(store, key) ? store[key] : null, + }; + } catch (error) { + console.error("Failed to read app setting:", error); + event.returnValue = { success: false, value: null }; + } + }); + + ipcMain.on("app-settings:set", (event, key: unknown, value: unknown) => { + try { + if (typeof key !== "string" || key.length === 0) { + event.returnValue = { success: false }; + return; + } + + const store = readAppSettingsStore(); + store[key] = value; + writeAppSettingsStore(store); + event.returnValue = { success: true }; + } catch (error) { + console.error("Failed to save app setting:", error); + event.returnValue = { success: false }; + } + }); + + // --------------------------------------------------------------------------- + // Cursor hiding for the browser-capture fallback. + // The IPC promise resolves only after the cursor hide attempt completes. + // --------------------------------------------------------------------------- + ipcMain.handle("hide-cursor", () => { + if (process.platform !== "win32") { + return { success: true }; + } + + return { success: hideCursor() }; + }); + + ipcMain.handle("get-shortcuts", async () => { + try { + const data = await fs.readFile(SHORTCUTS_FILE, "utf-8"); + return parseJsonWithByteOrderMark(data); + } catch { + return null; + } + }); + + ipcMain.handle("save-shortcuts", async (_, shortcuts: unknown) => { + try { + await fs.writeFile(SHORTCUTS_FILE, JSON.stringify(shortcuts, null, 2), "utf-8"); + return { success: true }; + } catch (error) { + console.error("Failed to save shortcuts:", error); + return { success: false, error: String(error) }; + } + }); + + // --------------------------------------------------------------------------- + // Countdown timer before recording + // --------------------------------------------------------------------------- + ipcMain.handle("get-recording-preferences", async () => { + try { + const parsed = await recordingPreferencesStore.read(); + return { + success: true, + microphoneEnabled: parsed.microphoneEnabled === true, + microphoneDeviceId: + typeof parsed.microphoneDeviceId === "string" + ? parsed.microphoneDeviceId + : undefined, + systemAudioEnabled: parsed.systemAudioEnabled === true, + webcamEnabled: parsed.webcamEnabled === true, + webcamDeviceId: + typeof parsed.webcamDeviceId === "string" ? parsed.webcamDeviceId : undefined, + }; + } catch { + return { + success: true, + microphoneEnabled: false, + microphoneDeviceId: undefined, + systemAudioEnabled: false, + webcamEnabled: false, + webcamDeviceId: undefined, + }; + } + }); + + ipcMain.handle("get-recording-audio-lab-config", () => { + return getBrowserMicrophoneProfileFromEnv(); + }); + + ipcMain.handle("set-recording-preferences", async (_, prefs: RecordingPreferencesPatch) => { + try { + await recordingPreferencesStore.update(prefs); + return { success: true }; + } catch (error) { + console.error("Failed to save recording preferences:", error); + return { success: false, error: String(error) }; + } + }); + + ipcMain.handle("get-countdown-delay", async () => { + try { + const content = await fs.readFile(COUNTDOWN_SETTINGS_FILE, "utf-8"); + const parsed = parseJsonWithByteOrderMark<{ delay?: number }>(content); + return { success: true, delay: parsed.delay ?? 3 }; + } catch { + return { success: true, delay: 3 }; + } + }); + + ipcMain.handle("set-countdown-delay", async (_, delay: number) => { + try { + await fs.writeFile( + COUNTDOWN_SETTINGS_FILE, + JSON.stringify({ delay }, null, 2), + "utf-8", + ); + return { success: true }; + } catch (error) { + console.error("Failed to save countdown delay:", error); + return { success: false, error: String(error) }; + } + }); + + const countdown = createCountdownController(createCountdownWindow, (remaining) => { + setCountdownRemaining(remaining); + setCountdownInProgress(remaining !== null); + }); + ipcMain.handle("start-countdown", (_, seconds: number) => countdown.start(seconds)); + ipcMain.handle("cancel-countdown", () => countdown.cancel()); + + ipcMain.handle("get-active-countdown", () => { + return { + success: true, + seconds: countdownInProgress ? countdownRemaining : null, + }; + }); +} diff --git a/electron/ipc/register/sourceMapping.test.ts b/electron/ipc/register/sourceMapping.test.ts new file mode 100644 index 000000000..ce7ff4fa6 --- /dev/null +++ b/electron/ipc/register/sourceMapping.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + getScreenSourceIdForDisplay, + LINUX_PORTAL_SCREEN_SOURCE_ID, + resolveDisplayMediaSource, +} from "./sourceMapping"; + +describe("getScreenSourceIdForDisplay", () => { + it("keeps the live Electron screen source when one is available", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + matchedSourceId: "screen:42:0", + platform: "linux", + }), + ).toBe("screen:42:0"); + }); + + it("routes unmatched Linux Wayland screens through the portal sentinel", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, + matchedSourceId: null, + platform: "linux", + }), + ).toBe(LINUX_PORTAL_SCREEN_SOURCE_ID); + }); + + it("keeps unmatched Linux X11 screens on the explicit fallback id", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + env: { XDG_SESSION_TYPE: "x11", DISPLAY: ":0" }, + matchedSourceId: null, + platform: "linux", + }), + ).toBe("screen:fallback:42"); + }); + + it("keeps non-Linux unmatched screens on the explicit fallback id", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + matchedSourceId: undefined, + platform: "win32", + }), + ).toBe("screen:fallback:42"); + }); +}); + +describe("display media source resolution", () => { + for (const selectedSourceId of [null, "window:101:0", LINUX_PORTAL_SCREEN_SOURCE_ID]) { + it(`opens Wayland through Chromium without pre-enumeration (${selectedSourceId})`, async () => { + const getSources = vi.fn(); + expect( + await resolveDisplayMediaSource({ + platform: "linux", + env: { XDG_SESSION_TYPE: "wayland" }, + selectedSourceId, + getSources, + }), + ).toEqual({ id: "screen:0:0", name: "Entire screen" }); + expect(getSources).not.toHaveBeenCalled(); + }); + } + it("returns only the requested X11 source and excludes preview pixels", async () => { + const getSources = vi.fn().mockResolvedValue([ + { id: "screen:1:0", name: "Screen" }, + { id: "window:101:0", name: "Document", thumbnail: "pixels" }, + ]); + expect( + await resolveDisplayMediaSource({ + platform: "linux", + env: { XDG_SESSION_TYPE: "x11" }, + selectedSourceId: "window:101:0", + getSources, + }), + ).toEqual({ id: "window:101:0", name: "Document" }); + }); + it("does not record a different source if the selected window closes", async () => { + const getSources = vi.fn().mockResolvedValue([{ id: "screen:1:0", name: "Screen" }]); + expect( + await resolveDisplayMediaSource({ + platform: "linux", + env: { XDG_SESSION_TYPE: "x11" }, + selectedSourceId: "window:101:0", + getSources, + }), + ).toBeNull(); + }); + for (const selectedSourceId of [null, LINUX_PORTAL_SCREEN_SOURCE_ID]) { + it(`does not open a portal or pick a default on X11 (${selectedSourceId})`, async () => { + const getSources = vi.fn(); + expect( + await resolveDisplayMediaSource({ + platform: "linux", + env: { XDG_SESSION_TYPE: "x11", WAYLAND_DISPLAY: "wayland-0" }, + selectedSourceId, + getSources, + }), + ).toBeNull(); + expect(getSources).not.toHaveBeenCalled(); + }); + } +}); diff --git a/electron/ipc/register/sourceMapping.ts b/electron/ipc/register/sourceMapping.ts new file mode 100644 index 000000000..9ae9372bc --- /dev/null +++ b/electron/ipc/register/sourceMapping.ts @@ -0,0 +1,56 @@ +export const LINUX_PORTAL_SCREEN_SOURCE_ID = "screen:linux-portal"; + +export function isLikelyLinuxWaylandSession(env: NodeJS.ProcessEnv) { + const sessionType = env.XDG_SESSION_TYPE?.trim().toLowerCase(); + if (sessionType === "wayland") { + return true; + } + if (sessionType === "x11") { + return false; + } + + return Boolean(env.WAYLAND_DISPLAY); +} + +export function getScreenSourceIdForDisplay({ + displayId, + env = process.env, + matchedSourceId, + platform, +}: { + displayId: string; + env?: NodeJS.ProcessEnv; + matchedSourceId?: string | null; + platform: NodeJS.Platform | string; +}) { + if (matchedSourceId) { + return matchedSourceId; + } + + if (platform === "linux" && isLikelyLinuxWaylandSession(env)) { + return LINUX_PORTAL_SCREEN_SOURCE_ID; + } + + return `screen:fallback:${displayId}`; +} + +export async function resolveDisplayMediaSource({ + platform, + env, + selectedSourceId, + getSources, +}: { + platform: string; + env: NodeJS.ProcessEnv; + selectedSourceId: string | null | undefined; + getSources: () => Promise>; +}): Promise<{ id: string; name: string } | null> { + if (platform === "linux" && isLikelyLinuxWaylandSession(env)) { + // Chromium opens the portal once when it resolves this source. Enumerating first opens it twice. + return { id: "screen:0:0", name: "Entire screen" }; + } + if (!selectedSourceId || selectedSourceId === LINUX_PORTAL_SCREEN_SOURCE_ID) return null; + const source = (await getSources()).find((source) => source.id === selectedSourceId); + // A vanished source must never silently become a different window or the whole screen. + return source ? { id: source.id, name: source.name } : null; +} diff --git a/electron/ipc/register/sources.test.ts b/electron/ipc/register/sources.test.ts new file mode 100644 index 000000000..8e77acaa0 --- /dev/null +++ b/electron/ipc/register/sources.test.ts @@ -0,0 +1,213 @@ +import { EventEmitter } from "node:events"; +import type { BrowserWindow } from "electron"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import type { SelectedSource } from "../types"; +const mocks = vi.hoisted(() => ({ + handlers: new Map unknown>(), + sources: vi.fn(), + validate: vi.fn(), + recording: false, + selected: null as SelectedSource | null, + exec: vi.fn(), + windows: vi.fn(), + writeAppSetting: vi.fn(), + setHudRecordingPreparationActive: vi.fn(), +})); +vi.mock("electron", () => ({ + ipcMain: { + handle: (name: string, handler: (...args: unknown[]) => unknown) => + mocks.handlers.set(name, handler), + }, + app: { getName: () => "Recordly", focus: vi.fn() }, + BrowserWindow: { getAllWindows: mocks.windows }, + desktopCapturer: { getSources: mocks.sources }, +})); +vi.mock("node:child_process", () => ({ execFile: mocks.exec })); +vi.mock("node:util", () => ({ promisify: () => mocks.exec })); +vi.mock("../../appSettingsStore", () => ({ writeAppSetting: mocks.writeAppSetting })); +vi.mock("../../windows", () => ({ + setHudRecordingPreparationActive: mocks.setHudRecordingPreparationActive, +})); +vi.mock("../constants", () => ({ ALLOW_RECORDLY_WINDOW_CAPTURE: false })); +vi.mock("../cursor/bounds", () => ({ stopWindowBoundsCapture: vi.fn() })); +vi.mock("../recording/ffmpeg", () => ({})); +vi.mock("../windowsWindowControl", () => ({})); +vi.mock("../captureSelection", () => ({ + validateCaptureSource: mocks.validate, + getSessionCaptureSource: () => mocks.selected, +})); +vi.mock("../state", () => ({ + get isCursorCaptureActive() { + return mocks.recording; + }, + get selectedSource() { + return mocks.selected; + }, + setSelectedSource: (source: SelectedSource) => { + mocks.selected = source; + }, +})); +vi.mock("../utils", () => ({ + parseWindowId: (id: string) => Number(id.split(":")[1]) || null, + getScreen: () => ({ + getAllDisplays: () => [{ id: 1, bounds: { x: 0, y: 0 } }], + getPrimaryDisplay: () => ({ id: 1 }), + }), +})); +import { getDesktopSources, registerSourceHandlers } from "./sources"; +const platform = process.platform; +beforeEach(() => { + vi.clearAllMocks(); + mocks.handlers.clear(); + mocks.recording = false; + mocks.selected = null; + mocks.windows.mockReturnValue([]); + mocks.validate.mockImplementation(async (source) => source); + mocks.exec.mockRejectedValue(new Error("wmctrl is not installed")); + Object.defineProperty(process, "platform", { value: "linux" }); + vi.stubEnv("XDG_SESSION_TYPE", "x11"); +}); +afterEach(() => { + Object.defineProperty(process, "platform", { value: platform }); + vi.unstubAllEnvs(); +}); +function pickerFixture() { + let current: BrowserWindow | null = null; + const win = Object.assign(new EventEmitter(), { + webContents: {}, + close: () => { + current = null; + win.emit("closed"); + }, + }); + const create = vi.fn(() => { + current = win as unknown as BrowserWindow; + return current; + }); + const createEditor = vi.fn(); + const { pickSourceList } = registerSourceHandlers({ + createEditorWindow: createEditor, + createSourceSelectorWindow: create, + getSourceSelectorWindow: () => current, + }); + return { + win, + create, + createEditor, + pickSourceList, + select: (source: SelectedSource) => + mocks.handlers.get("select-source")!({ sender: win.webContents }, source), + }; +} +for (const operatingSystem of ["darwin", "win32", "linux"]) { + it(`opens recording onboarding from the HUD on ${operatingSystem}`, () => { + Object.defineProperty(process, "platform", { value: operatingSystem }); + const picker = pickerFixture(); + const send = vi.fn(); + const destroyedSend = vi.fn(); + mocks.windows.mockReturnValue([ + { isDestroyed: () => false, webContents: { send } }, + { isDestroyed: () => true, webContents: { send: destroyedSend } }, + ]); + mocks.handlers.get("show-recording-permissions")!(); + expect(picker.createEditor).toHaveBeenCalledOnce(); + expect(mocks.writeAppSetting).toHaveBeenCalledWith( + "recordly.onboarding.permissionsRequested", + true, + ); + expect(mocks.setHudRecordingPreparationActive).toHaveBeenCalledWith(false); + expect(send).toHaveBeenCalledWith("recording-permissions-requested"); + expect(destroyedSend).not.toHaveBeenCalled(); + }); + it(`keeps recording onboarding closed during active capture on ${operatingSystem}`, () => { + Object.defineProperty(process, "platform", { value: operatingSystem }); + const picker = pickerFixture(); + mocks.recording = true; + mocks.handlers.get("show-recording-permissions")!(); + expect(picker.createEditor).not.toHaveBeenCalled(); + expect(mocks.writeAppSetting).not.toHaveBeenCalled(); + expect(mocks.setHudRecordingPreparationActive).not.toHaveBeenCalled(); + expect(mocks.windows).not.toHaveBeenCalled(); + }); +} +it("never enumerates or previews Wayland sources before recording", async () => { + vi.stubEnv("XDG_SESSION_TYPE", "wayland"); + expect(await getDesktopSources({ types: ["screen", "window"] })).toEqual([ + { id: "screen:linux-portal", name: "System picker", sourceType: "screen" }, + ]); + expect(await getDesktopSources({ types: ["window"] })).toEqual([]); + expect(mocks.sources).not.toHaveBeenCalled(); +}); +it("completes X11 selection even without optional window raising tools", async () => { + const picker = pickerFixture(); + const pending = picker.pickSourceList(); + const source = { id: "window:101:0", name: "Document" }; + expect(await picker.select(source)).toEqual(source); + expect(await pending).toEqual({ success: true, source }); +}); +it("leaves the source list pending when a selected window no longer exists", async () => { + const picker = pickerFixture(); + const pending = picker.pickSourceList(); + mocks.validate.mockResolvedValue(null); + await expect(picker.select({ id: "window:101:0", name: "Document" })).rejects.toThrow( + "no longer available", + ); + expect(mocks.selected).toBeNull(); + picker.win.close(); + expect(await pending).toEqual({ success: false, canceled: true }); +}); +it("does not apply a selection after the source list is dismissed during validation", async () => { + const picker = pickerFixture(); + const pending = picker.pickSourceList(); + let finish!: (value: SelectedSource) => void; + mocks.validate.mockReturnValue( + new Promise((resolve) => { + finish = resolve; + }), + ); + const source = { id: "screen:1:0", name: "Screen" }; + const selection = picker.select(source); + picker.win.close(); + finish(source); + await selection; + expect(await pending).toEqual({ success: false, canceled: true }); + expect(mocks.selected).toBeNull(); +}); +it("does not change sources if recording starts during validation", async () => { + const picker = pickerFixture(); + const pending = picker.pickSourceList(); + mocks.validate.mockImplementation(async (source) => { + mocks.recording = true; + return source; + }); + await picker.select({ id: "screen:1:0", name: "Screen" }); + expect(mocks.selected).toBeNull(); + picker.win.close(); + expect(await pending).toEqual({ success: false, canceled: true }); +}); + +for (const platform of ["darwin", "win32", "linux"]) { + it(`starts ${platform} with no source and keeps explicit selection within the session`, async () => { + Object.defineProperty(process, "platform", { value: platform }); + pickerFixture(); + const read = mocks.handlers.get("get-selected-source")!; + expect(read()).toBeNull(); + const source = { id: "screen:1:0", name: "Screen 1", display_id: "1" }; + mocks.selected = source; + expect(read()).toEqual(source); + }); +} + +it("Wayland starts unselected and prepares its portal only after an explicit picker request", async () => { + vi.stubEnv("XDG_SESSION_TYPE", "wayland"); + const picker = pickerFixture(); + expect(mocks.handlers.get("get-selected-source")!()).toBeNull(); + await mocks.handlers.get("open-source-selector")!(); + expect(mocks.handlers.get("get-selected-source")!()).toEqual({ + id: "screen:linux-portal", + name: "System picker", + sourceType: "screen", + }); + expect(picker.create).not.toHaveBeenCalled(); + expect(mocks.sources).not.toHaveBeenCalled(); +}); diff --git a/electron/ipc/register/sources.ts b/electron/ipc/register/sources.ts new file mode 100644 index 000000000..e1ae2a23b --- /dev/null +++ b/electron/ipc/register/sources.ts @@ -0,0 +1,685 @@ +import { createSourceListPicker } from "../../sourceListPicker"; +import { writeAppSetting } from "../../appSettingsStore"; +import { getSessionCaptureSource, validateCaptureSource } from "../captureSelection"; +import { createRecordingEditorNavigation } from "../../recordingEditorNavigation"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { app, BrowserWindow, desktopCapturer, ipcMain, systemPreferences } from "electron"; +import { + setHudRecordingPreparationActive, + createHudOverlayWindow, + getHudOverlayWindow, + reassertHudOverlayMousePassthrough, +} from "../../windows"; +import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants"; +import { + getNativeMacWindowSources, + resolveLinuxWindowBounds, + resolveMacWindowBounds, + stopWindowBoundsCapture, +} from "../cursor/bounds"; +import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg"; +import { isCursorCaptureActive, selectedSource, setSelectedSource } from "../state"; +import type { SelectedSource, WindowBounds } from "../types"; +import { getSourceArea } from "../sourceArea"; +import { getScreen, parseWindowId } from "../utils"; +import { bringWindowsWindowForward, resolveWindowsWindowBounds } from "../windowsWindowControl"; +import { + getScreenSourceIdForDisplay, + isLikelyLinuxWaylandSession, + LINUX_PORTAL_SCREEN_SOURCE_ID, +} from "./sourceMapping"; + +const execFileAsync = promisify(execFile); +const SOURCE_LIST_CACHE_TTL_MS = 1200; +let sourceListCache: { + key: string; + expiresAt: number; + value: SelectedSource[]; +} | null = null; + +function normalizeDesktopSourceName(value: string) { + return value.trim().replace(/\s+/g, " ").toLowerCase(); +} + +export function broadcastSelectedSourceChange() { + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed()) { + window.webContents.send("selected-source-changed", selectedSource); + } + } +} + +export async function bringSelectedWindowForward( + source: SelectedSource, +): Promise { + const windowId = parseWindowId(source.id); + if (!windowId) return null; + + try { + if (process.platform === "darwin") { + const rawAppName = source.appName || source.name?.split(" — ")[0]?.trim(); + const appName = + rawAppName && /^[\w .&()+'-]{1,64}$/.test(rawAppName) ? rawAppName : null; + if (!appName) return null; + await execFileAsync("open", ["-a", appName], { timeout: 2000 }); + try { + systemPreferences?.isTrustedAccessibilityClient?.(true); + const { stdout } = await execFileAsync( + "osascript", + [ + "-e", + "on run argv", + "-e", + 'tell application "System Events" to tell process (item 1 of argv)', + "-e", + "repeat with candidate in windows", + "-e", + "try", + "-e", + 'if value of attribute "AXWindowNumber" of candidate is (item 2 of argv) as integer then', + "-e", + 'perform action "AXRaise" of candidate', + "-e", + "set windowPosition to position of candidate", + "-e", + "set windowSize to size of candidate", + "-e", + 'return ((item 1 of windowPosition) as text) & "," & ((item 2 of windowPosition) as text) & "," & ((item 1 of windowSize) as text) & "," & ((item 2 of windowSize) as text)', + "-e", + "end if", + "-e", + "end try", + "-e", + "end repeat", + "-e", + "end tell", + "-e", + "end run", + "--", + appName, + String(windowId), + ], + { timeout: 2000 }, + ); + const [x, y, width, height] = stdout.trim().split(",").map(Number); + if ([x, y, width, height].every(Number.isFinite) && width > 0 && height > 0) { + await new Promise((resolve) => setTimeout(resolve, 250)); + return { x, y, width, height }; + } + } catch { + // App activation still works without macOS Accessibility permission. + } + } else if (process.platform === "win32") { + await bringWindowsWindowForward(windowId); + } else if (process.platform === "linux") { + await execFileAsync("wmctrl", ["-i", "-a", `0x${windowId.toString(16)}`], { + timeout: 1500, + }); + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } catch { + // Raising the source is best-effort; selection and capture can still continue. + } + return null; +} + +export async function getDesktopSources( + opts?: Electron.SourcesOptions, + options?: { strict?: boolean }, +): Promise { + if (process.platform === "linux" && isLikelyLinuxWaylandSession(process.env)) { + return opts?.types && !opts.types.includes("screen") + ? [] + : [{ id: LINUX_PORTAL_SCREEN_SOURCE_ID, name: "System picker", sourceType: "screen" }]; + } + const cacheKey = JSON.stringify({ + types: opts?.types, + thumbnailSize: opts?.thumbnailSize, + fetchWindowIcons: opts?.fetchWindowIcons, + }); + if ( + !options?.strict && + sourceListCache && + sourceListCache.key === cacheKey && + sourceListCache.expiresAt > Date.now() + ) { + return sourceListCache.value; + } + + const includeScreens = Array.isArray(opts?.types) ? opts.types.includes("screen") : true; + const includeWindows = Array.isArray(opts?.types) ? opts.types.includes("window") : true; + const includeWindowIcons = Boolean(opts?.fetchWindowIcons); + const electronTypes = [ + ...(includeScreens ? ["screen" as const] : []), + ...(includeWindows ? ["window" as const] : []), + ]; + const electronSources = + electronTypes.length > 0 + ? await desktopCapturer + .getSources({ + ...opts, + types: electronTypes, + }) + .catch((error) => { + if (options?.strict) + throw new Error( + "Unable to detect recording sources. Check capture permission and try again.", + ); + console.warn( + "desktopCapturer.getSources failed (screen recording permission may be missing):", + error, + ); + return []; + }) + : []; + const ownWindowNames = new Set( + [ + app.getName(), + "Recordly", + ...BrowserWindow.getAllWindows().flatMap((win) => { + const title = win.getTitle().trim(); + return title ? [title] : []; + }), + ] + .map((name) => normalizeDesktopSourceName(name)) + .filter(Boolean), + ); + const ownAppName = normalizeDesktopSourceName(app.getName()); + + const displays = includeScreens + ? [...getScreen().getAllDisplays()].sort( + (left, right) => + left.bounds.x - right.bounds.x || + left.bounds.y - right.bounds.y || + left.id - right.id, + ) + : []; + const primaryDisplayId = includeScreens ? String(getScreen().getPrimaryDisplay().id) : ""; + const electronScreenSourcesByDisplayId = new Map( + electronSources + .filter((source) => source.id.startsWith("screen:")) + .map((source) => [String(source.display_id ?? ""), source] as const), + ); + // On Linux, desktopCapturer display_id values may not match screen.getAllDisplays() IDs. + // Keep an ordered list so we can fall back to position-based matching. + const electronScreenSourcesByIndex = electronSources.filter((source) => + source.id.startsWith("screen:"), + ); + + const screenSources = displays.map((display, index) => { + const displayId = String(display.id); + const matchedSource = + electronScreenSourcesByDisplayId.get(displayId) ?? + (electronScreenSourcesByIndex.length === displays.length + ? electronScreenSourcesByIndex[index] + : undefined); + if (options?.strict && !matchedSource) + throw new Error("Unable to identify this screen. Try the picker again."); + const displayName = + displayId === primaryDisplayId + ? `Screen ${index + 1} (Primary)` + : `Screen ${index + 1}`; + + return { + id: getScreenSourceIdForDisplay({ + displayId, + env: process.env, + matchedSourceId: matchedSource?.id, + platform: process.platform, + }), + name: displayName, + originalName: matchedSource?.name ?? displayName, + display_id: displayId, + thumbnail: matchedSource?.thumbnail ? matchedSource.thumbnail.toDataURL() : null, + appIcon: null, + sourceType: "screen" as const, + }; + }); + + if (process.platform !== "darwin" || !includeWindows) { + const windowSources = electronSources + .filter((source) => source.id.startsWith("window:")) + .filter((source) => { + const normalizedName = normalizeDesktopSourceName(source.name); + if (!normalizedName) { + return true; + } + + if (ALLOW_RECORDLY_WINDOW_CAPTURE && normalizedName.includes("recordly")) { + return true; + } + + for (const ownName of ownWindowNames) { + if (!ownName) continue; + if (normalizedName === ownName) { + return false; + } + } + + return true; + }) + .map((source) => ({ + id: source.id, + name: source.name, + originalName: source.name, + display_id: source.display_id, + thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, + appIcon: includeWindowIcons && source.appIcon ? source.appIcon.toDataURL() : null, + sourceType: "window" as const, + })); + const result = [...screenSources, ...windowSources]; + sourceListCache = { + key: cacheKey, + expiresAt: Date.now() + SOURCE_LIST_CACHE_TTL_MS, + value: result, + }; + return result; + } + + try { + const nativeWindowSources = await getNativeMacWindowSources(); + const electronWindowSourceMap = new Map( + electronSources + .filter((source) => source.id.startsWith("window:")) + .map((source) => [source.id, source] as const), + ); + + const mergedWindowSources = nativeWindowSources + .filter((source) => { + const normalizedWindowName = normalizeDesktopSourceName( + source.windowTitle ?? source.name, + ); + const normalizedAppName = normalizeDesktopSourceName(source.appName ?? ""); + + if ( + !ALLOW_RECORDLY_WINDOW_CAPTURE && + normalizedAppName && + normalizedAppName === ownAppName + ) { + return false; + } + + if ( + ALLOW_RECORDLY_WINDOW_CAPTURE && + (normalizedAppName === "recordly" || normalizedWindowName?.includes("recordly")) + ) { + return true; + } + + if (!normalizedWindowName) { + return true; + } + + for (const ownName of ownWindowNames) { + if (!ownName) continue; + if (normalizedWindowName === ownName) { + return false; + } + } + + return true; + }) + .map((source) => { + const electronWindowSource = electronWindowSourceMap.get(source.id); + return { + id: source.id, + name: source.name, + originalName: source.name, + display_id: source.display_id ?? electronWindowSource?.display_id ?? "", + thumbnail: electronWindowSource?.thumbnail + ? electronWindowSource.thumbnail.toDataURL() + : null, + appIcon: includeWindowIcons + ? (source.appIcon ?? + (electronWindowSource?.appIcon + ? electronWindowSource.appIcon.toDataURL() + : null)) + : null, + appName: source.appName, + windowTitle: source.windowTitle, + bundleId: source.bundleId, + sourceType: "window" as const, + }; + }); + + const result = [...screenSources, ...mergedWindowSources]; + sourceListCache = { + key: cacheKey, + expiresAt: Date.now() + SOURCE_LIST_CACHE_TTL_MS, + value: result, + }; + return result; + } catch (error) { + console.warn("Falling back to Electron window enumeration on macOS:", error); + + const windowSources = electronSources + .filter((source) => source.id.startsWith("window:")) + .filter((source) => { + const normalizedName = normalizeDesktopSourceName(source.name); + if (!normalizedName) { + return true; + } + + if (ALLOW_RECORDLY_WINDOW_CAPTURE && normalizedName.includes("recordly")) { + return true; + } + + for (const ownName of ownWindowNames) { + if (!ownName) continue; + if ( + normalizedName === ownName || + normalizedName.includes(ownName) || + ownName.includes(normalizedName) + ) { + return false; + } + } + + return true; + }) + .map((source) => ({ + id: source.id, + name: source.name, + originalName: source.name, + display_id: source.display_id, + thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, + appIcon: includeWindowIcons && source.appIcon ? source.appIcon.toDataURL() : null, + sourceType: "window" as const, + })); + + const result = [...screenSources, ...windowSources]; + sourceListCache = { + key: cacheKey, + expiresAt: Date.now() + SOURCE_LIST_CACHE_TTL_MS, + value: result, + }; + return result; + } +} + +export function registerSourceHandlers({ + createEditorWindow, + createSourceSelectorWindow, + getSourceSelectorWindow, +}: { + createEditorWindow: () => void; + createSourceSelectorWindow: () => BrowserWindow; + getSourceSelectorWindow: () => BrowserWindow | null; +}) { + const recordingNavigation = createRecordingEditorNavigation(createEditorWindow); + const sourceListPicker = createSourceListPicker( + () => getSourceSelectorWindow() ?? createSourceSelectorWindow(), + ); + ipcMain.handle("get-sources", (_, opts) => getDesktopSources(opts)); + + ipcMain.handle("select-source", async (event, source: SelectedSource) => { + if (isCursorCaptureActive) return selectedSource; + const fromSelector = getSourceSelectorWindow()?.webContents === event.sender; + if (process.platform === "linux") { + const validated = await validateCaptureSource(source); + if (!validated) + throw new Error( + "That window or screen is no longer available. Refresh the source list.", + ); + source = validated; + } + if (source.id?.startsWith("window:")) { + await bringSelectedWindowForward(source); + } + if (isCursorCaptureActive) return selectedSource; + if (fromSelector && getSourceSelectorWindow()?.webContents !== event.sender) + return selectedSource; + setSelectedSource(source); + broadcastSelectedSourceChange(); + if (sourceListPicker.isSender(event.sender)) sourceListPicker.complete(source); + stopWindowBoundsCapture(); + const sourceSelectorWin = getSourceSelectorWindow(); + if (sourceSelectorWin) { + sourceSelectorWin.close(); + } + app.focus({ steal: true }); + return selectedSource; + }); + + ipcMain.handle("show-source-highlight", async (_, source: SelectedSource) => { + try { + const isWindow = source.id?.startsWith("window:"); + + // ── 1. Resolve bounds ── + let bounds: { x: number; y: number; width: number; height: number } | null = + getSourceArea(source); + + if (bounds) { + // An area highlights exactly the rectangle that will be recorded. + } else if (source.id?.startsWith("screen:")) { + bounds = + process.platform === "darwin" + ? getDisplayWorkAreaForSource(source) + : getDisplayBoundsForSource(source); + } else if (isWindow) { + if (process.platform === "darwin") { + bounds = await resolveMacWindowBounds(source); + } else if (process.platform === "win32") { + bounds = await resolveWindowsWindowBounds(source); + } else if (process.platform === "linux") { + bounds = await resolveLinuxWindowBounds(source); + } + } + + // A window highlight must never silently become a fullscreen highlight. + // If HWND bounds cannot be resolved, skip the animation and report the + // failure so the next selection can retry with the same window source. + if (isWindow && (!bounds || bounds.width <= 0 || bounds.height <= 0)) { + console.warn("Unable to resolve selected window bounds for highlight", { + sourceId: source.id, + platform: process.platform, + }); + return { success: false }; + } + + if (!bounds || bounds.width <= 0 || bounds.height <= 0) { + bounds = getDisplayBoundsForSource(source); + } + + if (!bounds || bounds.width <= 0 || bounds.height <= 0) { + const primaryBounds = getScreen().getPrimaryDisplay().bounds; + if (primaryBounds.width <= 0 || primaryBounds.height <= 0) { + return { success: false }; + } + bounds = primaryBounds; + } + + const resolvedBounds = bounds; + + // ── 2. Show traveling wave highlight ── + // On macOS, screen highlights use workArea and no outward padding — + // macOS clamps window positions below the menu bar so outward + // padding only works on the left/top while right/bottom run off-screen. + const isScreen = source.id?.startsWith("screen:"); + const isMacScreen = isScreen && process.platform === "darwin"; + const pad = isMacScreen ? 0 : 6; + const highlightWin = new BrowserWindow({ + x: Math.round(resolvedBounds.x - pad), + y: Math.round(resolvedBounds.y - pad), + width: Math.max(1, Math.round(resolvedBounds.width + pad * 2)), + height: Math.max(1, Math.round(resolvedBounds.height + pad * 2)), + frame: false, + transparent: true, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, + resizable: false, + focusable: false, + show: false, + ...(process.platform === "darwin" ? { type: "panel" as const } : {}), + webPreferences: { nodeIntegration: false, contextIsolation: true }, + }); + + highlightWin.setIgnoreMouseEvents(true); + highlightWin.setAlwaysOnTop(true, "screen-saver"); + if (process.platform === "darwin") { + highlightWin.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + skipTransformProcessType: true, + }); + } + + const borderRadius = isMacScreen ? 0 : 10; + const glowInset = isMacScreen ? 0 : -4; + const glowRadius = isMacScreen ? 0 : 14; + const glowPad = isMacScreen ? 3 : 6; + + const html = ` + +
+
+`; + + try { + await highlightWin.loadURL( + `data:text/html;charset=utf-8,${encodeURIComponent(html)}`, + ); + highlightWin.showInactive(); + } catch (loadError) { + if (!highlightWin.isDestroyed()) { + highlightWin.close(); + } + throw loadError; + } + + // The highlight window appearing (even with focusable:false) can corrupt + // the WS_EX_TRANSPARENT flag on the HUD on Windows 11+, breaking hover + // detection until the user moves their mouse over the bar again. + // Re-assert passthrough immediately so click-through is restored at once. + reassertHudOverlayMousePassthrough(); + + const highlightCloseTimer = setTimeout(() => { + if (!highlightWin.isDestroyed()) highlightWin.close(); + }, 1700); + + highlightWin.on("closed", () => { + clearTimeout(highlightCloseTimer); + // Re-assert once more when the window is actually destroyed so the + // native flag is clean regardless of timing. + reassertHudOverlayMousePassthrough(); + }); + + return { success: true }; + } catch (error) { + console.error("Failed to show source highlight:", error); + return { success: false }; + } + }); + + ipcMain.handle("get-selected-source", () => { + return getSessionCaptureSource(); + }); + + ipcMain.handle("open-source-selector", async () => { + if (isCursorCaptureActive) return; + if (process.platform === "linux" && isLikelyLinuxWaylandSession(process.env)) { + setSelectedSource({ + id: LINUX_PORTAL_SCREEN_SOURCE_ID, + name: "System picker", + sourceType: "screen", + }); + broadcastSelectedSourceChange(); + return; + } + const sourceSelectorWin = getSourceSelectorWindow(); + if (sourceSelectorWin) { + sourceSelectorWin.focus(); + return; + } + createSourceSelectorWindow(); + }); + ipcMain.handle("show-recording-hud", (event) => { + setHudRecordingPreparationActive(true); + recordingNavigation.setReturnWindow(BrowserWindow.fromWebContents(event.sender)); + const hud = getHudOverlayWindow(); + if (hud && !hud.isDestroyed()) { + hud.show(); + hud.focus(); + } else { + createHudOverlayWindow(); + } + }); + ipcMain.handle("show-recording-permissions", () => { + if (isCursorCaptureActive) return; + writeAppSetting("recordly.onboarding.permissionsRequested", true); + setHudRecordingPreparationActive(false); + recordingNavigation.open(false); + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed()) window.webContents.send("recording-permissions-requested"); + } + }); + ipcMain.handle("show-project-dashboard", () => { + setHudRecordingPreparationActive(false); + recordingNavigation.open(false); + }); + ipcMain.handle("switch-to-editor", () => { + setHudRecordingPreparationActive(false); + console.log("[switch-to-editor] Opening editor window"); + const sourceSelectorWin = getSourceSelectorWindow(); + if (sourceSelectorWin && !sourceSelectorWin.isDestroyed()) { + sourceSelectorWin.close(); + } + recordingNavigation.open(); + }); + return { pickSourceList: () => sourceListPicker.open() }; +} diff --git a/electron/ipc/settings/recordingPreferencesStore.test.ts b/electron/ipc/settings/recordingPreferencesStore.test.ts new file mode 100644 index 000000000..9caaeaae8 --- /dev/null +++ b/electron/ipc/settings/recordingPreferencesStore.test.ts @@ -0,0 +1,46 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createRecordingPreferencesStore } from "./recordingPreferencesStore"; + +vi.mock("electron", () => ({ + app: { + getPath: () => "", + }, +})); + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((directory) => + fs.rm(directory, { + recursive: true, + force: true, + }), + ), + ); +}); + +describe("recording preferences store", () => { + it("preserves concurrent microphone and webcam preference updates", async () => { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-preferences-")); + temporaryDirectories.push(directory); + const store = createRecordingPreferencesStore(path.join(directory, "recording.json")); + + await Promise.all([ + store.update({ microphoneEnabled: true }), + store.update({ microphoneDeviceId: "preferred-mic" }), + store.update({ webcamEnabled: true }), + store.update({ webcamDeviceId: "preferred-camera" }), + ]); + + await expect(store.read()).resolves.toEqual({ + microphoneEnabled: true, + microphoneDeviceId: "preferred-mic", + webcamEnabled: true, + webcamDeviceId: "preferred-camera", + }); + }); +}); diff --git a/electron/ipc/settings/recordingPreferencesStore.ts b/electron/ipc/settings/recordingPreferencesStore.ts new file mode 100644 index 000000000..26eced169 --- /dev/null +++ b/electron/ipc/settings/recordingPreferencesStore.ts @@ -0,0 +1,45 @@ +import fs from "node:fs/promises"; +import { parseJsonWithByteOrderMark } from "../utils"; + +export interface RecordingPreferencesPatch { + microphoneEnabled?: boolean; + microphoneDeviceId?: string; + systemAudioEnabled?: boolean; + webcamEnabled?: boolean; + webcamDeviceId?: string; +} + +export function createRecordingPreferencesStore(filePath: string) { + let operationQueue: Promise = Promise.resolve(); + + const readFile = async (): Promise> => { + try { + const content = await fs.readFile(filePath, "utf-8"); + const parsed = parseJsonWithByteOrderMark(content); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Record) + : {}; + } catch { + return {}; + } + }; + + return { + async read(): Promise> { + await operationQueue; + return readFile(); + }, + async update(patch: RecordingPreferencesPatch): Promise { + const operation = operationQueue.then(async () => { + const existing = await readFile(); + await fs.writeFile( + filePath, + JSON.stringify({ ...existing, ...patch }, null, 2), + "utf-8", + ); + }); + operationQueue = operation.catch(() => undefined); + await operation; + }, + }; +} diff --git a/electron/ipc/sourceArea.test.ts b/electron/ipc/sourceArea.test.ts new file mode 100644 index 000000000..ef245a473 --- /dev/null +++ b/electron/ipc/sourceArea.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from "vitest"; +import { + createAreaSource, + createWindowSource, + getSourceArea, + normalizeCapturePick, +} from "./sourceArea"; + +const displays = [ + { id: 1, bounds: { x: 0, y: 0, width: 1728, height: 1117 } }, + { id: 2, bounds: { x: 1728, y: -200, width: 2560, height: 1440 } }, +]; +const windows = [ + { + id: "window:42:0", + appName: "Safari", + title: "Docs", + display_id: "1", + x: 10, + y: 40, + width: 1200, + height: 800, + }, +]; + +describe("normalizeCapturePick", () => { + it("keeps an area that lies on its display", () => { + expect( + normalizeCapturePick( + { + kind: "area", + x: 100.4, + y: 50, + width: 1280, + height: 720, + displayId: 1, + record: true, + }, + displays, + windows, + ), + ).toEqual({ + kind: "area", + x: 100, + y: 50, + width: 1280, + height: 720, + displayId: 1, + record: true, + }); + }); + + it("clips an area to its display, including displays left of or above the origin", () => { + expect( + normalizeCapturePick( + { kind: "area", x: 1700, y: -300, width: 400, height: 400, displayId: 2 }, + displays, + windows, + ), + ).toEqual({ + kind: "area", + x: 1728, + y: -200, + width: 372, + height: 300, + displayId: 2, + record: false, + }); + }); + + it("trims an area to an even size so it maps onto whole screen pixels", () => { + expect( + normalizeCapturePick( + { kind: "area", x: 10, y: 20, width: 401, height: 301, displayId: 1 }, + displays, + windows, + ), + ).toMatchObject({ x: 10, y: 20, width: 400, height: 300 }); + }); + + it("rejects areas that are too small, unknown displays and malformed input", () => { + const area = { kind: "area", x: 0, y: 0, width: 20, height: 400, displayId: 1 }; + expect(normalizeCapturePick(area, displays, windows)).toBeNull(); + expect( + normalizeCapturePick({ ...area, width: 400, displayId: 9 }, displays, windows), + ).toBeNull(); + expect(normalizeCapturePick({ ...area, width: "400" }, displays, windows)).toBeNull(); + expect(normalizeCapturePick({ ...area, kind: "region" }, displays, windows)).toBeNull(); + expect(normalizeCapturePick(null, displays, windows)).toBeNull(); + }); + + it("accepts only windows the picker offered", () => { + expect( + normalizeCapturePick( + { kind: "window", windowId: "window:42:0", displayId: 1, record: true }, + displays, + windows, + ), + ).toEqual({ kind: "window", windowId: "window:42:0", displayId: 1, record: true }); + expect( + normalizeCapturePick( + { kind: "window", windowId: "window:7:0", displayId: 1 }, + displays, + windows, + ), + ).toBeNull(); + }); + + it("accepts a connected screen", () => { + expect(normalizeCapturePick({ kind: "screen", displayId: 2 }, displays, windows)).toEqual({ + kind: "screen", + displayId: 2, + record: false, + }); + }); +}); + +describe("picked sources", () => { + it("round-trips an area's rectangle", () => { + const source = createAreaSource({ + kind: "area", + x: 10, + y: 20, + width: 640, + height: 480, + displayId: 1, + record: false, + }); + expect(source).toMatchObject({ id: "area:1", name: "Area 640×480", display_id: "1" }); + expect(getSourceArea(source)).toEqual({ x: 10, y: 20, width: 640, height: 480 }); + }); + + it("builds the same window source the window list offers", () => { + expect(createWindowSource(windows[0])).toEqual({ + id: "window:42:0", + name: "Docs", + display_id: "1", + sourceType: "window", + appName: "Safari", + windowTitle: "Docs", + }); + }); + + it("ignores sources that are not areas", () => { + expect(getSourceArea({ id: "screen:1:0", name: "Screen" })).toBeNull(); + expect(getSourceArea(null)).toBeNull(); + }); +}); diff --git a/electron/ipc/sourceArea.ts b/electron/ipc/sourceArea.ts new file mode 100644 index 000000000..9e363c24d --- /dev/null +++ b/electron/ipc/sourceArea.ts @@ -0,0 +1,107 @@ +import type { + AreaCapturePick, + CaptureArea, + CapturePick, + CapturePickerWindow, + SelectedSource, +} from "./types"; + +/** Smallest area worth recording, in points. */ +export const MIN_CAPTURE_AREA_SIZE = 32; + +type PickerDisplay = { id: number; bounds: CaptureArea }; + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +/** + * Validates what a capture-picker window reports. An area must have a usable size + * and lie on the display it was drawn on; a window must be one the picker offered; + * a screen must be a connected display. + */ +export function normalizeCapturePick( + input: unknown, + displays: PickerDisplay[], + windows: CapturePickerWindow[], +): CapturePick | null { + if (!input || typeof input !== "object") return null; + const candidate = input as Record; + const record = candidate.record === true; + const display = displays.find((entry) => entry.id === candidate.displayId); + if (!display) return null; + + if (candidate.kind === "screen") { + return { kind: "screen", displayId: display.id, record }; + } + + if (candidate.kind === "window") { + const window = windows.find((entry) => entry.id === candidate.windowId); + return window + ? { kind: "window", windowId: window.id, displayId: display.id, record } + : null; + } + + if (candidate.kind !== "area") return null; + const { x, y, width, height } = candidate; + if ( + !isFiniteNumber(x) || + !isFiniteNumber(y) || + !isFiniteNumber(width) || + !isFiniteNumber(height) + ) { + return null; + } + const left = Math.max(display.bounds.x, Math.round(x)); + const top = Math.max(display.bounds.y, Math.round(y)); + const right = Math.min(display.bounds.x + display.bounds.width, Math.round(x + width)); + const bottom = Math.min(display.bounds.y + display.bounds.height, Math.round(y + height)); + // Whole, even sizes map the area onto screen pixels one to one at any display + // scale, so the recording is never resampled. + const areaWidth = right - left - ((right - left) % 2); + const areaHeight = bottom - top - ((bottom - top) % 2); + if (areaWidth < MIN_CAPTURE_AREA_SIZE || areaHeight < MIN_CAPTURE_AREA_SIZE) { + return null; + } + + return { + kind: "area", + x: left, + y: top, + width: areaWidth, + height: areaHeight, + displayId: display.id, + record, + }; +} + +export function createAreaSource(pick: AreaCapturePick): SelectedSource { + return { + id: `area:${pick.displayId}`, + name: `Area ${pick.width}×${pick.height}`, + display_id: String(pick.displayId), + sourceType: "area", + area: { x: pick.x, y: pick.y, width: pick.width, height: pick.height }, + }; +} + +/** The same source the window list offers for this window. */ +export function createWindowSource(window: CapturePickerWindow): SelectedSource { + const title = window.title || window.appName || "Window"; + return { + id: window.id, + name: title, + display_id: window.display_id ?? "", + sourceType: "window", + appName: window.appName || undefined, + windowTitle: title, + }; +} + +export function getSourceArea(source: SelectedSource | null | undefined): CaptureArea | null { + if (!source?.id?.startsWith("area:") || !source.area) return null; + const { x, y, width, height } = source.area; + return [x, y, width, height].every(Number.isFinite) && width > 0 && height > 0 + ? { x, y, width, height } + : null; +} diff --git a/electron/ipc/state.ts b/electron/ipc/state.ts new file mode 100644 index 000000000..a0a41744e --- /dev/null +++ b/electron/ipc/state.ts @@ -0,0 +1,293 @@ +import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import type { + CursorInteractionType, + CursorTelemetryPoint, + CursorVisualType, + NativeCaptureDiagnostics, + RecordingSessionData, + SelectedSource, + SystemCursorAsset, + WindowBounds, +} from "./types"; + +// ── Source selection ────────────────────────────────────────────────────────── +export let selectedSource: SelectedSource | null = null; + +// ── Project / video state ───────────────────────────────────────────────────── +export let currentProjectPath: string | null = null; +export let currentVideoPath: string | null = null; +export let currentRecordingSession: RecordingSessionData | null = null; + +// ── Security: approved read paths ───────────────────────────────────────────── +export const approvedLocalReadPaths = new Set(); + +// ── Native macOS capture ────────────────────────────────────────────────────── +export let nativeScreenRecordingActive = false; +export let nativeCaptureProcess: ChildProcessWithoutNullStreams | null = null; +export let nativeCaptureOutputBuffer = ""; +export let nativeCaptureTargetPath: string | null = null; +export let nativeCaptureStopRequested = false; +export let nativeCaptureSystemAudioPath: string | null = null; +export let nativeCaptureMicrophonePath: string | null = null; +export let nativeCapturePaused = false; + +// ── Native cursor monitor ───────────────────────────────────────────────────── +export let nativeCursorMonitorProcess: ChildProcessWithoutNullStreams | null = null; +export let nativeCursorMonitorOutputBuffer = ""; + +// ── Windows native capture ──────────────────────────────────────────────────── +export let windowsCaptureProcess: ChildProcessWithoutNullStreams | null = null; +export let windowsCaptureOutputBuffer = ""; +export let windowsCaptureTargetPath: string | null = null; +export let windowsNativeCaptureActive = false; +export let windowsCaptureStopRequested = false; +export let windowsCapturePaused = false; +export let windowsSystemAudioPath: string | null = null; +export let windowsMicAudioPath: string | null = null; +export let windowsOrphanedMicAudioPath: string | null = null; +export let windowsPendingVideoPath: string | null = null; + +// ── Diagnostics ─────────────────────────────────────────────────────────────── +export let lastNativeCaptureDiagnostics: NativeCaptureDiagnostics | null = null; + +// ── FFmpeg capture ──────────────────────────────────────────────────────────── +export let ffmpegScreenRecordingActive = false; +export let ffmpegCaptureProcess: ChildProcessWithoutNullStreams | null = null; +export let ffmpegCaptureOutputBuffer = ""; +export let ffmpegCaptureTargetPath: string | null = null; + +// ── Recordings directory ────────────────────────────────────────────────────── +export let customRecordingsDir: string | null = null; +export let recordingsDirLoaded = false; + +// ── System cursor assets cache ──────────────────────────────────────────────── +export let cachedSystemCursorAssets: Record | null = null; +export let cachedSystemCursorAssetsSourceMtimeMs: number | null = null; + +// ── Countdown ───────────────────────────────────────────────────────────────── +export let countdownTimer: ReturnType | null = null; +export let countdownCancelled = false; +export let countdownInProgress = false; +export let countdownRemaining: number | null = null; + +// ── Cursor visual type ──────────────────────────────────────────────────────── +export let currentCursorVisualType: CursorVisualType | undefined = undefined; + +// ── Cursor telemetry ────────────────────────────────────────────────────────── +export let cursorCaptureInterval: NodeJS.Timeout | null = null; +export let cursorCaptureStartTimeMs = 0; +export let cursorCaptureAccumulatedPausedMs = 0; +export let cursorCapturePauseStartedAtMs: number | null = null; +export let activeCursorSamples: CursorTelemetryPoint[] = []; +export let pendingCursorSamples: CursorTelemetryPoint[] = []; +export let isCursorCaptureActive = false; +export let interactionCaptureCleanup: (() => void) | null = null; +export let hasLoggedInteractionHookFailure = false; +export let lastLeftClick: { timeMs: number; cx: number; cy: number } | null = null; +export let linuxCursorScreenPoint: { x: number; y: number; updatedAt: number } | null = null; +export let selectedWindowBounds: WindowBounds | null = null; +export let windowBoundsCaptureInterval: NodeJS.Timeout | null = null; + +// ── Native macOS window source cache ───────────────────────────────────────── +export let cachedNativeMacWindowSources: import("./types").NativeMacWindowSource[] | null = null; +export let cachedNativeMacWindowSourcesAtMs = 0; + +// ── Native video export ─────────────────────────────────────────────────────── +export let cachedNativeVideoEncoder: { + ffmpegPath: string; + encodingMode: string; + encoderName: string; +} | null = null; + +// ── Native helper migration ─────────────────────────────────────────────────── +export let nativeHelperMigrationPromise: Promise | null = null; + +// ── Cursor interaction capture types ───────────────────────────────────────── +export type { CursorInteractionType, CursorTelemetryPoint }; + +// ── Setters (for modules that need to reassign exported lets) ───────────────── +// TypeScript exported `let` can be reassigned by the owning module but importers +// cannot assign to them directly. Provide simple setters for cross-module writes. + +export function setSelectedSource(v: SelectedSource | null) { + selectedSource = v; +} +export function setCurrentProjectPath(v: string | null) { + currentProjectPath = v; +} +export function setCurrentVideoPath(v: string | null) { + currentVideoPath = v; +} +export function setCurrentRecordingSession(v: RecordingSessionData | null) { + currentRecordingSession = v; +} + +export function setNativeScreenRecordingActive(v: boolean) { + nativeScreenRecordingActive = v; +} +export function setNativeCaptureProcess(v: ChildProcessWithoutNullStreams | null) { + nativeCaptureProcess = v; +} +export function setNativeCaptureOutputBuffer(v: string) { + nativeCaptureOutputBuffer = v; +} +export function setNativeCaptureTargetPath(v: string | null) { + nativeCaptureTargetPath = v; +} +export function setNativeCaptureStopRequested(v: boolean) { + nativeCaptureStopRequested = v; +} +export function setNativeCaptureSystemAudioPath(v: string | null) { + nativeCaptureSystemAudioPath = v; +} +export function setNativeCaptureMicrophonePath(v: string | null) { + nativeCaptureMicrophonePath = v; +} +export function setNativeCapturePaused(v: boolean) { + nativeCapturePaused = v; +} + +export function setNativeCursorMonitorProcess(v: ChildProcessWithoutNullStreams | null) { + nativeCursorMonitorProcess = v; +} +export function setNativeCursorMonitorOutputBuffer(v: string) { + nativeCursorMonitorOutputBuffer = v; +} + +export function setWindowsCaptureProcess(v: ChildProcessWithoutNullStreams | null) { + windowsCaptureProcess = v; +} +export function setWindowsCaptureOutputBuffer(v: string) { + windowsCaptureOutputBuffer = v; +} +export function setWindowsCaptureTargetPath(v: string | null) { + windowsCaptureTargetPath = v; +} +export function setWindowsNativeCaptureActive(v: boolean) { + windowsNativeCaptureActive = v; +} +export function setWindowsCaptureStopRequested(v: boolean) { + windowsCaptureStopRequested = v; +} +export function setWindowsCapturePaused(v: boolean) { + windowsCapturePaused = v; +} +export function setWindowsSystemAudioPath(v: string | null) { + windowsSystemAudioPath = v; +} +export function setWindowsMicAudioPath(v: string | null) { + windowsMicAudioPath = v; +} +export function setWindowsOrphanedMicAudioPath(v: string | null) { + windowsOrphanedMicAudioPath = v; +} +export function setWindowsPendingVideoPath(v: string | null) { + windowsPendingVideoPath = v; +} + +export function setLastNativeCaptureDiagnostics(v: NativeCaptureDiagnostics | null) { + lastNativeCaptureDiagnostics = v; +} + +export function setFfmpegScreenRecordingActive(v: boolean) { + ffmpegScreenRecordingActive = v; +} +export function setFfmpegCaptureProcess(v: ChildProcessWithoutNullStreams | null) { + ffmpegCaptureProcess = v; +} +export function setFfmpegCaptureOutputBuffer(v: string) { + ffmpegCaptureOutputBuffer = v; +} +export function setFfmpegCaptureTargetPath(v: string | null) { + ffmpegCaptureTargetPath = v; +} + +export function setCustomRecordingsDir(v: string | null) { + customRecordingsDir = v; +} +export function setRecordingsDirLoaded(v: boolean) { + recordingsDirLoaded = v; +} + +export function setCachedSystemCursorAssets(v: Record | null) { + cachedSystemCursorAssets = v; +} +export function setCachedSystemCursorAssetsSourceMtimeMs(v: number | null) { + cachedSystemCursorAssetsSourceMtimeMs = v; +} + +export function setCountdownTimer(v: ReturnType | null) { + countdownTimer = v; +} +export function setCountdownCancelled(v: boolean) { + countdownCancelled = v; +} +export function setCountdownInProgress(v: boolean) { + countdownInProgress = v; +} +export function setCountdownRemaining(v: number | null) { + countdownRemaining = v; +} + +export function setCurrentCursorVisualType(v: CursorVisualType | undefined) { + currentCursorVisualType = v; +} + +export function setCursorCaptureInterval(v: NodeJS.Timeout | null) { + cursorCaptureInterval = v; +} +export function setCursorCaptureStartTimeMs(v: number) { + cursorCaptureStartTimeMs = v; +} +export function setCursorCaptureAccumulatedPausedMs(v: number) { + cursorCaptureAccumulatedPausedMs = v; +} +export function setCursorCapturePauseStartedAtMs(v: number | null) { + cursorCapturePauseStartedAtMs = v; +} +export function setActiveCursorSamples(v: CursorTelemetryPoint[]) { + activeCursorSamples = v; +} +export function setPendingCursorSamples(v: CursorTelemetryPoint[]) { + pendingCursorSamples = v; +} +export function setIsCursorCaptureActive(v: boolean) { + isCursorCaptureActive = v; +} +export function setInteractionCaptureCleanup(v: (() => void) | null) { + interactionCaptureCleanup = v; +} +export function setHasLoggedInteractionHookFailure(v: boolean) { + hasLoggedInteractionHookFailure = v; +} +export function setLastLeftClick(v: { timeMs: number; cx: number; cy: number } | null) { + lastLeftClick = v; +} +export function setLinuxCursorScreenPoint(v: { x: number; y: number; updatedAt: number } | null) { + linuxCursorScreenPoint = v; +} +export function setSelectedWindowBounds(v: WindowBounds | null) { + selectedWindowBounds = v; +} +export function setWindowBoundsCaptureInterval(v: NodeJS.Timeout | null) { + windowBoundsCaptureInterval = v; +} + +export function setCachedNativeMacWindowSources( + v: import("./types").NativeMacWindowSource[] | null, +) { + cachedNativeMacWindowSources = v; +} +export function setCachedNativeMacWindowSourcesAtMs(v: number) { + cachedNativeMacWindowSourcesAtMs = v; +} + +export function setCachedNativeVideoEncoder( + v: { ffmpegPath: string; encodingMode: string; encoderName: string } | null, +) { + cachedNativeVideoEncoder = v; +} + +export function setNativeHelperMigrationPromise(v: Promise | null) { + nativeHelperMigrationPromise = v; +} diff --git a/electron/ipc/types.ts b/electron/ipc/types.ts new file mode 100644 index 000000000..5d90df63f --- /dev/null +++ b/electron/ipc/types.ts @@ -0,0 +1,236 @@ +/** A rectangle in global display points (top-left origin). */ +export type CaptureArea = { + x: number; + y: number; + width: number; + height: number; +}; + +export type SelectedSource = { + id?: string; + name: string; + display_id?: string; + sourceType?: "screen" | "window" | "area"; + appName?: string; + windowTitle?: string; + /** For area sources, the recorded part of the display. */ + area?: CaptureArea; + [key: string]: unknown; +}; + +/** A window the capture picker can highlight, framed in global display points. */ +export type CapturePickerWindow = CaptureArea & { + /** The window's source id, `window::0`. */ + id: string; + appName: string; + title: string; + display_id?: string; +}; + +/** + * What the capture picker reports when the user confirms: an area they drew, a + * window they clicked, or a whole screen. `record` starts recording right away. + */ +export type CapturePick = + | (CaptureArea & { kind: "area"; displayId: number; record: boolean }) + | { kind: "window"; windowId: string; displayId: number; record: boolean } + | { kind: "screen"; displayId: number; record: boolean }; + +export type AreaCapturePick = Extract; + +export type NativeMacRecordingOptions = { + capturesSystemAudio?: boolean; + capturesMicrophone?: boolean; + microphoneDeviceId?: string; + microphoneLabel?: string; +}; + +export type WindowBounds = { + x: number; + y: number; + width: number; + height: number; +}; + +export type NativeCaptureDiagnostics = { + backend: "windows-wgc" | "mac-screencapturekit" | "browser-store" | "ffmpeg"; + phase: "availability" | "start" | "stop" | "mux"; + timestamp: string; + sourceId?: string | null; + sourceType?: SelectedSource["sourceType"] | "unknown"; + displayId?: number | null; + displayBounds?: WindowBounds | null; + windowHandle?: number | null; + helperPath?: string | null; + outputPath?: string | null; + systemAudioPath?: string | null; + microphonePath?: string | null; + osRelease?: string; + supported?: boolean; + helperExists?: boolean; + fileSizeBytes?: number | null; + processOutput?: string; + error?: string; +}; + +export type RecordingSessionData = { + videoPath: string; + webcamPath?: string | null; + timeOffsetMs?: number; + hideOverlayCursorByDefault?: boolean; +}; + +export type PauseSegment = { + startMs: number; + endMs: number; +}; + +export type RecordingSessionManifest = { + version: 1 | 2; + videoFileName: string; + webcamFileName?: string | null; + timeOffsetMs?: number; +}; + +export type ProjectLibraryEntry = { + path: string; + name: string; + createdAt?: number; + updatedAt: number; + thumbnailPath: string | null; + isCurrent: boolean; + isInProjectsDirectory: boolean; +}; + +export type SystemCursorAsset = { + dataUrl: string; + hotspotX: number; + hotspotY: number; + width: number; + height: number; +}; + +export type CursorVisualType = + | "arrow" + | "text" + | "pointer" + | "crosshair" + | "open-hand" + | "closed-hand" + | "resize-ew" + | "resize-ns" + | "not-allowed"; + +export type CursorInteractionType = + | "move" + | "click" + | "double-click" + | "right-click" + | "middle-click" + | "mouseup"; + +export interface CursorTelemetryPoint { + timeMs: number; + cx: number; + cy: number; + interactionType?: CursorInteractionType; + cursorType?: CursorVisualType; +} + +export type NativeMacWindowSource = { + id: string; + name: string; + display_id?: string; + appName?: string; + windowTitle?: string; + bundleId?: string; + appIcon?: string | null; + x?: number; + y?: number; + width?: number; + height?: number; + /** Process that owns the window. */ + ownerPid?: number; +}; + +export type HookEventName = "mousedown" | "mouseup" | "mousemove"; + +export type HookMouseEvent = { + button?: number; + mouseButton?: number; + x?: number; + y?: number; + screenX?: number; + screenY?: number; + data?: { + button?: number; + mouseButton?: number; + x?: number; + y?: number; + screenX?: number; + screenY?: number; + }; +}; + +export type HookEventListener = (event: HookMouseEvent) => void; + +export type UiohookLike = { + on: (eventName: HookEventName, listener: HookEventListener) => void; + off?: (eventName: HookEventName, listener: HookEventListener) => void; + removeListener?: (eventName: HookEventName, listener: HookEventListener) => void; + start: () => void; + stop?: () => void; +}; + +export type UiohookModuleNamespace = { + uIOhook?: UiohookLike; + uiohook?: UiohookLike; + Uiohook?: UiohookLike; + default?: UiohookLike | UiohookModuleNamespace; +}; + +export type AudioSyncAdjustment = { + mode: "none" | "tempo" | "delay" | "pad"; + delayMs: number; + tempoRatio: number; + durationDeltaMs: number; +}; + +export type CompanionAudioCandidate = { + platform: "mac" | "win"; + systemPath: string; + micPath: string; + usablePaths: string[]; +}; + +export type CaptionWordPayload = { + text: string; + startMs: number; + endMs: number; + leadingSpace?: boolean; +}; + +export type CaptionCuePayload = { + id: string; + startMs: number; + endMs: number; + text: string; + words?: CaptionWordPayload[]; +}; + +export type WhisperJsonToken = { + text?: unknown; + offsets?: { + from?: unknown; + to?: unknown; + }; +}; + +export type WhisperJsonSegment = { + text?: unknown; + offsets?: { + from?: unknown; + to?: unknown; + }; + tokens?: unknown; +}; diff --git a/electron/ipc/utils.ts b/electron/ipc/utils.ts new file mode 100644 index 000000000..af6b3c2bd --- /dev/null +++ b/electron/ipc/utils.ts @@ -0,0 +1,132 @@ +import { getLocalMediaServerPath } from "../../src/lib/localMediaUrl"; +import fs from "node:fs/promises"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { app } from "electron"; +import { RECORDINGS_DIR } from "../appPaths"; +import { AUTO_RECORDING_PREFIX, RECORDINGS_SETTINGS_FILE } from "./constants"; +import { + approvedLocalReadPaths, + customRecordingsDir, + recordingsDirLoaded, + setCustomRecordingsDir, + setRecordingsDirLoaded, +} from "./state"; + +const nodeRequire = createRequire(import.meta.url); + +export function getScreen() { + if (!app.isReady()) { + throw new Error( + "getScreen() called before app is ready. Ensure all screen access happens after app.whenReady().", + ); + } + return nodeRequire("electron").screen as typeof import("electron").screen; +} + +export function normalizePath(filePath: string) { + return path.resolve(filePath); +} + +export function normalizeVideoSourcePath(videoPath?: string | null): string | null { + if (typeof videoPath !== "string") { + return null; + } + + const trimmed = videoPath.trim(); + if (!trimmed) { + return null; + } + + if (/^file:\/\//i.test(trimmed)) { + try { + return fileURLToPath(trimmed); + } catch { + // Fall through and keep best-effort string path below. + } + } + + return getLocalMediaServerPath(trimmed) ?? trimmed; +} + +export function stripJsonByteOrderMark(content: string) { + return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content; +} + +export function parseJsonWithByteOrderMark(content: string): T { + return JSON.parse(stripJsonByteOrderMark(content)) as T; +} + +export function parseWindowId(sourceId?: string) { + if (!sourceId) return null; + const match = sourceId.match(/^window:(\d+)/); + return match ? Number.parseInt(match[1], 10) : null; +} + +export function getTelemetryPathForVideo(videoPath: string) { + return `${videoPath}.cursor.json`; +} + +export function isAutoRecordingPath(filePath: string) { + return path.basename(filePath).startsWith(AUTO_RECORDING_PREFIX); +} + +export async function moveFileWithOverwrite(sourcePath: string, destinationPath: string) { + await fs.mkdir(path.dirname(destinationPath), { recursive: true }); + await fs.rm(destinationPath, { force: true }); + + try { + await fs.rename(sourcePath, destinationPath); + } catch (error) { + const nodeError = error as NodeJS.ErrnoException; + if (nodeError.code !== "EXDEV") { + throw error; + } + + await fs.copyFile(sourcePath, destinationPath); + await fs.unlink(sourcePath); + } +} + +async function loadRecordingsDirectorySetting() { + if (recordingsDirLoaded) { + return; + } + + setRecordingsDirLoaded(true); + + try { + const content = await fs.readFile(RECORDINGS_SETTINGS_FILE, "utf-8"); + const parsed = parseJsonWithByteOrderMark<{ recordingsDir?: unknown }>(content); + if (typeof parsed.recordingsDir === "string" && parsed.recordingsDir.trim()) { + setCustomRecordingsDir(path.resolve(parsed.recordingsDir)); + } + } catch { + setCustomRecordingsDir(null); + } +} + +export async function getRecordingsDir() { + await loadRecordingsDirectorySetting(); + const targetDir = customRecordingsDir ?? RECORDINGS_DIR; + await fs.mkdir(targetDir, { recursive: true }); + return targetDir; +} + +export function getMacPrivacySettingsUrl(pane: "screen" | "accessibility" | "microphone"): string { + if (pane === "screen") + return "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"; + if (pane === "microphone") + return "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"; + return "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"; +} + +export function approveUserPath(filePath: string | null | undefined): void { + if (!filePath) return; + try { + approvedLocalReadPaths.add(path.resolve(filePath)); + } catch { + // Ignore invalid paths; later reads will surface the underlying error. + } +} diff --git a/electron/ipc/windowsCaptureSelection.test.ts b/electron/ipc/windowsCaptureSelection.test.ts new file mode 100644 index 000000000..cfe954101 --- /dev/null +++ b/electron/ipc/windowsCaptureSelection.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from "vitest"; + +import { + convertPhysicalBoundsToDip, + resolveWindowsCaptureDisplay, + resolveWindowsCaptureTarget, +} from "./windowsCaptureSelection"; + +describe("convertPhysicalBoundsToDip", () => { + it("converts both physical edges so mixed-DPI window sizes stay aligned", () => { + expect( + convertPhysicalBoundsToDip({ x: 240, y: 60, width: 938, height: 1030 }, (point) => ({ + x: point.x / 1.5, + y: point.y / 1.5, + })), + ).toEqual({ + x: 160, + y: 40, + width: 625.3333333333334, + height: 686.6666666666666, + }); + }); +}); + +describe("resolveWindowsCaptureDisplay", () => { + const primaryDisplay = { + id: 101, + bounds: { + x: 0, + y: 0, + width: 1920, + height: 1080, + }, + }; + + const secondaryDisplay = { + id: 202, + bounds: { + x: 1920, + y: -40, + width: 2560, + height: 1440, + }, + }; + + it("uses the requested secondary display bounds for WGC fallback metadata", () => { + const resolved = resolveWindowsCaptureDisplay( + { display_id: String(secondaryDisplay.id) }, + [primaryDisplay, secondaryDisplay], + primaryDisplay, + ); + + expect(resolved).toEqual({ + displayId: secondaryDisplay.id, + bounds: secondaryDisplay.bounds, + }); + }); + + it("falls back to the primary display when the source has no display id", () => { + const resolved = resolveWindowsCaptureDisplay( + undefined, + [primaryDisplay, secondaryDisplay], + primaryDisplay, + ); + + expect(resolved).toEqual({ + displayId: primaryDisplay.id, + bounds: primaryDisplay.bounds, + }); + }); + + it("keeps the requested display id even if Electron cannot rematch it, while using primary bounds", () => { + const resolved = resolveWindowsCaptureDisplay( + { display_id: "303" }, + [primaryDisplay, secondaryDisplay], + primaryDisplay, + ); + + expect(resolved).toEqual({ + displayId: 303, + bounds: primaryDisplay.bounds, + }); + }); +}); + +describe("resolveWindowsCaptureTarget", () => { + const primaryDisplay = { + id: 101, + bounds: { + x: 0, + y: 0, + width: 1920, + height: 1080, + }, + }; + + const secondaryDisplay = { + id: 202, + bounds: { + x: 1920, + y: -40, + width: 2560, + height: 1440, + }, + }; + + it("uses a window handle when a Windows window source is selected", () => { + const resolved = resolveWindowsCaptureTarget( + { id: "window:123456:0", sourceType: "window" }, + [primaryDisplay, secondaryDisplay], + primaryDisplay, + ); + + expect(resolved).toEqual({ + kind: "window", + windowHandle: 123456, + }); + }); + + it("does not silently turn an invalid window source into display capture", () => { + const resolved = resolveWindowsCaptureTarget( + { id: "window:0:0", sourceType: "window", display_id: String(secondaryDisplay.id) }, + [primaryDisplay, secondaryDisplay], + primaryDisplay, + ); + + expect(resolved).toEqual({ + kind: "invalid-window", + }); + }); + + it("keeps display capture behavior for selected screens", () => { + const resolved = resolveWindowsCaptureTarget( + { id: "screen:202:0", sourceType: "screen", display_id: String(secondaryDisplay.id) }, + [primaryDisplay, secondaryDisplay], + primaryDisplay, + ); + + expect(resolved).toEqual({ + kind: "display", + displayId: secondaryDisplay.id, + bounds: secondaryDisplay.bounds, + }); + }); +}); diff --git a/electron/ipc/windowsCaptureSelection.ts b/electron/ipc/windowsCaptureSelection.ts new file mode 100644 index 000000000..ac88c65e1 --- /dev/null +++ b/electron/ipc/windowsCaptureSelection.ts @@ -0,0 +1,120 @@ +export type WindowsCaptureSourceLike = { + id?: string; + display_id?: string; + sourceType?: string; +}; + +export type WindowsCaptureDisplayBounds = { + x: number; + y: number; + width: number; + height: number; +}; + +export type WindowsCaptureDisplayLike = { + id: number; + bounds: WindowsCaptureDisplayBounds; +}; + +export type ResolvedWindowsCaptureDisplay = { + displayId: number; + bounds: WindowsCaptureDisplayBounds; +}; + +export function convertPhysicalBoundsToDip( + bounds: WindowsCaptureDisplayBounds, + convertPoint: (point: { x: number; y: number }) => { x: number; y: number }, +): WindowsCaptureDisplayBounds { + const topLeft = convertPoint({ x: bounds.x, y: bounds.y }); + const bottomRight = convertPoint({ + x: bounds.x + bounds.width, + y: bounds.y + bounds.height, + }); + + return { + x: topLeft.x, + y: topLeft.y, + width: Math.max(1, bottomRight.x - topLeft.x), + height: Math.max(1, bottomRight.y - topLeft.y), + }; +} + +export type ResolvedWindowsCaptureTarget = + | { + kind: "window"; + windowHandle: number; + } + | { + kind: "display"; + displayId: number; + bounds: WindowsCaptureDisplayBounds; + } + | { + kind: "invalid-window"; + }; + +function parseDesktopCapturerWindowHandle(sourceId?: string) { + if (!sourceId) { + return null; + } + + const match = sourceId.match(/^window:(\d+)/); + if (!match) { + return null; + } + + const handle = Number.parseInt(match[1], 10); + return Number.isFinite(handle) && handle > 0 ? handle : null; +} + +function isWindowCaptureSource(source: WindowsCaptureSourceLike | null | undefined) { + return source?.sourceType === "window" || source?.id?.startsWith("window:") === true; +} + +export function resolveWindowsCaptureDisplay( + source: WindowsCaptureSourceLike | null | undefined, + allDisplays: WindowsCaptureDisplayLike[], + primaryDisplay: WindowsCaptureDisplayLike, +): ResolvedWindowsCaptureDisplay { + const requestedDisplayId = Number(source?.display_id); + const primaryDisplayId = Number(primaryDisplay.id); + const requestedOrPrimaryDisplayId = + Number.isFinite(requestedDisplayId) && requestedDisplayId > 0 + ? requestedDisplayId + : primaryDisplayId; + + const matchedDisplay = + allDisplays.find((display) => String(display.id) === String(requestedOrPrimaryDisplayId)) ?? + primaryDisplay; + + return { + displayId: requestedOrPrimaryDisplayId, + bounds: matchedDisplay.bounds, + }; +} + +export function resolveWindowsCaptureTarget( + source: WindowsCaptureSourceLike | null | undefined, + allDisplays: WindowsCaptureDisplayLike[], + primaryDisplay: WindowsCaptureDisplayLike, +): ResolvedWindowsCaptureTarget { + if (isWindowCaptureSource(source)) { + const windowHandle = parseDesktopCapturerWindowHandle(source?.id); + if (windowHandle !== null) { + return { + kind: "window", + windowHandle, + }; + } + + return { + kind: "invalid-window", + }; + } + + const resolvedDisplay = resolveWindowsCaptureDisplay(source, allDisplays, primaryDisplay); + return { + kind: "display", + ...resolvedDisplay, + }; +} diff --git a/electron/ipc/windowsWindowControl.ts b/electron/ipc/windowsWindowControl.ts new file mode 100644 index 000000000..9c4c43e8d --- /dev/null +++ b/electron/ipc/windowsWindowControl.ts @@ -0,0 +1,100 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import type { SelectedSource, WindowBounds } from "./types"; +import { getScreen, parseWindowId } from "./utils"; +import { convertPhysicalBoundsToDip } from "./windowsCaptureSelection"; + +const execFileAsync = promisify(execFile); + +export async function bringWindowsWindowForward(windowId: number): Promise { + const script = [ + 'Add-Type -TypeDefinition @"', + "using System; using System.Runtime.InteropServices;", + "public static class RecordlyForegroundWindow {", + ' [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);', + ' [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);', + ' [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);', + "}", + '"@', + "$handle = [IntPtr][Int64]$env:RECORDLY_WINDOW_ID", + "if ([RecordlyForegroundWindow]::IsIconic($handle)) { [RecordlyForegroundWindow]::ShowWindowAsync($handle, 9) | Out-Null }", + "[RecordlyForegroundWindow]::SetForegroundWindow($handle) | Out-Null", + ].join("\n"); + + await execFileAsync("powershell.exe", ["-NoProfile", "-Command", script], { + timeout: 5000, + env: { ...process.env, RECORDLY_WINDOW_ID: String(windowId) }, + }); +} + +export async function resolveWindowsWindowBounds( + source: SelectedSource, +): Promise { + const windowId = parseWindowId(source.id); + const windowTitle = + typeof source.windowTitle === "string" ? source.windowTitle.trim() : source.name.trim(); + + if (!windowId && !windowTitle) return null; + + const script = [ + "$windowId = $env:RECORDLY_WINDOW_ID", + "$windowTitle = $env:RECORDLY_WINDOW_TITLE", + 'Add-Type -TypeDefinition @"', + "using System;", + "using System.Runtime.InteropServices;", + "public static class RecordlyWindowBounds {", + " [StructLayout(LayoutKind.Sequential)]", + " public struct RECT {", + " public int Left;", + " public int Top;", + " public int Right;", + " public int Bottom;", + " }", + ' [DllImport("user32.dll")]', + " [return: MarshalAs(UnmanagedType.Bool)]", + " public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);", + ' [DllImport("dwmapi.dll")]', + " public static extern int DwmGetWindowAttribute(IntPtr hWnd, int attribute, out RECT rect, int size);", + ' [DllImport("user32.dll")]', + " public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);", + "}", + '"@', + "$handle = [Int64]0", + "if ($windowId) { $handle = [Int64]$windowId }", + "$escapedWindowTitle = if ($windowTitle) { [WildcardPattern]::Escape($windowTitle) } else { $null }", + "if ($handle -le 0 -and $windowTitle) {", + ' $matchingProcess = Get-Process | Where-Object { $_.MainWindowTitle -eq $windowTitle -or ($escapedWindowTitle -and $_.MainWindowTitle -like "*$escapedWindowTitle*") } | Select-Object -First 1', + " if ($matchingProcess) { $handle = $matchingProcess.MainWindowHandle.ToInt64() }", + "}", + "if ($handle -le 0) { exit 1 }", + "$rect = New-Object RecordlyWindowBounds+RECT", + "[RecordlyWindowBounds]::SetThreadDpiAwarenessContext([IntPtr](-4)) | Out-Null", + "$dwmResult = [RecordlyWindowBounds]::DwmGetWindowAttribute([IntPtr]$handle, 9, [ref]$rect, [Runtime.InteropServices.Marshal]::SizeOf($rect))", + "if ($dwmResult -ne 0 -and -not [RecordlyWindowBounds]::GetWindowRect([IntPtr]$handle, [ref]$rect)) { exit 1 }", + "@{ x = $rect.Left; y = $rect.Top; width = $rect.Right - $rect.Left; height = $rect.Bottom - $rect.Top } | ConvertTo-Json -Compress", + ].join("\n"); + + try { + const { stdout } = await execFileAsync( + "powershell.exe", + ["-NoProfile", "-Command", script], + { + timeout: 5000, + env: { + ...process.env, + RECORDLY_WINDOW_ID: String(windowId ?? ""), + RECORDLY_WINDOW_TITLE: windowTitle, + }, + }, + ); + const bounds = JSON.parse(stdout) as WindowBounds; + if (!bounds || bounds.width <= 0 || bounds.height <= 0) return null; + + const electronScreen = getScreen(); + return typeof electronScreen.screenToDipPoint === "function" + ? convertPhysicalBoundsToDip(bounds, (point) => electronScreen.screenToDipPoint(point)) + : bounds; + } catch { + return null; + } +} diff --git a/electron/macosDistributionPolicy.test.mjs b/electron/macosDistributionPolicy.test.mjs new file mode 100644 index 000000000..8fd6f05cd --- /dev/null +++ b/electron/macosDistributionPolicy.test.mjs @@ -0,0 +1,139 @@ +import { describe, expect, it } from "vitest"; +import { + assertValidAppleTeamId, + collectArchitectureErrors, + collectCodeSigningMetadataErrors, + collectEntitlementErrors, + expectedMachOArchitecture, + hasMachOMagic, + parseLipoArchitectures, +} from "../scripts/macos-distribution-policy.mjs"; + +const validCodeSigningDetails = ` +Identifier=dev.recordly.app +CodeDirectory v=20500 size=123 flags=0x10000(runtime) hashes=3+7 location=embedded +Authority=Developer ID Application: Recordly Developer (A1B2C3D4E5) +Authority=Developer ID Certification Authority +Authority=Apple Root CA +Timestamp=Aug 9, 2026 at 10:00:00 +TeamIdentifier=A1B2C3D4E5 +`; + +describe("macOS distribution signing policy", () => { + it("accepts a valid Developer ID signature", () => { + expect(collectCodeSigningMetadataErrors(validCodeSigningDetails, "A1B2C3D4E5")).toEqual([]); + }); + + it("rejects a wrong identity, team, timestamp, runtime, and bundle identifier", () => { + const details = ` +Identifier=dev.example.app +CodeDirectory v=20400 size=123 flags=0x0(none) +Authority=Apple Development: Example (Z9Y8X7W6V5) +Timestamp=none +TeamIdentifier=Z9Y8X7W6V5 +`; + + expect(collectCodeSigningMetadataErrors(details, "A1B2C3D4E5")).toEqual([ + "unexpected bundle identifier: dev.example.app", + "the leaf signing authority is not Developer ID Application", + "unexpected TeamIdentifier: Z9Y8X7W6V5", + "secure signing timestamp is missing", + "hardened runtime flag is missing", + ]); + }); + + it("validates the expected Apple team ID shape", () => { + expect(() => assertValidAppleTeamId("A1B2C3D4E5")).not.toThrow(); + expect(() => assertValidAppleTeamId("short")).toThrow(/exactly 10/); + }); +}); + +describe("macOS distribution entitlement policy", () => { + const validEntitlements = { + "com.apple.security.cs.allow-jit": true, + "com.apple.security.cs.allow-unsigned-executable-memory": true, + "com.apple.security.cs.disable-library-validation": true, + "com.apple.security.device.audio-input": true, + "com.apple.security.device.camera": true, + }; + + it("accepts the intended production entitlements", () => { + expect(collectEntitlementErrors(validEntitlements)).toEqual([]); + }); + + it("rejects missing capabilities and debug attachment", () => { + expect( + collectEntitlementErrors({ + ...validEntitlements, + "com.apple.security.cs.allow-jit": false, + "com.apple.security.get-task-allow": true, + }), + ).toEqual([ + "required entitlement is missing or disabled: com.apple.security.cs.allow-jit", + "distribution build must not enable com.apple.security.get-task-allow", + ]); + }); + + it("rejects unreviewed root runtime exceptions", () => { + expect( + collectEntitlementErrors({ + ...validEntitlements, + "com.apple.security.cs.allow-dyld-environment-variables": true, + }), + ).toEqual([ + "unexpected root application entitlement: com.apple.security.cs.allow-dyld-environment-variables", + ]); + }); + + it("rejects disabled but unreviewed entitlement keys", () => { + expect( + collectEntitlementErrors({ + ...validEntitlements, + "com.apple.security.get-task-allow": false, + }), + ).toEqual(["unexpected root application entitlement: com.apple.security.get-task-allow"]); + }); +}); + +describe("macOS distribution architecture policy", () => { + it("recognizes thin and universal Mach-O magic bytes without parsing file output", () => { + for (const header of [ + [0xfe, 0xed, 0xfa, 0xce], + [0xcf, 0xfa, 0xed, 0xfe], + [0xca, 0xfe, 0xba, 0xbe], + [0xbf, 0xba, 0xfe, 0xca], + ]) { + expect(hasMachOMagic(Uint8Array.from(header))).toBe(true); + } + + expect(hasMachOMagic(Uint8Array.from([0x7f, 0x45, 0x4c, 0x46]))).toBe(false); + expect(hasMachOMagic(Uint8Array.from([0xfe, 0xed, 0xfa]))).toBe(false); + }); + + it("parses thin and fat lipo output", () => { + expect(parseLipoArchitectures("Non-fat file: App is architecture: arm64")).toEqual([ + "arm64", + ]); + expect( + parseLipoArchitectures("Architectures in the fat file: App are: x86_64 arm64"), + ).toEqual(["x86_64", "arm64"]); + }); + + it("uses path-specific helper architecture before the build architecture", () => { + expect( + expectedMachOArchitecture("app/electron/native/bin/darwin-arm64/helper", "x64"), + ).toBe("arm64"); + expect( + expectedMachOArchitecture("app/electron/native/bin/darwin-x64/helper", "arm64"), + ).toBe("x86_64"); + expect(expectedMachOArchitecture("Recordly.app/Contents/MacOS/Recordly", "x64")).toBe( + "x86_64", + ); + }); + + it("reports a binary that lacks the required architecture", () => { + expect( + collectArchitectureErrors("Recordly.app/Contents/MacOS/Recordly", "arm64", "x64"), + ).toEqual(["Recordly.app/Contents/MacOS/Recordly does not contain x86_64 (found: arm64)"]); + }); +}); diff --git a/electron/main.ts b/electron/main.ts index 04d15b69e..076aa42d4 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,43 +1,131 @@ -import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron' -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import fs from 'node:fs/promises' -import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow } from './windows' -import { registerIpcHandlers } from './ipc/handlers' -import { cleanupMouseTracking } from './ipc/mouseTracking' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -export const RECORDINGS_DIR = path.join(app.getPath('userData'), 'recordings') - -// Cleanup old recordings (older than 1 day) -async function cleanupOldRecordings() { - try { - const files = await fs.readdir(RECORDINGS_DIR) - const now = Date.now() - const maxAge = 1 * 24 * 60 * 60 * 1000 - - for (const file of files) { - const filePath = path.join(RECORDINGS_DIR, file) - const stats = await fs.stat(filePath) - - if (now - stats.mtimeMs > maxAge) { - await fs.unlink(filePath) - console.log(`Deleted old recording: ${file}`) - } - } - } catch (error) { - console.error('Failed to cleanup old recordings:', error) - } +import { resolveDisplayMediaSource } from "./ipc/register/sourceMapping"; +import { createSaveBeforeCloseController } from "./saveBeforeClose"; +import { clearRecordingTrashUndo } from "./ipc/recording/library"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { + app, + BrowserWindow, + desktopCapturer, + dialog, + webContents as electronWebContents, + ipcMain, + Menu, + nativeImage, + session, + shell, + systemPreferences, + Tray, +} from "electron"; +import { RECORDINGS_DIR } from "./appPaths"; +import { createAuthCallbackController } from "./authCallback"; +import { showCursor } from "./cursorHider"; +import { getGpuSwitches } from "./gpuSwitches"; +import { + cleanupAllExportStreams, + cleanupNativeVideoExportSessions, + getSelectedSourceId, + killWindowsCaptureProcess, + registerIpcHandlers, +} from "./ipc/handlers"; +import { ensureMediaServer } from "./mediaServer"; +import { hardenWebContentsNavigation, shouldHardenWebContentsType } from "./navigationPolicy"; +import { shouldGrantDisplayCapture, shouldGrantMediaPermission } from "./permissionPolicy"; +import { ensurePackagedRendererServer, getPackagedRendererBaseUrl } from "./rendererServer"; +import { + checkForAppUpdates, + deferUpdateReminder, + dismissUpdateToast, + downloadAvailableUpdate, + getCurrentUpdateToastPayload, + getExperimentalUpdatesEnabled, + getUpdaterLogPath, + getUpdateStatusSummary, + installDownloadedUpdateNow, + previewNativeUpdateDialog, + previewUpdateToast, + setExperimentalUpdatesEnabled, + setupAutoUpdates, + skipAvailableUpdateVersion, +} from "./updater"; +import { + createEditorWindow, + createHudOverlayWindow, + createSourceSelectorWindow, + getHudOverlayWindow, + getUpdateToastWindow, + hideUpdateToastWindow, + isHudOverlayMousePassthroughSupported, + beginHudCaptureProtection, + reassertHudOverlayMousePassthrough as reassertHudOverlayMouseState, + setHudOverlayRecordingActive, + showUpdateToastWindow, +} from "./windows"; + +const electronMainDir = path.dirname(fileURLToPath(import.meta.url)); +const IS_SMOKE_EXPORT = process.env.RECORDLY_SMOKE_EXPORT === "1"; + +function ignoreBrokenConsolePipe(stream: NodeJS.WritableStream | undefined) { + stream?.on("error", (error: NodeJS.ErrnoException) => { + if (error.code === "EPIPE" || error.code === "EIO") { + return; + } + throw error; + }); +} + +ignoreBrokenConsolePipe(process.stdout); +ignoreBrokenConsolePipe(process.stderr); + +app.commandLine.appendSwitch("ignore-gpu-blocklist"); +app.commandLine.appendSwitch("enable-unsafe-webgpu"); +app.commandLine.appendSwitch("enable-gpu-rasterization"); + +app.on("web-contents-created", (_event, contents) => { + if (!shouldHardenWebContentsType(contents.getType())) { + return; + } + + hardenWebContentsNavigation(contents, (url) => shell.openExternal(url)); +}); + +function configureGpuAccelerationSwitches() { + const { useAngle, useGl, disableFeatures } = getGpuSwitches(process.platform, process.env); + if (useAngle) { + app.commandLine.appendSwitch("use-angle", useAngle); + } + if (useGl) { + app.commandLine.appendSwitch("use-gl", useGl); + } + if (disableFeatures && disableFeatures.length > 0) { + app.commandLine.appendSwitch("disable-features", disableFeatures.join(",")); + } +} + +async function logSmokeExportGpuDiagnostics() { + if (!IS_SMOKE_EXPORT) { + return; + } + + try { + console.log("[smoke-export] GPU feature status", JSON.stringify(app.getGPUFeatureStatus())); + console.log("[smoke-export] GPU info", JSON.stringify(await app.getGPUInfo("basic"))); + } catch (error) { + console.warn("[smoke-export] Failed to read GPU diagnostics:", error); + } } +configureGpuAccelerationSwitches(); + async function ensureRecordingsDir() { - try { - await fs.mkdir(RECORDINGS_DIR, { recursive: true }) - console.log('Recordings directory ready:', RECORDINGS_DIR) - } catch (error) { - console.error('Failed to create recordings directory:', error) - } + try { + await fs.mkdir(RECORDINGS_DIR, { recursive: true }); + console.log("RECORDINGS_DIR:", RECORDINGS_DIR); + console.log("User Data Path:", app.getPath("userData")); + } catch (error) { + console.error("Failed to create recordings directory:", error); + } } // The built directory structure @@ -49,117 +137,1004 @@ async function ensureRecordingsDir() { // │ │ ├── main.js // │ │ └── preload.mjs // │ -process.env.APP_ROOT = path.join(__dirname, '..') +process.env.APP_ROOT = path.join(electronMainDir, ".."); // Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x -export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] -export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') -export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') +export const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; +export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); +export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); +const IS_DEV = Boolean(VITE_DEV_SERVER_URL); + +process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL + ? path.join(process.env.APP_ROOT, "public") + : RENDERER_DIST; + +function getTrustedCaptureDocumentBaseUrls(): string[] { + const trustedUrls = [pathToFileURL(path.join(RENDERER_DIST, "index.html")).href]; + + if (VITE_DEV_SERVER_URL) { + trustedUrls.push(VITE_DEV_SERVER_URL); + } + + const packagedRendererBaseUrl = getPackagedRendererBaseUrl(); + if (packagedRendererBaseUrl) { + trustedUrls.push(new URL("/", packagedRendererBaseUrl).href); + } + + return trustedUrls; +} -process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST +function isHudWebContents(webContents: Electron.WebContents | null): boolean { + if (!webContents || webContents.isDestroyed()) { + return false; + } + + const hudWindow = getHudOverlayWindow(); + return Boolean(hudWindow && hudWindow.webContents === webContents); +} // Window references -let mainWindow: BrowserWindow | null = null -let sourceSelectorWindow: BrowserWindow | null = null -let tray: Tray | null = null -let selectedSourceName = '' +let mainWindow: BrowserWindow | null = null; +let sourceSelectorWindow: BrowserWindow | null = null; +let tray: Tray | null = null; +let trayContextMenu: Menu | null = null; +let selectedSourceName = ""; +let editorHasUnsavedChanges = false; +let isForceClosing = false; +let isAppQuitting = false; +let isCreatingMainWindow = false; +let isCreatingEditorWindow = false; +const shouldEnforceSingleInstanceLock = !IS_DEV; +const hasSingleInstanceLock = shouldEnforceSingleInstanceLock + ? app.requestSingleInstanceLock() + : true; + +if (!hasSingleInstanceLock) { + app.quit(); +} + +const authCallbacks = createAuthCallbackController({ + isDev: IS_DEV, + focusApp: () => focusOrCreateMainWindow(), +}); + +function closeEditorWindowBypassingUnsavedPrompt(window: BrowserWindow | null) { + if (!window || window.isDestroyed()) { + return; + } + + if (isEditorWindow(window)) { + isForceClosing = true; + editorHasUnsavedChanges = false; + } + window.close(); +} + +function closeEditorWindowToHud(window: BrowserWindow | null) { + if (!window || window.isDestroyed()) { + return; + } + + // The HUD renderer normally remains hidden while the editor is open so + // recording finalization can continue. Restore that HUD before destroying + // the editor, keeping Recordly in its ready-to-record state on the taskbar. + window.hide(); + if (mainWindow === window) { + mainWindow = null; + } + createWindow(); + closeEditorWindowBypassingUnsavedPrompt(window); +} + +function restoreWindowSafely(window: BrowserWindow | null) { + if (!window || window.isDestroyed()) { + return; + } + + if (!isEditorWindow(window) && process.platform === "win32") { + showHudOverlayFromTray(); + return; + } + + if (window.isMinimized()) { + window.restore(); + } + + if (!window.isVisible()) { + window.show(); + } + + window.moveTop(); + window.focus(); +} + +function getExistingEditorWindow(): BrowserWindow | null { + return ( + BrowserWindow.getAllWindows().find( + (window) => !window.isDestroyed() && isEditorWindow(window), + ) ?? null + ); +} + +// Tray Icons (lazily created after app is ready to avoid accessing Electron APIs too early) +let defaultTrayIcon: ReturnType | null = null; +let recordingTrayIcon: ReturnType | null = null; + +function getPlatformAppIconFilename(size: 32 | 128 | 512) { + const baseName = process.platform === "darwin" ? "recordlymac" : "recordly"; + return `app-icons/${baseName}-${size}.png`; +} + +function getDefaultTrayIcon() { + if (!defaultTrayIcon) { + defaultTrayIcon = getTrayIcon(getPlatformAppIconFilename(32)); + } + return defaultTrayIcon; +} + +function getRecordingTrayIcon() { + if (!recordingTrayIcon) { + recordingTrayIcon = getTrayIcon("rec-button.png"); + } + return recordingTrayIcon; +} + +function showHudOverlayFromTray() { + const updateToast = getUpdateToastWindow(); + if (updateToast?.isVisible()) { + updateToast.show(); + updateToast.moveTop(); + updateToast.focus(); + return true; + } + + const hud = getHudOverlayWindow(); + if (!hud) { + return false; + } + + if (hud.isMinimized()) { + hud.restore(); + } + + if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { + hud.showInactive(); + hud.moveTop(); + reassertHudOverlayMouseState(); + return true; + } + + hud.show(); + hud.moveTop(); + hud.focus(); + return true; +} + +ipcMain.on("set-has-unsaved-changes", (_event, hasChanges: boolean) => { + editorHasUnsavedChanges = hasChanges; +}); function createWindow() { - mainWindow = createHudOverlayWindow() + if (!app.isReady()) { + void app.whenReady().then(() => { + if (!mainWindow || mainWindow.isDestroyed()) { + createWindow(); + } + }); + return; + } + + if (isCreatingMainWindow) { + return; + } + + if (mainWindow && !mainWindow.isDestroyed()) { + restoreWindowSafely(mainWindow); + return; + } + + const existingHudWindow = getHudOverlayWindow(); + if (existingHudWindow) { + mainWindow = existingHudWindow; + restoreWindowSafely(existingHudWindow); + return; + } + + isCreatingMainWindow = true; + const createdHudWindow = createHudOverlayWindow(); + mainWindow = createdHudWindow; + createdHudWindow.once("closed", () => { + if (mainWindow === createdHudWindow) { + mainWindow = null; + } + }); + isCreatingMainWindow = false; +} + +function focusOrCreateMainWindow() { + if (!app.isReady()) { + void app.whenReady().then(() => { + focusOrCreateMainWindow(); + }); + return; + } + + const updateToast = getUpdateToastWindow(); + if (updateToast?.isVisible()) { + updateToast.show(); + updateToast.moveTop(); + updateToast.focus(); + return; + } + + if (!mainWindow || mainWindow.isDestroyed()) { + const existingHud = getHudOverlayWindow(); + if (existingHud && !existingHud.isDestroyed()) { + mainWindow = existingHud; + } else { + createWindow(); + return; + } + } + + if (mainWindow && !mainWindow.isDestroyed()) { + // On Linux/Wayland, focus() often doesn't take effect (compositor ignores it). Apps like Telegram + // work because they receive an XDG activation token via StatusNotifierItem.ProvideXdgActivationToken; + // Electron's tray doesn't handle that yet. Workaround: destroy and recreate the HUD so the new + // window gets focus (creation path works). Only for HUD, not editor. + if ( + process.platform === "linux" && + !mainWindow.isFocused() && + !isEditorWindow(mainWindow) + ) { + const win = mainWindow; + mainWindow = null; + win.once("closed", () => createWindow()); + win.destroy(); + return; + } + + // On Win32 with mouse passthrough enabled (Win11+), calling + // show/moveTop/focus on the transparent HUD overlay permanently corrupts + // setIgnoreMouseEvents forwarding, making it click-through. Only focus + // the editor window; the HUD is alwaysOnTop so it doesn't need explicit + // focus. On Win10 (passthrough disabled), the HUD is always interactive + // and can be safely shown/restored. + if ( + process.platform === "win32" && + !isEditorWindow(mainWindow) && + isHudOverlayMousePassthroughSupported() + ) { + showHudOverlayFromTray(); + return; + } + + mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.moveTop(); + mainWindow.focus(); + } +} + +function isEditorWindow(window: BrowserWindow) { + return window.webContents.getURL().includes("windowType=editor"); +} + +function sendEditorMenuAction( + channel: "menu-load-project" | "menu-save-project" | "menu-save-project-as", +) { + let targetWindow = BrowserWindow.getFocusedWindow() ?? mainWindow; + + if (!targetWindow || targetWindow.isDestroyed() || !isEditorWindow(targetWindow)) { + createEditorWindowWrapper(); + targetWindow = mainWindow; + if (!targetWindow || targetWindow.isDestroyed()) return; + + const send = () => { + if (!targetWindow || targetWindow.isDestroyed()) return; + targetWindow.webContents.send(channel); + }; + if (targetWindow.webContents.isLoadingMainFrame()) + targetWindow.webContents.once("did-finish-load", send); + else send(); + return; + } + + targetWindow.webContents.send(channel); +} + +function setupApplicationMenu() { + const isMac = process.platform === "darwin"; + const template: Electron.MenuItemConstructorOptions[] = []; + if (isMac) { + template.push({ + label: app.name, + submenu: [ + { role: "about" }, + { type: "separator" }, + { role: "services" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideOthers" }, + { role: "unhide" }, + { type: "separator" }, + { role: "quit" }, + ], + }); + } + + template.push( + { + label: "File", + submenu: [ + { + label: "Open Projects…", + accelerator: "CmdOrCtrl+O", + click: () => sendEditorMenuAction("menu-load-project"), + }, + { + label: "Save Project…", + accelerator: "CmdOrCtrl+S", + click: () => sendEditorMenuAction("menu-save-project"), + }, + { + label: "Save Project As…", + accelerator: "CmdOrCtrl+Shift+S", + click: () => sendEditorMenuAction("menu-save-project-as"), + }, + ...(isMac + ? [] + : [ + { type: "separator" as const }, + { role: "quit" as const, accelerator: "CmdOrCtrl+Q" }, + ]), + ], + }, + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: isMac + ? [{ role: "minimize" }, { role: "zoom" }, { type: "separator" }, { role: "front" }] + : [{ role: "minimize" }, { role: "close" }], + }, + { + label: "Help", + submenu: [ + { + label: "Check for Updates…", + click: () => { + void checkForAppUpdates(getUpdateDialogWindow, { manual: true }); + }, + }, + ], + }, + ); + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} + +function isPrimaryTrayClick(event: unknown) { + const button = + event && typeof event === "object" && "button" in event + ? (event as { button?: number | string }).button + : undefined; + return button === undefined || button === 0 || button === "left"; } function createTray() { - const iconPath = path.join(process.env.VITE_PUBLIC || RENDERER_DIST, 'rec-button.png'); - let icon = nativeImage.createFromPath(iconPath); - icon = icon.resize({ width: 24, height: 24, quality: 'best' }); - tray = new Tray(icon); - updateTrayMenu(); -} - -function updateTrayMenu() { - if (!tray) return; - const menuTemplate = [ - { - label: 'Stop Recording', - click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('stop-recording-from-tray'); - } - } - } - ]; - const contextMenu = Menu.buildFromTemplate(menuTemplate); - tray.setContextMenu(contextMenu); - tray.setToolTip(`Recording: ${selectedSourceName}`); + tray = new Tray(getDefaultTrayIcon()); + tray.on("click", (event) => { + if (process.platform === "win32" && !isPrimaryTrayClick(event)) { + return; + } + + focusOrCreateMainWindow(); + }); + + if (process.platform === "win32") { + tray.on("right-click", () => { + if (!tray || !trayContextMenu) { + return; + } + + tray.popUpContextMenu(trayContextMenu); + }); + return; + } + + tray.on("double-click", () => focusOrCreateMainWindow()); +} + +function shouldUseTray() { + // macOS and Windows expose Recordly through their Dock/taskbar. Keep the + // tray entry only on Linux, where it remains the primary app entry point. + return process.platform === "linux"; +} + +function getPublicAssetPath(filename: string) { + return path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename); +} + +function getAppImage(filename: string) { + return nativeImage.createFromPath(getPublicAssetPath(filename)); +} + +function getTrayIcon(filename: string) { + return getAppImage(filename).resize({ + width: 24, + height: 24, + quality: "best", + }); +} + +function syncDockIcon() { + if (process.platform !== "darwin" || !app.dock) { + return; + } + + const dockIcon = getAppImage(getPlatformAppIconFilename(512)); + if (!dockIcon.isEmpty()) { + app.dock.setIcon(dockIcon); + } +} + +function sendUpdateToastToWindows(channel: "update-toast-state", payload: unknown) { + if (process.platform !== "darwin") { + return false; + } + + if (!payload) { + const existingWindow = getUpdateToastWindow(); + if (existingWindow) { + existingWindow.webContents.send(channel, null); + } + hideUpdateToastWindow(); + return true; + } + + const toastWindow = showUpdateToastWindow(); + const sendPayload = () => { + toastWindow.webContents.send(channel, payload); + showUpdateToastWindow(); + }; + + if (toastWindow.webContents.isLoadingMainFrame()) { + toastWindow.webContents.once("did-finish-load", sendPayload); + } else { + sendPayload(); + } + + return true; +} + +function getUpdateDialogWindow() { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow && !focusedWindow.isDestroyed()) { + return focusedWindow; + } + + if (mainWindow && !mainWindow.isDestroyed()) { + return mainWindow; + } + + return getHudOverlayWindow(); +} + +ipcMain.handle("install-downloaded-update", () => { + installDownloadedUpdateNow(sendUpdateToastToWindows); + return { success: true }; +}); + +ipcMain.handle("download-available-update", (_event, installAfterDownload?: boolean) => { + return downloadAvailableUpdate(sendUpdateToastToWindows, { + installAfterDownload: Boolean(installAfterDownload), + }); +}); + +ipcMain.handle("defer-downloaded-update", (_event, delayMs?: number) => { + return deferUpdateReminder(getUpdateDialogWindow, sendUpdateToastToWindows, delayMs); +}); + +ipcMain.handle("dismiss-update-toast", () => { + return dismissUpdateToast(getUpdateDialogWindow, sendUpdateToastToWindows); +}); + +ipcMain.handle("skip-update-version", () => { + return skipAvailableUpdateVersion(sendUpdateToastToWindows); +}); + +ipcMain.handle("get-current-update-toast-payload", () => { + return getCurrentUpdateToastPayload(); +}); + +ipcMain.handle("get-update-status-summary", () => { + return getUpdateStatusSummary(); +}); + +ipcMain.handle("get-experimental-updates-enabled", () => { + return getExperimentalUpdatesEnabled(); +}); + +ipcMain.handle("set-experimental-updates-enabled", async (_event, enabled: unknown) => { + if (typeof enabled !== "boolean") { + return { success: false, enabled: getExperimentalUpdatesEnabled() }; + } + + try { + const savedValue = setExperimentalUpdatesEnabled(enabled); + await checkForAppUpdates(getUpdateDialogWindow); + return { success: true, enabled: savedValue }; + } catch (error) { + console.error("Failed to update experimental updates preference:", error); + return { + success: false, + enabled: getExperimentalUpdatesEnabled(), + error: String(error), + }; + } +}); + +ipcMain.handle("preview-update-toast", async () => { + if (process.platform !== "darwin") { + await previewNativeUpdateDialog(getUpdateDialogWindow); + return { success: true }; + } + + return { success: previewUpdateToast(sendUpdateToastToWindows) }; +}); + +ipcMain.handle("check-for-app-updates", async () => { + await checkForAppUpdates(getUpdateDialogWindow, { manual: true }); + return { success: true, logPath: getUpdaterLogPath() }; +}); + +function updateTrayMenu(recording: boolean = false) { + if (!tray) return; + const trayIcon = recording ? getRecordingTrayIcon() : getDefaultTrayIcon(); + const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "Recordly"; + const menuTemplate = recording + ? [ + { + label: "Show Controls", + click: () => { + if (!showHudOverlayFromTray()) { + focusOrCreateMainWindow(); + } + }, + }, + { + label: "Stop Recording", + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("stop-recording-from-tray"); + } + }, + }, + ] + : [ + { + label: "Open", + click: () => { + if (!showHudOverlayFromTray()) { + focusOrCreateMainWindow(); + } + }, + }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]; + const menu = Menu.buildFromTemplate(menuTemplate); + trayContextMenu = menu; + tray.setImage(trayIcon); + tray.setToolTip(trayToolTip); + if (process.platform !== "win32") { + tray.setContextMenu(menu); + } } function createEditorWindowWrapper() { - if (mainWindow) { - mainWindow.close() - mainWindow = null - } - mainWindow = createEditorWindow() + const existingEditorWindow = getExistingEditorWindow(); + if (existingEditorWindow) { + mainWindow = existingEditorWindow; + restoreWindowSafely(existingEditorWindow); + return existingEditorWindow; + } + + if (isCreatingEditorWindow) { + const currentWindow = mainWindow; + if (currentWindow && !currentWindow.isDestroyed()) { + return currentWindow; + } + + const currentEditorWindow = getExistingEditorWindow(); + if (currentEditorWindow) { + mainWindow = currentEditorWindow; + return currentEditorWindow; + } + } + + isCreatingEditorWindow = true; + const previousWindow = mainWindow; + if (previousWindow && !previousWindow.isDestroyed()) { + const closingEditorWindow = isEditorWindow(previousWindow); + + if (closingEditorWindow) { + closeEditorWindowBypassingUnsavedPrompt(previousWindow); + } else { + // It's the HUD or another window. Hide it instead of closing so background + // tasks (like webcam finalizing) can finish in its renderer process. + previousWindow.hide(); + } + + if (!closingEditorWindow) { + isForceClosing = false; + } + if (mainWindow === previousWindow) { + mainWindow = null; + } + } + const editorWindow = createEditorWindow(); + mainWindow = editorWindow; + editorHasUnsavedChanges = false; + + editorWindow.on("closed", () => { + if (mainWindow === editorWindow) { + mainWindow = null; + } + isCreatingEditorWindow = false; + isForceClosing = false; + editorHasUnsavedChanges = false; + }); + + const closeAfterSave = () => { + if (process.platform === "win32" && !isAppQuitting) closeEditorWindowToHud(editorWindow); + else closeEditorWindowBypassingUnsavedPrompt(editorWindow); + }; + let closeSaveRequestId = 0; + const closeSave = createSaveBeforeCloseController({ + requestSave: () => + editorWindow.webContents.send("request-save-before-close", ++closeSaveRequestId), + subscribe: (done) => { + const saved = (event: Electron.IpcMainEvent, success: unknown, requestId: unknown) => { + if (event.sender === editorWindow.webContents && requestId === closeSaveRequestId) + done(success === true); + }; + ipcMain.on("save-before-close-done", saved); + return () => ipcMain.removeListener("save-before-close-done", saved); + }, + offerDiscard: async () => { + const result = await dialog.showMessageBox(editorWindow, { + type: "warning", + buttons: ["Discard & Close", "Cancel"], + defaultId: 1, + cancelId: 1, + title: "Project could not be saved", + message: "Saving failed or did not respond. Discard unsaved changes and close?", + }); + return result.response === 0; + }, + close: closeAfterSave, + cancel: () => { + isAppQuitting = false; + }, + }); + editorWindow.once("closed", closeSave.dispose); + editorWindow.on("close", (event) => { + if (isForceClosing || !editorHasUnsavedChanges) { + if (process.platform === "win32" && !isForceClosing && !isAppQuitting) { + event.preventDefault(); + closeEditorWindowToHud(editorWindow); + } + return; + } + event.preventDefault(); + closeSave.start(); + }); + + return editorWindow; } function createSourceSelectorWindowWrapper() { - sourceSelectorWindow = createSourceSelectorWindow() - sourceSelectorWindow.on('closed', () => { - sourceSelectorWindow = null - }) - return sourceSelectorWindow + sourceSelectorWindow = createSourceSelectorWindow(); + sourceSelectorWindow.on("closed", () => { + sourceSelectorWindow = null; + }); + return sourceSelectorWindow; } -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits +// On macOS, applications and their menu bar stay active until the user quits // explicitly with Cmd + Q. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - cleanupMouseTracking() - app.quit() - mainWindow = null - } -}) - -app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { - createWindow() - } -}) - -// Cleanup old recordings on quit (both macOS and other platforms) -app.on('before-quit', async (event) => { - event.preventDefault() - cleanupMouseTracking() - await cleanupOldRecordings() - app.exit(0) -}) +app.on("before-quit", () => { + isAppQuitting = true; + authCallbacks.close(); + void clearRecordingTrashUndo().catch((error) => + console.warn("Could not clear recording undo cache", error), + ); + killWindowsCaptureProcess(); + showCursor(); + cleanupNativeVideoExportSessions(); + void cleanupAllExportStreams(); +}); + +app.on("window-all-closed", () => { + if (IS_SMOKE_EXPORT || process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("activate", () => { + if (getHudOverlayWindow()?.isVisible()) return; + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + focusOrCreateMainWindow(); +}); + +app.on("second-instance", (_event, commandLine) => { + const authCallback = authCallbacks.find(commandLine); + if (authCallback) authCallbacks.dispatch(authCallback); + focusOrCreateMainWindow(); +}); // Register all IPC handlers when app is ready app.whenReady().then(async () => { - // Ensure recordings directory exists - await ensureRecordingsDir() - - registerIpcHandlers( - createEditorWindowWrapper, - createSourceSelectorWindowWrapper, - () => mainWindow, - () => sourceSelectorWindow, - (recording: boolean, sourceName: string) => { - selectedSourceName = sourceName - if (recording) { - if (!tray) createTray(); - updateTrayMenu(); - if (mainWindow) mainWindow.minimize(); - } else { - if (tray) { - tray.destroy(); - tray = null; - } - if (mainWindow) mainWindow.restore(); - } - } - ) - createWindow() -}) + authCallbacks.startDevServer(); + if (process.defaultApp && process.argv[1]) { + app.setAsDefaultProtocolClient(authCallbacks.protocol, process.execPath, [ + path.resolve(process.argv[1]), + ]); + } else { + app.setAsDefaultProtocolClient(authCallbacks.protocol); + } + const startupAuthCallback = authCallbacks.find(process.argv); + if (startupAuthCallback) authCallbacks.dispatch(startupAuthCallback); + + if (process.platform === "win32") { + app.setAppUserModelId("dev.recordly.app"); + } + + session.defaultSession.setPermissionCheckHandler( + (webContents, permission, requestingOrigin, details) => { + return shouldGrantMediaPermission( + { + permission, + isTrustedCaptureWindow: isHudWebContents(webContents), + isMainFrame: details.isMainFrame, + currentDocumentUrl: webContents?.getURL() ?? "", + // Electron 39 may supply the last committed document URL, including its + // query, in the requestingOrigin argument for media checks. + requestingUrl: details.requestingUrl ?? requestingOrigin, + securityOrigins: + details.securityOrigin === undefined ? [] : [details.securityOrigin], + }, + getTrustedCaptureDocumentBaseUrls(), + ); + }, + ); + + session.defaultSession.setPermissionRequestHandler( + (webContents, permission, callback, details) => { + const securityOrigin = "securityOrigin" in details ? details.securityOrigin : undefined; + + callback( + shouldGrantMediaPermission( + { + permission, + isTrustedCaptureWindow: isHudWebContents(webContents), + isMainFrame: details.isMainFrame, + currentDocumentUrl: webContents.getURL(), + requestingUrl: details.requestingUrl, + securityOrigins: securityOrigin === undefined ? [] : [securityOrigin], + }, + getTrustedCaptureDocumentBaseUrls(), + ), + ); + }, + ); + + // Recordly does not use WebHID, Web Serial, or WebUSB. Do not grant devices by default. + session.defaultSession.setDevicePermissionHandler(() => false); + + // macOS prompts for camera and microphone access at the point of use. Asking + // here blocks the first window behind two modal OS permission flows and makes + // a fresh install look hung. Windows has no equivalent request API, so retain + // its diagnostic warnings. + if (process.platform === "win32") { + const cameraStatus = systemPreferences.getMediaAccessStatus("camera"); + const micStatus = systemPreferences.getMediaAccessStatus("microphone"); + if (cameraStatus !== "granted") { + console.warn( + `[permissions] Camera access is "${cameraStatus}" — webcam may not work. Check Windows Settings > Privacy > Camera.`, + ); + } + if (micStatus !== "granted") { + console.warn( + `[permissions] Microphone access is "${micStatus}" — mic recording may not work. Check Windows Settings > Privacy > Microphone.`, + ); + } + } + + ipcMain.on("hud-overlay-close", () => { + const hud = getHudOverlayWindow(); + if (hud) { + console.log("[main] Closing HUD window via hud-overlay-close"); + hud.close(); + } + + // If this was the last window (or we are in a state where we should quit), do it. + // We use a small delay to allow window.close() to propagate. + setTimeout(() => { + const windows = BrowserWindow.getAllWindows().filter((w) => !w.isDestroyed()); + if (windows.length === 0) { + console.log("[main] No windows left, quitting app"); + app.quit(); + } + }, 100); + }); + if (process.platform === "darwin" && app.dock) { + await app.dock.show(); + } + syncDockIcon(); + if (shouldUseTray()) { + createTray(); + updateTrayMenu(); + } + setupApplicationMenu(); + await Promise.all([ + ensureRecordingsDir(), + !VITE_DEV_SERVER_URL + ? ensurePackagedRendererServer(RENDERER_DIST).catch((error) => { + console.warn( + "[renderer-server] Failed to start packaged renderer server:", + error, + ); + }) + : Promise.resolve(), + ensureMediaServer().catch((error) => { + console.warn("[media-server] Failed to start media server:", error); + }), + ]); + + registerIpcHandlers( + createEditorWindowWrapper, + createSourceSelectorWindowWrapper, + () => mainWindow, + () => sourceSelectorWindow, + (recording: boolean, sourceName: string) => { + selectedSourceName = sourceName; + setHudOverlayRecordingActive(recording); + if (shouldUseTray()) { + if (!tray) createTray(); + updateTrayMenu(recording); + } + if (recording) { + reassertHudOverlayMouseState(); + } + if (!recording) { + restoreWindowSafely(mainWindow); + } + }, + ); + + if (IS_SMOKE_EXPORT || process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT) { + await logSmokeExportGpuDiagnostics(); + if (IS_SMOKE_EXPORT) { + const smokeSource = + process.env.RECORDLY_SMOKE_EXPORT_PROJECT ?? + process.env.RECORDLY_SMOKE_EXPORT_INPUT ?? + ""; + console.log(`[smoke-export] Starting editor smoke export for ${smokeSource}`); + } else { + console.log( + `[dev-open-recording] Starting editor for ${process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT}`, + ); + } + createEditorWindowWrapper(); + return; + } + + createWindow(); + setupAutoUpdates(getUpdateDialogWindow, sendUpdateToastToWindows); + if (IS_DEV && process.env.RECORDLY_DEV_PREVIEW_UPDATE === "1") { + setTimeout(() => { + if (process.platform === "darwin") { + previewUpdateToast(sendUpdateToastToWindows); + return; + } + + void previewNativeUpdateDialog(getUpdateDialogWindow); + }, 750); + } + + // Register the display media handler so that renderer's getDisplayMedia() + // calls land on the pre-selected source without showing a system picker. + // + // IMPORTANT: The callback must receive a plain { id, name } Video object. + // Passing the full DesktopCapturerSource (with thumbnail, appIcon, etc.) + // via an unsafe cast breaks Electron's internal cursor-constraint + // propagation and causes cursor: 'never' from the renderer to be silently + // ignored by the native capture pipeline. + session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { + try { + const frame = request.frame; + const isLiveFrame = Boolean(frame && !frame.isDestroyed()); + const requestingWebContents = + isLiveFrame && frame ? electronWebContents.fromFrame(frame) : undefined; + const isHudMainFrame = Boolean( + isLiveFrame && + requestingWebContents && + isHudWebContents(requestingWebContents) && + frame === requestingWebContents.mainFrame, + ); + + if ( + !shouldGrantDisplayCapture( + { + isTrustedCaptureWindow: isHudMainFrame, + isMainFrame: Boolean(isLiveFrame && frame?.parent === null), + currentDocumentUrl: isLiveFrame ? (frame?.url ?? "") : "", + securityOrigin: request.securityOrigin, + videoRequested: request.videoRequested, + }, + getTrustedCaptureDocumentBaseUrls(), + ) + ) { + callback({}); + return; + } + + // Browser and Linux portal capture starts as soon as this callback + // resolves, before recording-state-changed is emitted. + beginHudCaptureProtection(); + + const video = await resolveDisplayMediaSource({ + platform: process.platform, + env: process.env, + selectedSourceId: getSelectedSourceId(), + getSources: () => + desktopCapturer.getSources({ + types: ["screen", "window"], + thumbnailSize: { width: 0, height: 0 }, + }), + }); + callback(video ? { video } : {}); + } catch (error) { + console.error("setDisplayMediaRequestHandler error:", error); + callback({}); + } + }); + + const currentToastPayload = getCurrentUpdateToastPayload(); + if (currentToastPayload) { + sendUpdateToastToWindows("update-toast-state", currentToastPayload); + } +}); diff --git a/electron/mainCjsNormalize.test.mjs b/electron/mainCjsNormalize.test.mjs new file mode 100644 index 000000000..9ccf91da7 --- /dev/null +++ b/electron/mainCjsNormalize.test.mjs @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { + findElectronMainCjsEsmSyntax, + normalizeElectronMainCjsSource, +} from "../scripts/normalize-electron-main-cjs.mjs"; + +describe("Electron main CJS normalizer", () => { + it("converts Rollup named export blocks to CommonJS assignments", () => { + const source = [ + 'const MAIN_DIST = "dist-electron";', + 'const RENDERER_DIST = "dist";', + "const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;", + "export {", + " MAIN_DIST,", + " RENDERER_DIST,", + " VITE_DEV_SERVER_URL", + "};", + ].join("\n"); + + const result = normalizeElectronMainCjsSource(source); + + expect(result.changed).toBe(true); + expect(result.source).toContain("exports.MAIN_DIST = MAIN_DIST;"); + expect(result.source).toContain("exports.RENDERER_DIST = RENDERER_DIST;"); + expect(result.source).toContain("exports.VITE_DEV_SERVER_URL = VITE_DEV_SERVER_URL;"); + expect(findElectronMainCjsEsmSyntax(result.source)).toEqual([]); + }); + + it("converts single-line aliased named exports to CommonJS assignments", () => { + const source = "export { VITE_DEV_SERVER_URL as devServerUrl };"; + + const result = normalizeElectronMainCjsSource(source); + + expect(result.changed).toBe(true); + expect(result.source).toBe("exports.devServerUrl = VITE_DEV_SERVER_URL;"); + expect(findElectronMainCjsEsmSyntax(result.source)).toEqual([]); + }); + + it("reports unsupported ESM export syntax that cannot be normalized safely", () => { + const source = "export default MAIN_DIST;"; + + expect(findElectronMainCjsEsmSyntax(source)).toEqual([ + { line: 1, text: "export default MAIN_DIST;" }, + ]); + }); + + it("preserves an unsupported export block exactly while normalizing other syntax", () => { + const source = [ + 'import fs from "node:fs";', + "export {", + ' MAIN_DIST as "main-dist",', + "};", + ].join("\n"); + + const result = normalizeElectronMainCjsSource(source); + + expect(result.changed).toBe(true); + expect(result.source).toBe( + [ + 'const fs = require("node:fs");', + "export {", + ' MAIN_DIST as "main-dist",', + "};", + ].join("\n"), + ); + expect(findElectronMainCjsEsmSyntax(result.source)).toEqual([ + { line: 2, text: "export {" }, + ]); + }); +}); diff --git a/electron/mediaServer.test.ts b/electron/mediaServer.test.ts new file mode 100644 index 000000000..cf6cd01ba --- /dev/null +++ b/electron/mediaServer.test.ts @@ -0,0 +1,105 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("media server path policy", () => { + let tempRoot: string; + let appDataPath: string; + let userDataPath: string; + let tempPath: string; + let appPath: string; + + beforeEach(async () => { + tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-media-server-")); + appDataPath = path.join(tempRoot, "AppData"); + userDataPath = path.join(tempRoot, "UserData"); + tempPath = path.join(tempRoot, "Temp"); + appPath = path.join(tempRoot, "App"); + + await Promise.all( + [appDataPath, userDataPath, tempPath, appPath].map((dirPath) => + fs.mkdir(dirPath, { recursive: true }), + ), + ); + + vi.resetModules(); + vi.doMock("electron", () => ({ + app: { + isPackaged: false, + getAppPath: () => appPath, + getPath: (name: string) => { + if (name === "appData") return appDataPath; + if (name === "userData") return userDataPath; + if (name === "temp") return tempPath; + return tempRoot; + }, + setPath: () => undefined, + }, + })); + }); + + afterEach(async () => { + vi.resetModules(); + vi.doUnmock("electron"); + if (tempRoot) { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + + it("rejects existing media files outside the session directories until they are approved", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const videoPath = path.join(downloadsPath, "personal-video.mp4"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(videoPath, "test-video"); + + const { isAllowedMediaPath } = await import("./mediaServer"); + const { rememberApprovedLocalReadPath } = await import("./ipc/project/manager"); + + expect(isAllowedMediaPath(videoPath)).toBe(false); + + await rememberApprovedLocalReadPath(videoPath); + + expect(isAllowedMediaPath(videoPath)).toBe(true); + }); + + it("rejects missing media files outside the allowed directories", async () => { + const missingPath = path.join(tempRoot, "Downloads", "missing.mp4"); + const { isAllowedMediaPath } = await import("./mediaServer"); + + expect(isAllowedMediaPath(missingPath)).toBe(false); + }); +}); + +describe("resolveHttpByteRange", () => { + it("rejects malformed and multi-range headers", async () => { + const { resolveHttpByteRange } = await import("./mediaServer"); + + expect(resolveHttpByteRange("bytes=0-1,2-3", 100)).toBeNull(); + expect(resolveHttpByteRange("bytes=0-1foo", 100)).toBeNull(); + }); + + it("clamps oversized explicit end offsets to EOF", async () => { + const { resolveHttpByteRange } = await import("./mediaServer"); + + expect(resolveHttpByteRange("bytes=0-9999999999", 3_221_225_472)).toEqual({ + start: 0, + end: 3_221_225_471, + }); + }); + + it("rejects ranges that start beyond EOF", async () => { + const { resolveHttpByteRange } = await import("./mediaServer"); + + expect(resolveHttpByteRange("bytes=500-999", 500)).toBeNull(); + }); + + it("preserves suffix range semantics", async () => { + const { resolveHttpByteRange } = await import("./mediaServer"); + + expect(resolveHttpByteRange("bytes=-500", 1_000)).toEqual({ + start: 500, + end: 999, + }); + }); +}); diff --git a/electron/mediaServer.ts b/electron/mediaServer.ts new file mode 100644 index 000000000..c979a84e8 --- /dev/null +++ b/electron/mediaServer.ts @@ -0,0 +1,237 @@ +import { createReadStream } from "node:fs"; +import fs from "node:fs/promises"; +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import path from "node:path"; +import { approvedLocalReadPaths } from "./ipc/state"; +import { getMediaContentType } from "./mediaTypes"; + +let mediaServerBaseUrl: string | null = null; +let mediaServerStartPromise: Promise | null = null; + +export function resolveHttpByteRange( + rangeHeader: string, + fileSize: number, +): { start: number; end: number } | null { + const match = rangeHeader.trim().match(/^bytes=(\d*)-(\d*)$/); + if (!match || (!match[1] && !match[2])) { + return null; + } + + if (fileSize === 0) { + return null; + } + + if (!match[1] && match[2]) { + // Suffix range: bytes=-500 + const suffixLength = Number.parseInt(match[2], 10); + if (Number.isNaN(suffixLength) || suffixLength <= 0) { + return null; + } + + return { + start: Math.max(0, fileSize - suffixLength), + end: fileSize - 1, + }; + } + + const start = Number.parseInt(match[1], 10); + if (Number.isNaN(start) || start < 0 || start >= fileSize) { + return null; + } + + const requestedEnd = match[2] ? Number.parseInt(match[2], 10) : fileSize - 1; + if (Number.isNaN(requestedEnd) || requestedEnd < start) { + return null; + } + + return { + start, + end: Math.min(requestedEnd, fileSize - 1), + }; +} + +async function resolveRealPath(filePath: string): Promise { + try { + return await fs.realpath(path.resolve(filePath)); + } catch { + return null; + } +} + +export function isAllowedMediaPath(realPath: string): boolean { + return approvedLocalReadPaths.has(realPath); +} + +async function handleMediaRequest( + request: IncomingMessage, + response: ServerResponse, +): Promise { + try { + const url = new URL(request.url ?? "/", "http://127.0.0.1"); + + if (url.pathname !== "/video") { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not Found"); + return; + } + + const rawPath = url.searchParams.get("path"); + if (!rawPath) { + response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Missing path parameter"); + return; + } + + const resolvedPath = await resolveRealPath(rawPath); + if (!resolvedPath || !isAllowedMediaPath(resolvedPath)) { + console.warn(`[media-server] Blocked access to unapproved path: ${rawPath}`); + response.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Forbidden"); + return; + } + + const stat = await fs.stat(resolvedPath); + if (!stat.isFile()) { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not Found"); + return; + } + + const contentType = getMediaContentType(resolvedPath); + const fileSize = stat.size; + const rangeHeader = request.headers.range; + + const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": "false", + "Access-Control-Expose-Headers": "Content-Range, Content-Length, Accept-Ranges", + }; + + if (request.method === "OPTIONS") { + response.writeHead(204, { + ...corsHeaders, + "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", + "Access-Control-Allow-Headers": "Range", + }); + response.end(); + return; + } + + if (rangeHeader) { + if (fileSize === 0) { + response.writeHead(416, { ...corsHeaders, "Content-Range": `bytes */0` }); + response.end(); + return; + } + + const byteRange = resolveHttpByteRange(rangeHeader, fileSize); + if (!byteRange) { + response.writeHead(416, { ...corsHeaders, "Content-Range": `bytes */${fileSize}` }); + response.end(); + return; + } + + const { start, end } = byteRange; + + const chunkSize = end - start + 1; + response.writeHead(206, { + ...corsHeaders, + "Content-Range": `bytes ${start}-${end}/${fileSize}`, + "Accept-Ranges": "bytes", + "Content-Length": String(chunkSize), + "Content-Type": contentType, + "Cache-Control": "no-cache", + }); + + if (request.method === "HEAD") { + response.end(); + return; + } + + const stream = createReadStream(resolvedPath, { start, end }); + stream.pipe(response); + stream.on("error", () => { + if (!response.headersSent) { + response.writeHead(500, { "Content-Type": "text/plain" }); + } + response.end(); + }); + } else { + response.writeHead(200, { + ...corsHeaders, + "Accept-Ranges": "bytes", + "Content-Length": String(fileSize), + "Content-Type": contentType, + "Cache-Control": "no-cache", + }); + + if (request.method === "HEAD") { + response.end(); + return; + } + + const stream = createReadStream(resolvedPath); + stream.pipe(response); + stream.on("error", () => { + if (!response.headersSent) { + response.writeHead(500, { "Content-Type": "text/plain" }); + } + response.end(); + }); + } + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not Found"); + return; + } + + console.error("[media-server] Error handling request:", error); + response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Internal Server Error"); + } +} + +export function getMediaServerBaseUrl(): string | null { + return mediaServerBaseUrl; +} + +export async function ensureMediaServer(): Promise { + if (mediaServerBaseUrl) { + return mediaServerBaseUrl; + } + + if (mediaServerStartPromise) { + return mediaServerStartPromise; + } + + mediaServerStartPromise = new Promise((resolve, reject) => { + const server = createServer((request, response) => { + void handleMediaRequest(request, response); + }); + + server.once("error", (error) => { + reject(error); + }); + + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Media server did not expose a TCP address")); + return; + } + + mediaServerBaseUrl = `http://127.0.0.1:${address.port}`; + console.log(`[media-server] Listening at ${mediaServerBaseUrl}`); + resolve(mediaServerBaseUrl); + }); + }); + + return mediaServerStartPromise; +} + +export function buildMediaUrl(baseUrl: string, filePath: string): string { + const resolved = path.resolve(filePath); + return `${baseUrl}/video?path=${encodeURIComponent(resolved)}`; +} diff --git a/electron/mediaTypes.test.ts b/electron/mediaTypes.test.ts new file mode 100644 index 000000000..9bcff6fe2 --- /dev/null +++ b/electron/mediaTypes.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { getMediaContentType, isSupportedLocalMediaPath } from "./mediaTypes"; + +describe("mediaTypes", () => { + it("recognizes .m4a as audio/mp4", () => { + expect(getMediaContentType("recording.m4a")).toBe("audio/mp4"); + }); + + it("supports .m4a as a local media path", () => { + expect(isSupportedLocalMediaPath("recording.m4a")).toBe(true); + }); +}); diff --git a/electron/mediaTypes.ts b/electron/mediaTypes.ts new file mode 100644 index 000000000..a27fb39a1 --- /dev/null +++ b/electron/mediaTypes.ts @@ -0,0 +1,24 @@ +import path from "node:path"; + +export const MEDIA_CONTENT_TYPES: Record = { + ".mp4": "video/mp4", + ".webm": "video/webm", + ".mov": "video/quicktime", + ".mkv": "video/x-matroska", + ".avi": "video/x-msvideo", + ".wav": "audio/wav", + ".mp3": "audio/mpeg", + ".m4a": "audio/mp4", + ".ogg": "audio/ogg", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", +}; + +export function getMediaContentType(filePath: string): string { + return MEDIA_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream"; +} + +export function isSupportedLocalMediaPath(filePath: string): boolean { + return path.extname(filePath).toLowerCase() in MEDIA_CONTENT_TYPES; +} diff --git a/electron/native/NativeCursorMonitor.swift b/electron/native/NativeCursorMonitor.swift new file mode 100644 index 000000000..18c644150 --- /dev/null +++ b/electron/native/NativeCursorMonitor.swift @@ -0,0 +1,505 @@ +import Foundation +import AppKit +import ApplicationServices + +// Force AppKit initialization so cursor images are populated in CLI context +let _ = NSApplication.shared + +func knownCursorCandidates() -> [(String, NSCursor)] { + var candidates: [(String, NSCursor)] = [ + ("arrow", .arrow), + ("text", .iBeam), + ("pointer", .pointingHand), + ("pointer", .dragCopy), + ("pointer", .dragLink), + ("pointer", .contextualMenu), + ("crosshair", .crosshair), + ("open-hand", .openHand), + ("closed-hand", .closedHand), + ("resize-ew", .resizeLeft), + ("resize-ew", .resizeRight), + ("resize-ew", .resizeLeftRight), + ("resize-ns", .resizeUp), + ("resize-ns", .resizeDown), + ("resize-ns", .resizeUpDown), + ("not-allowed", .operationNotAllowed), + ] + + if #available(macOS 10.13, *) { + candidates.append(("text", .iBeamCursorForVerticalLayout)) + } + + return candidates +} + +let signatureAcceptanceThreshold = 12000 +let relaxedSignatureAcceptanceThresholds: [String: Int] = [ + "text": 28000, + "crosshair": 32000, +] +let strictSignatureAcceptanceThresholds: [String: Int] = [ + "open-hand": 3200, + "closed-hand": 3200, +] + +let systemWideElement = AXUIElementCreateSystemWide() +let totalScreenHeight = NSScreen.screens.reduce(CGFloat(0)) { max($0, $1.frame.maxY) } +let axEditableAttribute = "AXEditable" +let axLinkRole = "AXLink" + +struct CursorSignature { + let aspectRatio: Double + let hotspotXRatio: Double + let hotspotYRatio: Double + let shapeSamples: [UInt8] +} + +func signature(for cursor: NSCursor, sampleSize: Int = 32) -> CursorSignature? { + let image = cursor.image + let sourceSize = image.size + guard sourceSize.width > 0, sourceSize.height > 0 else { + return nil + } + + guard let bitmap = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: sampleSize, + pixelsHigh: sampleSize, + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0 + ) else { + return nil + } + + bitmap.size = NSSize(width: sampleSize, height: sampleSize) + + NSGraphicsContext.saveGraphicsState() + guard let context = NSGraphicsContext(bitmapImageRep: bitmap) else { + NSGraphicsContext.restoreGraphicsState() + return nil + } + + NSGraphicsContext.current = context + context.imageInterpolation = .high + + let scale = min(CGFloat(sampleSize) / sourceSize.width, CGFloat(sampleSize) / sourceSize.height) + let drawWidth = sourceSize.width * scale + let drawHeight = sourceSize.height * scale + let drawRect = NSRect( + x: (CGFloat(sampleSize) - drawWidth) / 2, + y: (CGFloat(sampleSize) - drawHeight) / 2, + width: drawWidth, + height: drawHeight + ) + + image.draw(in: drawRect, from: .zero, operation: .copy, fraction: 1) + context.flushGraphics() + NSGraphicsContext.restoreGraphicsState() + + guard let data = bitmap.bitmapData else { + return nil + } + + var alphaSamples: [UInt8] = [] + alphaSamples.reserveCapacity(sampleSize * sampleSize) + let bytesPerRow = bitmap.bytesPerRow + + for y in 0.. 24 ? 255 : 0) + } + } + + let hotspot = cursor.hotSpot + return CursorSignature( + aspectRatio: Double(sourceSize.width / max(1, sourceSize.height)), + hotspotXRatio: Double(hotspot.x / max(1, sourceSize.width)), + hotspotYRatio: Double(hotspot.y / max(1, sourceSize.height)), + shapeSamples: alphaSamples + ) +} + +func signatureScore(_ lhs: CursorSignature, _ rhs: CursorSignature) -> Int { + let count = min(lhs.shapeSamples.count, rhs.shapeSamples.count) + var imageDifference = 0 + for index in 0.. String? { + var value: CFTypeRef? + let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &value) + guard error == .success else { + return nil + } + + return value as? String +} + +func attributeBool(_ element: AXUIElement, _ attribute: String) -> Bool? { + var value: CFTypeRef? + let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &value) + guard error == .success else { + return nil + } + + return value as? Bool +} + +func actionNames(_ element: AXUIElement) -> [String] { + var names: CFArray? + let error = AXUIElementCopyActionNames(element, &names) + guard error == .success, let actions = names as? [String] else { + return [] + } + + return actions +} + +func currentElement() -> AXUIElement? { + guard let location = CGEvent(source: nil)?.location else { + return nil + } + + var element: AXUIElement? + let y = totalScreenHeight > 0 ? totalScreenHeight - location.y : location.y + let error = AXUIElementCopyElementAtPosition(systemWideElement, Float(location.x), Float(y), &element) + guard error == .success else { + return nil + } + + return element +} + +func focusedElement() -> AXUIElement? { + var value: CFTypeRef? + let error = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &value) + guard error == .success, let value else { + return nil + } + + return unsafeBitCast(value, to: AXUIElement.self) +} + +func parentElement(of element: AXUIElement) -> AXUIElement? { + var value: CFTypeRef? + let error = AXUIElementCopyAttributeValue(element, kAXParentAttribute as CFString, &value) + guard error == .success, let value else { + return nil + } + + return unsafeBitCast(value, to: AXUIElement.self) +} + +func hasAttribute(_ element: AXUIElement, _ attribute: String) -> Bool { + var value: CFTypeRef? + return AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success +} + +func ancestorChain(startingAt element: AXUIElement?, maxDepth: Int = 4) -> [AXUIElement] { + guard let element else { + return [] + } + + var elements: [AXUIElement] = [element] + var current = element + var depth = 0 + + while depth < maxDepth, let parent = parentElement(of: current) { + elements.append(parent) + current = parent + depth += 1 + } + + return elements +} + +func metadataString(for element: AXUIElement) -> String { + return [ + attributeString(element, kAXRoleAttribute), + attributeString(element, kAXSubroleAttribute), + attributeString(element, kAXRoleDescriptionAttribute), + attributeString(element, kAXDescriptionAttribute), + attributeString(element, kAXHelpAttribute), + attributeString(element, kAXTitleAttribute), + ] + .compactMap { $0?.lowercased() } + .joined(separator: " ") +} + +func elementLooksTextual(_ element: AXUIElement) -> Bool { + let role = attributeString(element, kAXRoleAttribute) + let subrole = attributeString(element, kAXSubroleAttribute) + let editable = attributeBool(element, axEditableAttribute) + let metadata = metadataString(for: element) + let actions = actionNames(element) + + let textRoles: Set = [ + kAXTextFieldRole as String, + kAXTextAreaRole as String, + kAXComboBoxRole as String, + kAXSearchFieldSubrole as String, + ] + + if editable == true || textRoles.contains(role ?? "") || textRoles.contains(subrole ?? "") { + return true + } + + if metadata.contains("text field") + || metadata.contains("search field") + || metadata.contains("editor") + || metadata.contains("insertion point") + || metadata.contains("caret") + || metadata.contains("source editor") { + return true + } + + return hasAttribute(element, kAXSelectedTextRangeAttribute as String) + || hasAttribute(element, kAXNumberOfCharactersAttribute as String) + || (actions.contains(kAXPressAction as String) && metadata.contains("text")) +} + +func accessibilityCursorMatch() -> String? { + let hoveredChain = ancestorChain(startingAt: currentElement()) + let focusedChain = ancestorChain(startingAt: focusedElement()) + + for element in hoveredChain + focusedChain { + if elementLooksTextual(element) { + return "text" + } + } + + guard let element = hoveredChain.first else { + return nil + } + + let role = attributeString(element, kAXRoleAttribute) + let enabled = attributeBool(element, kAXEnabledAttribute) + let actions = actionNames(element) + let metadata = hoveredChain + .map { metadataString(for: $0) } + .filter { !$0.isEmpty } + .joined(separator: " ") + if metadata.contains("crosshair") || metadata.contains("cross hair") || metadata.contains("precision") || metadata.contains("crop") { + return "crosshair" + } + + if role == kAXSplitterRole as String { + return "resize-ew" + } + + let pressableRoles: Set = [ + kAXButtonRole as String, + axLinkRole, + kAXMenuItemRole as String, + kAXPopUpButtonRole as String, + kAXRadioButtonRole as String, + kAXCheckBoxRole as String, + kAXTabGroupRole as String, + ] + let hasPressAction = actions.contains(kAXPressAction as String) + if enabled == false && (hasPressAction || pressableRoles.contains(role ?? "")) { + return "not-allowed" + } + if hasPressAction || pressableRoles.contains(role ?? "") { + return "pointer" + } + + return nil +} + +func currentSystemCursorType() -> String { + let resolvedCursor: NSCursor? = DispatchQueue.main.sync { + if #available(macOS 14.0, *) { + return NSCursor.currentSystem ?? NSCursor.current + } + + return NSCursor.current + } + + guard let resolvedCursor else { + return accessibilityCursorMatch() ?? "arrow" + } + + guard let currentSignature = signature(for: resolvedCursor) else { + return accessibilityCursorMatch() ?? "arrow" + } + + guard let bestMatch = knownCursorSignatures.min(by: { lhs, rhs in + signatureScore(currentSignature, lhs.1) < signatureScore(currentSignature, rhs.1) + }) else { + return accessibilityCursorMatch() ?? "arrow" + } + + let bestScore = signatureScore(currentSignature, bestMatch.1) + let primaryThreshold = strictSignatureAcceptanceThresholds[bestMatch.0] ?? signatureAcceptanceThreshold + let matchedCursorType: String + if bestScore > primaryThreshold { + if let relaxedThreshold = relaxedSignatureAcceptanceThresholds[bestMatch.0], bestScore <= relaxedThreshold { + matchedCursorType = bestMatch.0 + } else { + matchedCursorType = "arrow" + } + } else { + matchedCursorType = bestMatch.0 + } + + return matchedCursorType +} + +func exportCursorImages() { + let cursorForType: [(String, NSCursor)] = [ + ("arrow", .arrow), + ("text", .iBeam), + ("pointer", .pointingHand), + ("crosshair", .crosshair), + ("open-hand", .openHand), + ("closed-hand", .closedHand), + ("resize-ew", .resizeLeftRight), + ("resize-ns", .resizeUpDown), + ("not-allowed", .operationNotAllowed), + ] + + for (name, cursor) in cursorForType { + let image = cursor.image + let hotspot = cursor.hotSpot + let size = image.size + guard size.width > 0, size.height > 0 else { continue } + + guard let tiffData = image.tiffRepresentation, + let bitmapRep = NSBitmapImageRep(data: tiffData), + let pngData = bitmapRep.representation(using: .png, properties: [:]) else { + continue + } + + let base64 = pngData.base64EncodedString() + let hotspotXRatio = hotspot.x / size.width + let hotspotYRatio = hotspot.y / size.height + let aspectRatio = size.width / size.height + print("CURSOR_IMAGE:\(name):\(hotspotXRatio):\(hotspotYRatio):\(aspectRatio):\(base64)") + fflush(stdout) + } +} + +if CommandLine.arguments.contains("--export-images") { + exportCursorImages() + exit(0) +} + +func mouseInteractionCallback( + proxy: CGEventTapProxy, + type: CGEventType, + event: CGEvent, + refcon: UnsafeMutableRawPointer? +) -> Unmanaged? { + let action: String + let button: Int + switch type { + case .leftMouseDown: + action = "mousedown" + button = 1 + case .leftMouseUp: + action = "mouseup" + button = 1 + case .rightMouseDown: + action = "mousedown" + button = 2 + case .rightMouseUp: + action = "mouseup" + button = 2 + case .otherMouseDown: + guard event.getIntegerValueField(.mouseEventButtonNumber) == 2 else { + return Unmanaged.passUnretained(event) + } + action = "mousedown" + button = 3 + case .otherMouseUp: + guard event.getIntegerValueField(.mouseEventButtonNumber) == 2 else { + return Unmanaged.passUnretained(event) + } + action = "mouseup" + button = 3 + default: + return Unmanaged.passUnretained(event) + } + + print("INTERACTION:\(action):\(button)") + fflush(stdout) + return Unmanaged.passUnretained(event) +} + +let mouseEventTypes: [CGEventType] = [ + .leftMouseDown, + .leftMouseUp, + .rightMouseDown, + .rightMouseUp, + .otherMouseDown, + .otherMouseUp, +] +let mouseEventMask = mouseEventTypes.reduce(CGEventMask(0)) { mask, type in + mask | (CGEventMask(1) << type.rawValue) +} +let mouseEventTap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .listenOnly, + eventsOfInterest: mouseEventMask, + callback: mouseInteractionCallback, + userInfo: nil +) +if let mouseEventTap, + let eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0) { + CFRunLoopAddSource(CFRunLoopGetMain(), eventTapSource, .commonModes) + CGEvent.tapEnable(tap: mouseEventTap, enable: true) +} else { + fputs("Mouse interaction event tap unavailable; click telemetry disabled\n", stderr) + fflush(stderr) +} + +var lastState = "" +func emitStateIfNeeded() { + let state = currentSystemCursorType() + if state != lastState { + lastState = state + print("STATE:\(state)") + fflush(stdout) + } +} + +let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) +timer.schedule(deadline: .now(), repeating: .milliseconds(50)) +timer.setEventHandler { + emitStateIfNeeded() +} +timer.resume() + +DispatchQueue.global(qos: .utility).async { + while let line = readLine(strippingNewline: true)?.lowercased() { + if line == "stop" { + exit(0) + } + } + exit(0) +} + +RunLoop.main.run() diff --git a/electron/native/ScreenCaptureKitRecorder.lifecycle.test.ts b/electron/native/ScreenCaptureKitRecorder.lifecycle.test.ts new file mode 100644 index 000000000..1e01a43eb --- /dev/null +++ b/electron/native/ScreenCaptureKitRecorder.lifecycle.test.ts @@ -0,0 +1,103 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +// Exercise the production service and stdin reader in a real Swift process. +// Only the device backend is simulated: CI must not request screen/mic access. +describe.skipIf(process.platform !== "darwin")("recorder process lifecycle", () => { + let directory: string; + let executable: string; + beforeAll(() => { + directory = mkdtempSync(join(tmpdir(), "recordly-lifecycle-")); + executable = join(directory, "helper"); + const source = readFileSync( + new URL("./ScreenCaptureKitRecorder.swift", import.meta.url), + "utf8", + ); + const service = source.slice( + source.indexOf("final class RecorderService"), + source.indexOf("guard CommandLine.arguments.count"), + ); + const commands = source + .slice( + source.indexOf("let service = RecorderService()"), + source.indexOf("if !service.waitUntilFinished()"), + ) + .replace( + "service.stop()", + "service.stop()\n\tservice.drainCommandsForTest()\n\tcommandsFinished.signal()", + ); + writeFileSync( + join(directory, "main.swift"), + ` +import Foundation +final class ScreenCaptureRecorder { + var output = "" + func startCapture(configJSON: String) async throws { + try await Task.sleep(nanoseconds: 50_000_000) + if configJSON == "fail" { throw NSError(domain: "startup", code: 1) } + output = configJSON + try "recording".write(toFile: output, atomically: true, encoding: .utf8) + } + func stopCapture() async throws -> String { + print("BACKEND_STOP") + fflush(stdout) + if output.isEmpty { throw NSError(domain: "inactive", code: 2) } + try "finalized".write(toFile: output, atomically: true, encoding: .utf8) + return output + } + func pauseCapture() async -> Bool { true } + func resumeCapture() async -> Bool { true } +} +${service} +extension RecorderService { + func drainCommandsForTest() { queue.sync {} } +} +let commandsFinished = DispatchSemaphore(value: 0) +${commands} +let success = service.waitUntilFinished() +// Wait for the reader to enqueue stop AND for the serialized operations to finish. +commandsFinished.wait() +exit(success ? 0 : 1) +`, + ); + const build = spawnSync( + "swiftc", + [ + "-module-cache-path", + join(directory, "cache"), + join(directory, "main.swift"), + "-o", + executable, + ], + { encoding: "utf8", timeout: 60_000 }, + ); + expect(build.status, build.stderr).toBe(0); + }, 60_000); + afterAll(() => { + if (directory) rmSync(directory, { recursive: true, force: true }); + }); + + it.each([ + "", + "stop\n", + "pause\nresume\n", + "stop\nstop\n", + ])("finalizes once with stdin %j", (input) => { + const output = join(directory, "recording"); + const result = spawnSync(executable, [output], { input, encoding: "utf8", timeout: 5_000 }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.match(/BACKEND_STOP/g)).toHaveLength(1); + expect(readFileSync(output, "utf8")).toBe("finalized"); + }); + + it.each(["", "stop\n"])("exits cleanly after failed startup with stdin %j", (input) => { + const result = spawnSync(executable, ["fail"], { input, encoding: "utf8", timeout: 5_000 }); + expect(result.signal, result.stderr).toBeNull(); + expect(result.status, result.stderr).toBe(1); + expect(result.stderr).toContain("Error starting capture"); + expect(result.stdout).not.toContain("BACKEND_STOP"); + }); +}); diff --git a/electron/native/ScreenCaptureKitRecorder.swift b/electron/native/ScreenCaptureKitRecorder.swift new file mode 100644 index 000000000..707813a0a --- /dev/null +++ b/electron/native/ScreenCaptureKitRecorder.swift @@ -0,0 +1,1131 @@ +import Foundation +import ScreenCaptureKit +import AVFoundation +import CoreGraphics +import CoreImage + +struct CaptureConfig: Codable { + let fps: Int? + let displayId: CGDirectDisplayID? + let windowId: UInt32? + let windowX: Double? + let windowY: Double? + let windowWidth: Double? + let windowHeight: Double? + /// A fixed area of `displayId` in global display points (top-left origin). + let regionX: Double? + let regionY: Double? + let regionWidth: Double? + let regionHeight: Double? + let outputPath: String? + let capturesSystemAudio: Bool? + let capturesMicrophone: Bool? + let systemAudioOutputPath: String? + let microphoneDeviceId: String? + let microphoneLabel: String? + let microphoneOutputPath: String? + let excludedProcessIds: [Int32]? +} + +let targetCaptureFPS = 60 +let maxInlineAudioTailExtension = CMTime(seconds: 2.0, preferredTimescale: 600) +/// How long finalization waits for a backed-up encoder queue before giving up on +/// the optional tail frame: 100 polls x 10 ms = 1 s. +let writerReadinessPollAttempts = 100 +let writerReadinessPollInterval: UInt64 = 10_000_000 + +final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { + private struct CaptureFinalizationResult { + let outputResult: Result + let interactiveStopParticipated: Bool + } + + private let queue = DispatchQueue(label: "recordly.screencapturekit.video") + private var assetWriter: AVAssetWriter? + private var videoInput: AVAssetWriterInput? + private var videoPixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor? + private var windowCropRect: CGRect? + private var windowCropDisplayId: CGDirectDisplayID? + private var excludedProcessIds = Set() + private var lastCroppedPixelBuffer: CVPixelBuffer? + private let imageContext = CIContext(options: [.cacheIntermediates: false]) + private var systemAudioWriter: AVAssetWriter? + private var systemAudioInput: AVAssetWriterInput? + private var microphoneOnlyWriter: AVAssetWriter? + private var microphoneOnlyInput: AVAssetWriterInput? + private var stream: SCStream? + private var firstSampleTime: CMTime = .zero + private var firstSystemAudioSampleTime: CMTime? + private var firstMicrophoneSampleTime: CMTime? + private var lastSystemAudioPresentationTime: CMTime = .invalid + private var lastMicrophonePresentationTime: CMTime = .invalid + private var lastSampleBuffer: CMSampleBuffer? + private var lastVideoPresentationTime: CMTime = .zero + private var lastVideoDuration: CMTime = .zero + private var lastInlineAudioPresentationTime: CMTime = .invalid + private var lastInlineAudioDuration: CMTime = .zero + private var isRecording = false + private var isPaused = false + private var pauseStartedHostTime: CMTime? + private var pendingResumeAdjustment = false + private var accumulatedPausedDuration: CMTime = .zero + private var sessionStarted = false + private var frameCount = 0 + private var outputURL: URL? + private var microphoneOutputURL: URL? + private var trackedWindowId: UInt32? + private var windowValidationTask: Task? + private var isFinalizing = false + private var interactiveStopParticipated = false + private var finalizationWaiters: [CheckedContinuation] = [] + private var inlineAudioInput: AVAssetWriterInput? + private var firstInlineAudioSampleTime: CMTime? + private var capturesSystemAudio = false + private var capturesMicrophone = false + private var writesSystemAudioToSeparateTrack = false + private var writesMicrophoneToSeparateTrack = false + + private let microphoneOutputTypeRawValue = 2 + + func startCapture(configJSON: String) async throws { + guard !isRecording else { + throw NSError(domain: "RecordlyCapture", code: 1, userInfo: [NSLocalizedDescriptionKey: "Recording is already in progress"]) + } + + guard let data = configJSON.data(using: .utf8) else { + throw NSError(domain: "RecordlyCapture", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON input"]) + } + + let config = try JSONDecoder().decode(CaptureConfig.self, from: data) + let availableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) + let streamConfig = SCStreamConfiguration() + capturesSystemAudio = config.capturesSystemAudio ?? false + capturesMicrophone = config.capturesMicrophone ?? false + if capturesMicrophone && !supportsNativeMicrophoneCapture(streamConfig: streamConfig) { + fputs("MICROPHONE_CAPTURE_UNAVAILABLE\n", stderr) + fflush(stderr) + capturesMicrophone = false + } + writesSystemAudioToSeparateTrack = capturesSystemAudio + writesMicrophoneToSeparateTrack = capturesSystemAudio && capturesMicrophone + let requestedFPS = max(targetCaptureFPS, config.fps ?? targetCaptureFPS) + streamConfig.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(requestedFPS)) + streamConfig.queueDepth = 6 + streamConfig.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + streamConfig.colorSpaceName = CGColorSpace.sRGB + streamConfig.colorMatrix = CGDisplayStream.yCbCrMatrix_ITU_R_709_2 + streamConfig.showsCursor = false + streamConfig.capturesAudio = capturesSystemAudio || capturesMicrophone + streamConfig.sampleRate = 48000 + streamConfig.channelCount = 2 + streamConfig.excludesCurrentProcessAudio = true + + if capturesMicrophone { + streamConfig.setValue(true, forKey: "captureMicrophone") + if let microphoneDeviceId = Self.resolveMicrophoneCaptureDeviceID(config: config) { + streamConfig.setValue(microphoneDeviceId, forKey: "microphoneCaptureDeviceID") + } + } + + let filter: SCContentFilter + let outputWidth: Int + let outputHeight: Int + excludedProcessIds = Set(config.excludedProcessIds ?? []) + let excludedApplications = availableContent.applications.filter { + excludedProcessIds.contains($0.processID) + } + + // A window or an area records the part of its display inside a frame, so menus + // and popovers over a window are captured just as they appear on screen. + let requestedFrame: CGRect? + if let windowId = config.windowId { + trackedWindowId = windowId + guard let window = availableContent.windows.first(where: { $0.windowID == windowId }) else { + throw NSError(domain: "RecordlyCapture", code: 3, userInfo: [NSLocalizedDescriptionKey: "Window not found"]) + } + + // Accessibility reports the visible frame at the native border. + if let x = config.windowX, + let y = config.windowY, + let width = config.windowWidth, + let height = config.windowHeight, + width > 0, + height > 0 { + requestedFrame = CGRect(x: x, y: y, width: width, height: height) + } else { + requestedFrame = window.frame + } + } else if let x = config.regionX, + let y = config.regionY, + let width = config.regionWidth, + let height = config.regionHeight, + width > 0, + height > 0 { + trackedWindowId = nil + requestedFrame = CGRect(x: x, y: y, width: width, height: height) + } else { + requestedFrame = nil + } + + if let visibleFrame = requestedFrame { + let preferredDisplay = config.windowId == nil + ? config.displayId.flatMap { displayId in availableContent.displays.first(where: { $0.displayID == displayId }) } + : nil + guard let display = preferredDisplay ?? Self.captureDisplay(for: visibleFrame, from: availableContent.displays) else { + throw NSError(domain: "RecordlyCapture", code: 4, userInfo: [NSLocalizedDescriptionKey: "Window display not found"]) + } + let scaleFactor = ScreenCaptureRecorder.scaleFactor(for: display.displayID) + let captureRect = visibleFrame.intersection(display.frame) + guard captureRect.width >= 2, captureRect.height >= 2 else { + throw NSError(domain: "RecordlyCapture", code: 18, userInfo: [NSLocalizedDescriptionKey: "Capture area is outside the display"]) + } + filter = SCContentFilter( + display: display, + excludingApplications: excludedApplications, + exceptingWindows: [] + ) + windowCropRect = CGRect( + x: (captureRect.minX - display.frame.minX) / display.frame.width, + y: (captureRect.minY - display.frame.minY) / display.frame.height, + width: captureRect.width / display.frame.width, + height: captureRect.height / display.frame.height + ) + windowCropDisplayId = display.displayID + outputWidth = max(2, Int(captureRect.width) * scaleFactor) & ~1 + outputHeight = max(2, Int(captureRect.height) * scaleFactor) & ~1 + streamConfig.width = max(2, Int(display.frame.width) * scaleFactor) + streamConfig.height = max(2, Int(display.frame.height) * scaleFactor) + streamConfig.pixelFormat = kCVPixelFormatType_32BGRA + } else { + trackedWindowId = nil + windowCropRect = nil + windowCropDisplayId = nil + let displayId = config.displayId ?? CGMainDisplayID() + guard let display = availableContent.displays.first(where: { $0.displayID == displayId }) else { + throw NSError(domain: "RecordlyCapture", code: 4, userInfo: [NSLocalizedDescriptionKey: "Display not found"]) + } + + filter = SCContentFilter( + display: display, + excludingApplications: excludedApplications, + exceptingWindows: [] + ) + let displayBounds = CGDisplayBounds(display.displayID) + let scaleFactor = ScreenCaptureRecorder.scaleFactor(for: display.displayID) + outputWidth = max(2, Int(displayBounds.width) * scaleFactor) + outputHeight = max(2, Int(displayBounds.height) * scaleFactor) + streamConfig.width = outputWidth + streamConfig.height = outputHeight + } + + let destinationURL: URL + if let outputPath = config.outputPath, !outputPath.isEmpty { + destinationURL = URL(fileURLWithPath: outputPath) + } else { + destinationURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .appendingPathComponent("output_\(Int(Date().timeIntervalSince1970)).mp4") + } + + outputURL = destinationURL + let outputFileType: AVFileType = destinationURL.pathExtension.lowercased() == "mp4" ? .mp4 : .mov + assetWriter = try AVAssetWriter(url: destinationURL, fileType: outputFileType) + microphoneOutputURL = nil + firstSystemAudioSampleTime = nil + firstMicrophoneSampleTime = nil + lastSystemAudioPresentationTime = .invalid + lastMicrophonePresentationTime = .invalid + + guard let assistant = AVOutputSettingsAssistant(preset: .preset3840x2160) else { + throw NSError(domain: "RecordlyCapture", code: 5, userInfo: [NSLocalizedDescriptionKey: "Unable to create output settings assistant"]) + } + + let sourceVideoFormat = try CMVideoFormatDescription( + videoCodecType: CMFormatDescription.MediaSubType( + rawValue: windowCropRect == nil + ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + : kCVPixelFormatType_32BGRA + ), + width: outputWidth, + height: outputHeight + ) + assistant.sourceVideoFormat = sourceVideoFormat + + guard var outputSettings = assistant.videoSettings else { + throw NSError(domain: "RecordlyCapture", code: 6, userInfo: [NSLocalizedDescriptionKey: "Output settings unavailable"]) + } + + outputSettings[AVVideoWidthKey] = outputWidth + outputSettings[AVVideoHeightKey] = outputHeight + outputSettings[AVVideoColorPropertiesKey] = [ + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2, + ] + + let videoInput = AVAssetWriterInput( + mediaType: .video, + outputSettings: outputSettings, + sourceFormatHint: sourceVideoFormat + ) + videoInput.expectsMediaDataInRealTime = true + + guard let assetWriter = assetWriter, assetWriter.canAdd(videoInput) else { + throw NSError(domain: "RecordlyCapture", code: 7, userInfo: [NSLocalizedDescriptionKey: "Unable to add video writer input"]) + } + + assetWriter.add(videoInput) + self.videoInput = videoInput + videoPixelBufferAdaptor = windowCropRect.map { _ in + AVAssetWriterInputPixelBufferAdaptor( + assetWriterInput: videoInput, + sourcePixelBufferAttributes: [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, + kCVPixelBufferWidthKey as String: outputWidth, + kCVPixelBufferHeightKey as String: outputHeight, + ] + ) + } + + // Add inline audio track directly to the video so the .mp4 always contains audio. + // This eliminates the dependency on the post-recording ffmpeg mux step. + if capturesSystemAudio || capturesMicrophone { + let inlineAudio = AVAssetWriterInput(mediaType: .audio, outputSettings: Self.audioOutputSettings(bitRate: 192_000)) + inlineAudio.expectsMediaDataInRealTime = true + if assetWriter.canAdd(inlineAudio) { + assetWriter.add(inlineAudio) + self.inlineAudioInput = inlineAudio + } + } + + if writesSystemAudioToSeparateTrack { + guard let systemAudioOutputPath = config.systemAudioOutputPath, !systemAudioOutputPath.isEmpty else { + throw NSError(domain: "RecordlyCapture", code: 11, userInfo: [NSLocalizedDescriptionKey: "Missing system audio output path for audio capture"]) + } + + let systemAudioURL = URL(fileURLWithPath: systemAudioOutputPath) + let systemAudioWriter = try AVAssetWriter(url: systemAudioURL, fileType: .m4a) + let systemAudioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: Self.audioOutputSettings(bitRate: 160_000)) + systemAudioInput.expectsMediaDataInRealTime = true + + guard systemAudioWriter.canAdd(systemAudioInput) else { + throw NSError(domain: "RecordlyCapture", code: 12, userInfo: [NSLocalizedDescriptionKey: "Unable to add system audio writer input"]) + } + + systemAudioWriter.add(systemAudioInput) + self.systemAudioWriter = systemAudioWriter + self.systemAudioInput = systemAudioInput + + guard systemAudioWriter.startWriting() else { + throw NSError(domain: "RecordlyCapture", code: 13, userInfo: [NSLocalizedDescriptionKey: systemAudioWriter.error?.localizedDescription ?? "Unable to start system audio writing"]) + } + + systemAudioWriter.startSession(atSourceTime: .zero) + } + + if writesMicrophoneToSeparateTrack { + guard let microphoneOutputPath = config.microphoneOutputPath, !microphoneOutputPath.isEmpty else { + throw NSError(domain: "RecordlyCapture", code: 14, userInfo: [NSLocalizedDescriptionKey: "Missing microphone output path for microphone capture"]) + } + + let microphoneURL = URL(fileURLWithPath: microphoneOutputPath) + microphoneOutputURL = microphoneURL + let microphoneWriter = try AVAssetWriter(url: microphoneURL, fileType: .m4a) + let microphoneInput = AVAssetWriterInput(mediaType: .audio, outputSettings: Self.audioOutputSettings(bitRate: 128_000)) + microphoneInput.expectsMediaDataInRealTime = true + + guard microphoneWriter.canAdd(microphoneInput) else { + throw NSError(domain: "RecordlyCapture", code: 15, userInfo: [NSLocalizedDescriptionKey: "Unable to add microphone writer input"]) + } + + microphoneWriter.add(microphoneInput) + self.microphoneOnlyWriter = microphoneWriter + self.microphoneOnlyInput = microphoneInput + + guard microphoneWriter.startWriting() else { + throw NSError(domain: "RecordlyCapture", code: 16, userInfo: [NSLocalizedDescriptionKey: microphoneWriter.error?.localizedDescription ?? "Unable to start microphone audio writing"]) + } + + microphoneWriter.startSession(atSourceTime: .zero) + } + + let stream = SCStream(filter: filter, configuration: streamConfig, delegate: self) + self.stream = stream + try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: queue) + if capturesSystemAudio { + try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: queue) + } + if capturesMicrophone { + guard let microphoneOutputType = SCStreamOutputType(rawValue: microphoneOutputTypeRawValue) else { + throw NSError( + domain: "RecordlyCapture", + code: 17, + userInfo: [NSLocalizedDescriptionKey: "Microphone stream output type is unavailable"] + ) + } + try stream.addStreamOutput(self, type: microphoneOutputType, sampleHandlerQueue: queue) + } + try await stream.startCapture() + + guard assetWriter.startWriting() else { + throw NSError(domain: "RecordlyCapture", code: 8, userInfo: [NSLocalizedDescriptionKey: assetWriter.error?.localizedDescription ?? "Unable to start video writing"]) + } + + assetWriter.startSession(atSourceTime: .zero) + sessionStarted = true + isRecording = true + isPaused = false + pauseStartedHostTime = nil + pendingResumeAdjustment = false + accumulatedPausedDuration = .zero + frameCount = 0 + firstSampleTime = .zero + lastVideoPresentationTime = .zero + lastVideoDuration = .zero + startWindowValidationIfNeeded() + } + + func stopCapture() async throws -> String { + let finalization = await finalizeCapture(interactive: true) + return try finalization.outputResult.get() + } + + func pauseCapture() async -> Bool { + await withCheckedContinuation { continuation in + queue.async { + guard self.isRecording, !self.isPaused else { + continuation.resume(returning: self.isRecording && self.isPaused) + return + } + self.isPaused = true + self.pauseStartedHostTime = CMClockGetTime(CMClockGetHostTimeClock()) + self.pendingResumeAdjustment = false + continuation.resume(returning: true) + } + } + } + + func resumeCapture() async -> Bool { + await withCheckedContinuation { continuation in + queue.async { + guard self.isRecording, self.isPaused else { + continuation.resume(returning: self.isRecording && !self.isPaused) + return + } + self.isPaused = false + self.pendingResumeAdjustment = true + continuation.resume(returning: true) + } + } + } + + func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { + guard sessionStarted, sampleBuffer.isValid, isRecording else { return } + + if outputType == .screen { + guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]], + let attachment = attachments.first, + let statusRawValue = attachment[SCStreamFrameInfo.status] as? Int, + let status = SCFrameStatus(rawValue: statusRawValue), + status == .complete else { + return + } + + guard let videoInput = videoInput, + assetWriter?.status == .writing, + videoInput.isReadyForMoreMediaData else { return } + + // Only a complete frame that the writer can accept may establish time zero. + guard let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } + if frameCount > 0 && CMTimeCompare(presentationTime, lastVideoPresentationTime) <= 0 { + return + } + + lastSampleBuffer = sampleBuffer + let appended: Bool + if videoPixelBufferAdaptor != nil { + appended = appendCroppedVideoFrame(sampleBuffer, at: presentationTime) + } else { + let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: presentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp) + if let retimed = try? CMSampleBuffer(copying: sampleBuffer, withNewTiming: [timing]) { + appended = videoInput.append(retimed) + } else { + appended = false + } + } + if appended { + lastVideoPresentationTime = presentationTime + lastVideoDuration = sampleBuffer.duration + frameCount += 1 + if frameCount == 1 { + // Signal readiness only after AVAssetWriter has accepted a + // real frame, so countdown warm-start cannot pause too early. + print("Recording started") + fflush(stdout) + } + } else if frameCount == 0 { + // A failed crop/append must not leave an empty interval before frame one. + firstSampleTime = .zero + } + return + } + + guard frameCount > 0, + let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } + + if outputType == .audio { + guard let systemAudioInput else { return } + appendAudioSampleBuffer(sampleBuffer, to: systemAudioInput, of: systemAudioWriter, firstSampleTime: &firstSystemAudioSampleTime, lastPresentationTime: &lastSystemAudioPresentationTime, presentationTime: presentationTime) + // Also write system audio to the inline video track + if let inlineAudioInput, inlineAudioInput.isReadyForMoreMediaData { + appendAudioSampleBuffer(sampleBuffer, to: inlineAudioInput, of: assetWriter, firstSampleTime: &firstInlineAudioSampleTime, lastPresentationTime: &lastInlineAudioPresentationTime, presentationTime: presentationTime) + } + return + } + + if outputType.rawValue == microphoneOutputTypeRawValue { + if let microphoneOnlyInput { + appendAudioSampleBuffer(sampleBuffer, to: microphoneOnlyInput, of: microphoneOnlyWriter, firstSampleTime: &firstMicrophoneSampleTime, lastPresentationTime: &lastMicrophonePresentationTime, presentationTime: presentationTime) + } + // Write mic to inline video track only if there's no system audio (avoids double-writing) + if !capturesSystemAudio, let inlineAudioInput, inlineAudioInput.isReadyForMoreMediaData { + appendAudioSampleBuffer(sampleBuffer, to: inlineAudioInput, of: assetWriter, firstSampleTime: &firstInlineAudioSampleTime, lastPresentationTime: &lastInlineAudioPresentationTime, presentationTime: presentationTime) + } + return + } + + return + } + + private func appendCroppedVideoFrame(_ sampleBuffer: CMSampleBuffer, at presentationTime: CMTime) -> Bool { + guard let crop = windowCropRect, + let adaptor = videoPixelBufferAdaptor, + let pool = adaptor.pixelBufferPool, + let source = CMSampleBufferGetImageBuffer(sampleBuffer) else { return false } + + var destination: CVPixelBuffer? + guard CVPixelBufferPoolCreatePixelBuffer(nil, pool, &destination) == kCVReturnSuccess, + let destination else { return false } + + let sourceWidth = CGFloat(CVPixelBufferGetWidth(source)) + let sourceHeight = CGFloat(CVPixelBufferGetHeight(source)) + let sourceRect = CGRect( + x: crop.minX * sourceWidth, + y: (1 - crop.maxY) * sourceHeight, + width: crop.width * sourceWidth, + height: crop.height * sourceHeight + ) + let destinationSize = CGSize( + width: CVPixelBufferGetWidth(destination), + height: CVPixelBufferGetHeight(destination) + ) + let image = CIImage(cvPixelBuffer: source) + .cropped(to: sourceRect) + .transformed(by: CGAffineTransform(translationX: -sourceRect.minX, y: -sourceRect.minY)) + .transformed(by: CGAffineTransform( + scaleX: destinationSize.width / sourceRect.width, + y: destinationSize.height / sourceRect.height + )) + let bounds = CGRect(origin: .zero, size: destinationSize) + imageContext.render( + image, + to: destination, + bounds: bounds, + colorSpace: CGColorSpace(name: CGColorSpace.sRGB) + ) + + let appended = adaptor.append(destination, withPresentationTime: presentationTime) + if appended { lastCroppedPixelBuffer = destination } + return appended + } + + func stream(_ stream: SCStream, didStopWithError error: Error) { + fputs("Error: \(error.localizedDescription)\n", stderr) + fflush(stderr) + } + + /// Starts one finalization operation after all previously delivered samples on + /// the recorder queue have drained. Manual stop and automatic window-close + /// detection join the same operation instead of racing the asset writers. + private func finalizeCapture(interactive: Bool) async -> CaptureFinalizationResult { + await withCheckedContinuation { continuation in + queue.async { + if self.isFinalizing { + self.interactiveStopParticipated = self.interactiveStopParticipated || interactive + self.finalizationWaiters.append(continuation) + return + } + + guard self.isRecording else { + continuation.resume(returning: CaptureFinalizationResult( + outputResult: .failure(NSError( + domain: "RecordlyCapture", + code: 9, + userInfo: [NSLocalizedDescriptionKey: "No recording in progress"] + )), + interactiveStopParticipated: interactive + )) + return + } + + self.isFinalizing = true + self.interactiveStopParticipated = interactive + self.isRecording = false + self.windowValidationTask = nil + self.trackedWindowId = nil + self.finalizationWaiters.append(continuation) + + Task { + let outputResult: Result + do { + outputResult = .success(try await self.finishCapture()) + } catch { + outputResult = .failure(error) + } + + self.queue.async { + let finalizationResult = CaptureFinalizationResult( + outputResult: outputResult, + interactiveStopParticipated: self.interactiveStopParticipated + ) + let waiters = self.finalizationWaiters + self.finalizationWaiters.removeAll() + self.isFinalizing = false + self.interactiveStopParticipated = false + for waiter in waiters { + waiter.resume(returning: finalizationResult) + } + } + } + } + } + } + + private func finishCapture() async throws -> String { + + if let activeStream = stream { + do { + try await activeStream.stopCapture() + } catch { + // Stream may have already been stopped by the system — continue with file finalization + } + } + stream = nil + + // The tail frame only gives the last captured frame its full duration, so + // it must never put the file at risk. Appending to an input whose encoder + // queue is still backed up — routine after a long high-resolution capture — + // raises an Objective-C exception that Swift cannot catch, aborting the + // helper before `finishWriting()` and leaving an mdat with no moov atom: + // an unplayable recording. Wait briefly for the queue to drain, then skip + // the frame rather than lose the recording. + if let originalBuffer = lastSampleBuffer, + let videoInput = videoInput, + await waitUntilReady(videoInput, of: assetWriter) { + let additionalTime = lastVideoPresentationTime + frameDuration(for: originalBuffer) + if let adaptor = videoPixelBufferAdaptor, let pixelBuffer = lastCroppedPixelBuffer { + adaptor.append(pixelBuffer, withPresentationTime: additionalTime) + } else { + let timing = CMSampleTimingInfo(duration: originalBuffer.duration, presentationTimeStamp: additionalTime, decodeTimeStamp: originalBuffer.decodeTimeStamp) + if let additionalSampleBuffer = try? CMSampleBuffer(copying: originalBuffer, withNewTiming: [timing]) { + videoInput.append(additionalSampleBuffer) + } + } + } + + // `endSession`, `markAsFinished` and `finishWriting` all raise when the + // writer is no longer in the `.writing` state (a mid-capture failure, for + // example a full disk), which would abort the helper the same way. + let videoEndTime = lastVideoPresentationTime + (lastSampleBuffer.map { frameDuration(for: $0) } ?? .zero) + let endTime = resolvedCaptureEndTime(videoEndTime: videoEndTime) + if let assetWriter, assetWriter.status == .writing { + assetWriter.endSession(atSourceTime: endTime) + videoInput?.markAsFinished() + inlineAudioInput?.markAsFinished() + await assetWriter.finishWriting() + } + + if let systemAudioWriter, systemAudioWriter.status == .writing { + systemAudioInput?.markAsFinished() + await systemAudioWriter.finishWriting() + } + + if let microphoneOnlyWriter, microphoneOnlyWriter.status == .writing { + microphoneOnlyInput?.markAsFinished() + await microphoneOnlyWriter.finishWriting() + } + + let finalizeFailure: Error? = [assetWriter, systemAudioWriter, microphoneOnlyWriter] + .compactMap { $0 } + .compactMap { writer in + writer.status == .completed + ? nil + : (writer.error ?? unfinalizedWriterError(status: writer.status)) + } + .first + let path = outputURL?.path ?? "" + assetWriter = nil + videoInput = nil + videoPixelBufferAdaptor = nil + windowCropRect = nil + windowCropDisplayId = nil + excludedProcessIds.removeAll() + lastCroppedPixelBuffer = nil + systemAudioWriter = nil + systemAudioInput = nil + microphoneOnlyWriter = nil + microphoneOnlyInput = nil + inlineAudioInput = nil + outputURL = nil + microphoneOutputURL = nil + sessionStarted = false + firstSampleTime = .zero + firstSystemAudioSampleTime = nil + firstMicrophoneSampleTime = nil + lastSystemAudioPresentationTime = .invalid + lastMicrophonePresentationTime = .invalid + firstInlineAudioSampleTime = nil + lastSampleBuffer = nil + lastVideoPresentationTime = .zero + lastVideoDuration = .zero + lastInlineAudioPresentationTime = .invalid + lastInlineAudioDuration = .zero + frameCount = 0 + isPaused = false + pauseStartedHostTime = nil + pendingResumeAdjustment = false + accumulatedPausedDuration = .zero + capturesSystemAudio = false + capturesMicrophone = false + writesSystemAudioToSeparateTrack = false + writesMicrophoneToSeparateTrack = false + + // Report a half-written file as a failure instead of handing the editor a + // path it cannot decode. + if let finalizeFailure { + throw finalizeFailure + } + + return path + } + + /// Waits briefly for an input's encoder queue to drain. Returns false when the + /// input stays backed up or its writer is no longer accepting data, in which + /// case the caller must skip the append: `AVAssetWriterInput.append` raises an + /// uncatchable Objective-C exception in both cases. + private func waitUntilReady(_ input: AVAssetWriterInput, of writer: AVAssetWriter?) async -> Bool { + guard let writer else { return false } + + var attemptsRemaining = writerReadinessPollAttempts + while writer.status == .writing { + if input.isReadyForMoreMediaData { + return true + } + guard attemptsRemaining > 0 else { return false } + attemptsRemaining -= 1 + do { + try await Task.sleep(nanoseconds: writerReadinessPollInterval) + } catch is CancellationError { + return false + } catch { + return false + } + } + + return false + } + + private func unfinalizedWriterError(status: AVAssetWriter.Status) -> Error { + NSError(domain: "RecordlyCapture", code: 10, userInfo: [ + NSLocalizedDescriptionKey: "Recording could not be finalized (writer status \(status.rawValue))", + ]) + } + + private func adjustedPresentationTime(for sampleBuffer: CMSampleBuffer, outputType: SCStreamOutputType) -> CMTime? { + if isPaused { + return nil + } + + let sampleTime = sampleBuffer.presentationTimeStamp + if pendingResumeAdjustment { + // Audio and video callbacks share this queue but their timestamps can be + // offset slightly. Anchor the post-countdown adjustment to video and drop + // audio until that anchor exists; otherwise the first audio callback can + // make the following video timestamp move backwards and fail the writer. + guard outputType == .screen, let pauseStartedHostTime else { + return nil + } + let pauseGap = sampleTime - pauseStartedHostTime + if pauseGap > .zero { + accumulatedPausedDuration = accumulatedPausedDuration + pauseGap + } + self.pauseStartedHostTime = nil + pendingResumeAdjustment = false + } + + if outputType == .screen { + if firstSampleTime == .zero { + firstSampleTime = sampleTime + } + } + + // Use video's first sample time as the common time base for ALL tracks. + // This ensures audio files contain leading silence when audio hardware + // delivers its first sample after the first video frame (e.g. iPhone mic + // over Continuity Camera can lag 1-2 seconds behind). + if firstSampleTime == .zero { + // Video hasn't started yet — drop this audio sample to avoid + // negative timestamps. + return nil + } + + return max(.zero, sampleTime - firstSampleTime - accumulatedPausedDuration) + } + + private func frameDuration(for sampleBuffer: CMSampleBuffer) -> CMTime { + if sampleBuffer.duration.isValid && sampleBuffer.duration > .zero { + return sampleBuffer.duration + } + + if lastVideoDuration.isValid && lastVideoDuration > .zero { + return lastVideoDuration + } + + return CMTime(value: 1, timescale: CMTimeScale(targetCaptureFPS)) + } + + private func latestInlineAudioEndTime() -> CMTime { + guard lastInlineAudioPresentationTime.isValid else { + return .invalid + } + + if lastInlineAudioDuration.isValid && lastInlineAudioDuration > .zero { + return lastInlineAudioPresentationTime + lastInlineAudioDuration + } + + return lastInlineAudioPresentationTime + } + + private func resolvedCaptureEndTime(videoEndTime: CMTime) -> CMTime { + let inlineAudioEndTime = latestInlineAudioEndTime() + guard inlineAudioEndTime.isValid else { + return videoEndTime + } + + if CMTimeCompare(inlineAudioEndTime, videoEndTime) <= 0 { + return videoEndTime + } + + // Prevent a stray inline-audio timestamp from forcing finishWriting + // to finalize an arbitrarily long tail. + let tailExtension = CMTimeSubtract(inlineAudioEndTime, videoEndTime) + return videoEndTime + CMTimeMinimum(tailExtension, maxInlineAudioTailExtension) + } + + private func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer, to input: AVAssetWriterInput, of writer: AVAssetWriter?, firstSampleTime: inout CMTime?, lastPresentationTime: inout CMTime, presentationTime: CMTime) { + // A writer that failed mid-capture (a full disk, say) raises on every + // further append, which would abort the helper and lose the whole file. + guard writer?.status == .writing, input.isReadyForMoreMediaData else { return } + guard !lastPresentationTime.isValid || CMTimeCompare(presentationTime, lastPresentationTime) > 0 else { return } + + if firstSampleTime == nil { + firstSampleTime = presentationTime + } + + // presentationTime is already relative to the video's first frame + // (computed by adjustedPresentationTime), so use it directly. + let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: presentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp) + if let retimedSampleBuffer = try? CMSampleBuffer(copying: sampleBuffer, withNewTiming: [timing]) { + let appended = input.append(retimedSampleBuffer) + if appended { + lastPresentationTime = presentationTime + if input === inlineAudioInput { + lastInlineAudioDuration = sampleBuffer.duration + } + } + } + } + + private static func audioOutputSettings(bitRate: Int) -> [String: Any] { + [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVSampleRateKey: 48_000, + AVNumberOfChannelsKey: 2, + AVEncoderBitRateKey: bitRate, + ] + } + + private static func resolveMicrophoneCaptureDeviceID(config: CaptureConfig) -> String? { + let audioDevices = AVCaptureDevice.devices(for: .audio) + + if let microphoneLabel = config.microphoneLabel?.trimmingCharacters(in: .whitespacesAndNewlines), !microphoneLabel.isEmpty { + if let matchedDevice = audioDevices.first(where: { $0.localizedName == microphoneLabel }) { + return matchedDevice.uniqueID + } + } + + if let microphoneDeviceId = config.microphoneDeviceId?.trimmingCharacters(in: .whitespacesAndNewlines), !microphoneDeviceId.isEmpty { + if audioDevices.contains(where: { $0.uniqueID == microphoneDeviceId }) { + return microphoneDeviceId + } + } + + return nil + } + + private func supportsNativeMicrophoneCapture(streamConfig: SCStreamConfiguration) -> Bool { + let supportsConfigSelector = streamConfig.responds(to: Selector(("setCaptureMicrophone:"))) + let supportsDeviceSelector = streamConfig.responds(to: Selector(("setMicrophoneCaptureDeviceID:"))) + let supportsOutputType = SCStreamOutputType(rawValue: microphoneOutputTypeRawValue) != nil + return supportsConfigSelector && supportsDeviceSelector && supportsOutputType + } + + private func startWindowValidationIfNeeded() { + guard let trackedWindowId else { + windowValidationTask?.cancel() + windowValidationTask = nil + return + } + + windowValidationTask?.cancel() + windowValidationTask = Task.detached(priority: .utility) { [weak self] in + guard let self else { return } + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 500_000_000) + if Task.isCancelled { return } + guard self.isRecording else { return } + + let availableContent: SCShareableContent + do { + availableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) + } catch { + continue + } + + guard let window = availableContent.windows.first(where: { $0.windowID == trackedWindowId }) else { + print("WINDOW_UNAVAILABLE") + fflush(stdout) + let finalization = await self.finalizeCapture(interactive: false) + if finalization.interactiveStopParticipated { + return + } + do { + let outputPath = try finalization.outputResult.get() + print("Recording stopped. Output path: \(outputPath)") + fflush(stdout) + exit(0) + } catch { + fputs("Error stopping capture: \(error.localizedDescription)\n", stderr) + fflush(stderr) + exit(1) + } + return + } + + guard let display = Self.captureDisplay(for: window.frame, from: availableContent.displays) else { + continue + } + let captureRect = window.frame.intersection(display.frame) + guard captureRect.width > 0, captureRect.height > 0 else { continue } + let cropRect = CGRect( + x: (captureRect.minX - display.frame.minX) / display.frame.width, + y: (captureRect.minY - display.frame.minY) / display.frame.height, + width: captureRect.width / display.frame.width, + height: captureRect.height / display.frame.height + ) + + if self.windowCropDisplayId != display.displayID, let activeStream = self.stream { + let excludedApplications = availableContent.applications.filter { + self.excludedProcessIds.contains($0.processID) + } + let filter = SCContentFilter( + display: display, + excludingApplications: excludedApplications, + exceptingWindows: [] + ) + do { + try await activeStream.updateContentFilter(filter) + } catch { + continue + } + } + + await withCheckedContinuation { (continuation: CheckedContinuation) in + self.queue.async { + if self.isRecording { + self.windowCropRect = cropRect + self.windowCropDisplayId = display.displayID + } + continuation.resume() + } + } + } + } + } + + private static func captureDisplay(for frame: CGRect, from displays: [SCDisplay]) -> SCDisplay? { + let midpoint = CGPoint(x: frame.midX, y: frame.midY) + return displays.first(where: { $0.frame.contains(midpoint) }) + ?? displays.filter { $0.frame.intersects(frame) }.max { + $0.frame.intersection(frame).width * $0.frame.intersection(frame).height + < $1.frame.intersection(frame).width * $1.frame.intersection(frame).height + } + } + + private static func scaleFactor(for displayId: CGDirectDisplayID) -> Int { + guard let mode = CGDisplayCopyDisplayMode(displayId) else { + return 1 + } + return max(1, mode.pixelWidth / max(1, mode.width)) + } +} + +final class RecorderService { + private let recorder = ScreenCaptureRecorder() + private let queue = DispatchQueue(label: "recordly.screencapturekit.commands") + private let completionGroup = DispatchGroup() + private var succeeded = true + // Accessed only by serialized command operations. + private var captureStarted = false + + private func enqueue(_ operation: @escaping () async -> Void) { + queue.async { + let semaphore = DispatchSemaphore(value: 0) + Task { + await operation() + semaphore.signal() + } + semaphore.wait() + } + } + + func start(configJSON: String) { + completionGroup.enter() + enqueue { + do { + try await self.recorder.startCapture(configJSON: configJSON) + self.captureStarted = true + } catch { + self.succeeded = false + fputs("Error starting capture: \(error.localizedDescription)\n", stderr) + fflush(stderr) + self.completionGroup.leave() + } + } + } + + func stop() { + enqueue { + // Failed startup already releases completionGroup. EOF must not do it again. + guard self.captureStarted else { return } + self.captureStarted = false + do { + let outputPath = try await self.recorder.stopCapture() + print("Recording stopped. Output path: \(outputPath)") + fflush(stdout) + self.completionGroup.leave() + } catch { + self.succeeded = false + fputs("Error stopping capture: \(error.localizedDescription)\n", stderr) + fflush(stderr) + self.completionGroup.leave() + } + } + } + + func pause() { + enqueue { + if await self.recorder.pauseCapture() { + print("Recording paused") + fflush(stdout) + } + } + } + + func resume() { + enqueue { + if await self.recorder.resumeCapture() { + print("Recording resumed") + fflush(stdout) + } + } + } + + func waitUntilFinished() -> Bool { + completionGroup.wait() + return succeeded + } +} + +guard CommandLine.arguments.count >= 2 else { + fputs("Missing config JSON\n", stderr) + fflush(stderr) + exit(1) +} + +// Force CoreGraphics Services initialization on the main thread. +// ScreenCaptureKit still requires CoreGraphics Services to be initialized in a CLI tool. +let _ = CGMainDisplayID() + +// Pre-flight check: ensure screen recording permission is granted before +// attempting capture. On macOS 15+, a one-session grant may expire after the +// parent app restarts. CGRequestScreenCaptureAccess() will trigger the +// system-level permission dialog (or open System Settings) when not yet granted. +if !CGPreflightScreenCaptureAccess() { + let granted = CGRequestScreenCaptureAccess() + if !granted { + fputs("SCREEN_RECORDING_PERMISSION_DENIED\n", stderr) + fflush(stderr) + exit(1) + } +} + +// Pre-flight check for microphone access when mic capture is requested. +if let configData = CommandLine.arguments[1].data(using: .utf8), + let config = try? JSONDecoder().decode(CaptureConfig.self, from: configData), + config.capturesMicrophone == true { + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .authorized: + break + case .notDetermined: + let sem = DispatchSemaphore(value: 0) + AVCaptureDevice.requestAccess(for: .audio) { _ in sem.signal() } + sem.wait() + if AVCaptureDevice.authorizationStatus(for: .audio) != .authorized { + fputs("MICROPHONE_PERMISSION_DENIED\n", stderr) + fflush(stderr) + exit(1) + } + default: + fputs("MICROPHONE_PERMISSION_DENIED\n", stderr) + fflush(stderr) + exit(1) + } +} + +let service = RecorderService() +service.start(configJSON: CommandLine.arguments[1]) + +DispatchQueue.global(qos: .utility).async { + while let input = readLine(strippingNewline: true)?.lowercased() { + if input == "pause" { + service.pause() + continue + } + + if input == "resume" { + service.resume() + continue + } + + if input == "stop" { + break + } + } + // EOF means the Electron parent exited or restarted. Finalize and release + // capture devices just as we do for an explicit stop command. + service.stop() +} + +if !service.waitUntilFinished() { + exit(1) +} diff --git a/electron/native/ScreenCaptureKitRecorder.test.ts b/electron/native/ScreenCaptureKitRecorder.test.ts new file mode 100644 index 000000000..edaba7ee6 --- /dev/null +++ b/electron/native/ScreenCaptureKitRecorder.test.ts @@ -0,0 +1,119 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const recorderSource = readFileSync( + fileURLToPath(new URL("./ScreenCaptureKitRecorder.swift", import.meta.url)), + "utf8", +); + +describe("ScreenCaptureKitRecorder finalization coordination", () => { + it("finalizes on parent pipe closure as well as an explicit stop, exactly once", () => { + const commandLoop = recorderSource.slice( + recorderSource.indexOf("while let input = readLine"), + ); + expect(commandLoop).toMatch( + /if input == "stop"\s*\{\s*break\s*\}\s*\}\s*\/\/.*?service\.stop\(\)/s, + ); + expect(commandLoop.match(/service\.stop\(\)/g)).toHaveLength(1); + }); + + it("marks manual stops as participants in the shared finalization", () => { + expect(recorderSource).toContain("finalizeCapture(interactive: true)"); + expect(recorderSource).toContain("finalization.outputResult.get()"); + expect(recorderSource).toContain( + "self.interactiveStopParticipated = self.interactiveStopParticipated || interactive", + ); + }); + + it("does not let automatic window-close exit preempt a joined manual stop", () => { + expect(recorderSource).toContain("self.finalizeCapture(interactive: false)"); + expect(recorderSource).toMatch( + /if finalization\.interactiveStopParticipated\s*\{\s*return\s*\}/, + ); + }); +}); + +describe("ScreenCaptureKitRecorder resume timing", () => { + it("anchors warm-start resume timing to video before accepting audio", () => { + expect(recorderSource).toContain( + "guard outputType == .screen, let pauseStartedHostTime else", + ); + }); + + it("drops non-monotonic video and audio samples", () => { + expect(recorderSource).toContain( + "CMTimeCompare(presentationTime, lastVideoPresentationTime) <= 0", + ); + expect(recorderSource).toContain( + "CMTimeCompare(presentationTime, lastPresentationTime) > 0", + ); + }); +}); + +describe("ScreenCaptureKitRecorder colour metadata", () => { + it("asks ScreenCaptureKit for BT.709-compatible video-range frames", () => { + expect(recorderSource).toContain( + "streamConfig.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange", + ); + expect(recorderSource).toContain("streamConfig.colorSpaceName = CGColorSpace.sRGB"); + expect(recorderSource).toContain( + "streamConfig.colorMatrix = CGDisplayStream.yCbCrMatrix_ITU_R_709_2", + ); + expect(recorderSource).toContain("kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange"); + expect(recorderSource).toContain("sourceFormatHint: sourceVideoFormat"); + expect(recorderSource).not.toContain("videoCodecType: .h264"); + }); + + it("tags recordings as BT.709", () => { + expect(recorderSource).toContain("AVVideoColorPropertiesKey"); + expect(recorderSource).toContain("AVVideoColorPrimaries_ITU_R_709_2"); + expect(recorderSource).toContain("AVVideoTransferFunction_ITU_R_709_2"); + expect(recorderSource).toContain("AVVideoYCbCrMatrix_ITU_R_709_2"); + }); +}); + +describe("ScreenCaptureKitRecorder window capture", () => { + it("records the display and crops it to the selected window bounds", () => { + expect(recorderSource).not.toContain("streamConfig.sourceRect"); + expect(recorderSource).not.toContain("desktopIndependentWindow"); + expect(recorderSource).toContain( + "requestedFrame = CGRect(x: x, y: y, width: width, height: height)", + ); + expect(recorderSource).toContain( + "let captureRect = visibleFrame.intersection(display.frame)", + ); + expect(recorderSource).toContain("appendCroppedVideoFrame(sampleBuffer"); + }); + + it("records a fixed area of a display through the same crop", () => { + expect(recorderSource).toContain("let regionX: Double?"); + expect(recorderSource).toContain("} else if let x = config.regionX,"); + }); + + it("refreshes the crop and capture display while the window moves or resizes", () => { + expect(recorderSource).toContain( + "guard let display = Self.captureDisplay(for: window.frame", + ); + expect(recorderSource).toContain("try await activeStream.updateContentFilter(filter)"); + expect(recorderSource).toContain("self.windowCropRect = cropRect"); + }); +}); + +describe("ScreenCaptureKitRecorder first frame timing", () => { + const callback = recorderSource.slice( + recorderSource.indexOf("func stream(_ stream:"), + recorderSource.indexOf("func stream(_ stream:") + 5000, + ); + it("validates a complete frame and writer readiness before setting time zero", () => { + const clock = callback.indexOf("adjustedPresentationTime(for:"); + expect(clock).toBeGreaterThan(callback.indexOf("status == .complete")); + expect(clock).toBeGreaterThan(callback.indexOf("videoInput.isReadyForMoreMediaData")); + }); + it("resets the origin after a rejected first frame and gates audio on accepted video", () => { + expect(callback).toMatch(/else if frameCount == 0\s*\{[^}]*firstSampleTime = \.zero/); + const audioGuard = callback.indexOf("guard frameCount > 0,"); + expect(audioGuard).toBeGreaterThan(0); + expect(audioGuard).toBeLessThan(callback.indexOf("if outputType == .audio")); + }); +}); diff --git a/electron/native/ScreenCaptureKitWindowList.swift b/electron/native/ScreenCaptureKitWindowList.swift new file mode 100644 index 000000000..e0b3a6c20 --- /dev/null +++ b/electron/native/ScreenCaptureKitWindowList.swift @@ -0,0 +1,177 @@ +import Foundation +import CoreGraphics +import ScreenCaptureKit + +struct WindowListEntry: Codable { + let id: String + let name: String + let display_id: String + let appName: String? + let windowTitle: String? + let bundleId: String? + let x: Double + let y: Double + let width: Double + let height: Double + /// Process that owns the window, so callers can skip their own windows. + let ownerPid: Int32? +} + +func normalize(_ value: String?) -> String? { + guard let rawValue = value?.trimmingCharacters(in: .whitespacesAndNewlines), !rawValue.isEmpty else { + return nil + } + + return rawValue +} + +let excludedBundleIds: Set = [ + "com.apple.controlcenter", + "com.apple.dock", + "com.apple.WindowManager", + "com.apple.wallpaper.agent", +] + +let excludedWindowTitles: Set = [ + "Display 1 Backstop", + "Event Shield Window", + "Menubar", + "Offscreen Wallpaper Window", + "Wallpaper-", +] + +/// `--z-order` lists windows front to back, for hit-testing what is under a point, +/// instead of alphabetically for menus. +let listsInZOrder = CommandLine.arguments.contains("--z-order") + +// Force CoreGraphics Services initialization before asking ScreenCaptureKit for +// shareable content. Without this, the helper can stall sporadically when run +// as a standalone CLI process from Electron. +let _ = CGMainDisplayID() + +let group = DispatchGroup() +group.enter() + +Task { + do { + let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) + + struct RawWindowEntry { + let entry: WindowListEntry + let hasRawTitle: Bool + let bundleId: String? + } + + let rawEntries = shareableContent.windows.compactMap { window -> RawWindowEntry? in + let appName = normalize(window.owningApplication?.applicationName) + let windowTitle = normalize(window.title) + let bundleId = normalize(window.owningApplication?.bundleIdentifier) + let frame = window.frame + + guard window.windowLayer == 0 else { + return nil + } + + guard frame.width >= 50, frame.height >= 50 else { + return nil + } + + guard appName != nil || windowTitle != nil else { + return nil + } + + if let bundleId, excludedBundleIds.contains(bundleId) { + return nil + } + + if let windowTitle, excludedWindowTitles.contains(windowTitle) { + return nil + } + + let matchedDisplay = shareableContent.displays.first(where: { display in + display.frame.intersects(frame) || display.frame.contains(CGPoint(x: frame.midX, y: frame.midY)) + }) + + let resolvedWindowTitle = windowTitle ?? appName ?? "Window" + let resolvedName: String + if let appName, let windowTitle { + resolvedName = "\(appName) — \(windowTitle)" + } else { + resolvedName = resolvedWindowTitle + } + + let entry = WindowListEntry( + id: "window:\(window.windowID):0", + name: resolvedName, + display_id: matchedDisplay.map { String($0.displayID) } ?? "", + appName: appName, + windowTitle: resolvedWindowTitle, + bundleId: bundleId, + x: Double(frame.origin.x), + y: Double(frame.origin.y), + width: Double(frame.width), + height: Double(frame.height), + ownerPid: window.owningApplication?.processID + ) + + return RawWindowEntry(entry: entry, hasRawTitle: windowTitle != nil, bundleId: bundleId) + } + + // For apps with multiple windows, drop auxiliary windows that lack a + // distinct title (e.g. Arc's sidebar/tab-bar chrome). If ALL windows + // from an app lack titles, keep them all. + var titledCountByBundle: [String: Int] = [:] + for raw in rawEntries { + if let bid = raw.bundleId, raw.hasRawTitle { + titledCountByBundle[bid, default: 0] += 1 + } + } + + let visibleEntries = rawEntries + .filter { raw in + guard let bid = raw.bundleId else { return true } + if let titled = titledCountByBundle[bid], titled > 0 { + return raw.hasRawTitle + } + return true + } + .map { $0.entry } + + // CGWindowList reports on-screen windows front to back; ScreenCaptureKit's + // order is unspecified. + var frontToBackRank: [String: Int] = [:] + if listsInZOrder { + let windowInfo = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? [] + for (rank, info) in windowInfo.enumerated() { + if let number = info[kCGWindowNumber as String] as? Int { + frontToBackRank["window:\(number):0"] = rank + } + } + } + + let entries = listsInZOrder + ? visibleEntries.sorted { (frontToBackRank[$0.id] ?? Int.max) < (frontToBackRank[$1.id] ?? Int.max) } + : visibleEntries.sorted { lhs, rhs in + let lhsApp = lhs.appName ?? lhs.name + let rhsApp = rhs.appName ?? rhs.name + if lhsApp != rhsApp { + return lhsApp.localizedCaseInsensitiveCompare(rhsApp) == .orderedAscending + } + + return (lhs.windowTitle ?? lhs.name).localizedCaseInsensitiveCompare(rhs.windowTitle ?? rhs.name) == .orderedAscending + } + + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let data = try encoder.encode(entries) + FileHandle.standardOutput.write(data) + } catch { + fputs("Error listing windows: \(error.localizedDescription)\n", stderr) + fflush(stderr) + exit(1) + } + + group.leave() +} + +group.wait() diff --git a/electron/native/SystemCursorAssets.swift b/electron/native/SystemCursorAssets.swift new file mode 100644 index 000000000..b3adee519 --- /dev/null +++ b/electron/native/SystemCursorAssets.swift @@ -0,0 +1,210 @@ +import Foundation +import AppKit + +// Force AppKit initialization so all system cursor images are populated +let _ = NSApplication.shared + +struct CursorAsset: Codable { + let dataUrl: String + let hotspotX: Double + let hotspotY: Double + let width: Double + let height: Double +} + +let minimumCursorRenderScale: CGFloat = 12 + +/// Reference pixel height that all cursor images are normalized to after trimming. +/// Kept high so large in-app cursor scaling still preserves raster detail up to 10x. +let referencePixelHeight: CGFloat = 1024 + +func bestCursorCGImage(for image: NSImage) -> CGImage? { + var proposedRect = CGRect(origin: .zero, size: image.size) + if let cgImage = image.cgImage(forProposedRect: &proposedRect, context: nil, hints: nil) { + return cgImage + } + + let sortedRepresentations = image.representations.sorted { lhs, rhs in + let lhsPixels = lhs.pixelsWide * lhs.pixelsHigh + let rhsPixels = rhs.pixelsWide * rhs.pixelsHigh + return lhsPixels > rhsPixels + } + + for representation in sortedRepresentations { + if let bitmap = representation as? NSBitmapImageRep, let cgImage = bitmap.cgImage { + return cgImage + } + } + + return nil +} + +func makeRetinaImage(from cgImage: CGImage, pointSize: NSSize) -> NSImage { + let bitmap = NSBitmapImageRep(cgImage: cgImage) + bitmap.size = pointSize + let image = NSImage(size: pointSize) + image.addRepresentation(bitmap) + return image +} + +/// Find the bounding box of non-transparent pixels in a bitmap. +/// Returns (x, y, width, height) in pixel coordinates (origin top-left). +func findOpaqueBounds(_ bitmap: NSBitmapImageRep) -> (x: Int, y: Int, width: Int, height: Int)? { + let w = bitmap.pixelsWide + let h = bitmap.pixelsHigh + guard let data = bitmap.bitmapData else { return nil } + let bpr = bitmap.bytesPerRow + var minX = w, minY = h, maxX = -1, maxY = -1 + for y in 0.. 2 { + if x < minX { minX = x } + if x > maxX { maxX = x } + if y < minY { minY = y } + if y > maxY { maxY = y } + } + } + } + if maxX < 0 { return nil } + return (minX, minY, maxX - minX + 1, maxY - minY + 1) +} + +func makeAsset(for cursor: NSCursor) -> CursorAsset? { + let baseImage = cursor.image + let pointSize = baseImage.size + guard let sourceCGImage = bestCursorCGImage(for: baseImage) else { return nil } + let sourcePixelWidth = max(1, sourceCGImage.width) + let sourcePixelHeight = max(1, sourceCGImage.height) + let sourceScaleX = CGFloat(sourcePixelWidth) / max(1, pointSize.width) + let sourceScaleY = CGFloat(sourcePixelHeight) / max(1, pointSize.height) + let sourceScale = max(sourceScaleX, sourceScaleY) + let image = makeRetinaImage(from: sourceCGImage, pointSize: pointSize) + let renderScale = max( + minimumCursorRenderScale, + referencePixelHeight / max(1, CGFloat(sourcePixelHeight)) + ) + let pixelWidth = max(1, Int((CGFloat(sourcePixelWidth) * renderScale).rounded(.up))) + let pixelHeight = max(1, Int((CGFloat(sourcePixelHeight) * renderScale).rounded(.up))) + + // Render at full resolution + guard let bitmap = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: pixelWidth, + pixelsHigh: pixelHeight, + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0 + ) else { return nil } + + bitmap.size = NSSize(width: CGFloat(pixelWidth), height: CGFloat(pixelHeight)) + + NSGraphicsContext.saveGraphicsState() + guard let context = NSGraphicsContext(bitmapImageRep: bitmap) else { + NSGraphicsContext.restoreGraphicsState() + return nil + } + NSGraphicsContext.current = context + context.imageInterpolation = .none + context.cgContext.interpolationQuality = .none + image.draw( + in: NSRect(x: 0, y: 0, width: CGFloat(pixelWidth), height: CGFloat(pixelHeight)), + from: .zero, + operation: .copy, + fraction: 1 + ) + context.flushGraphics() + NSGraphicsContext.restoreGraphicsState() + + // Find opaque bounds and crop away transparent padding + guard let bounds = findOpaqueBounds(bitmap) else { return nil } + guard let fullCG = bitmap.cgImage else { return nil } + let cropRect = CGRect(x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height) + guard let croppedCG = fullCG.cropping(to: cropRect) else { return nil } + + // Scale cropped image to referencePixelHeight + let aspect = CGFloat(bounds.width) / CGFloat(bounds.height) + let outH = Int(referencePixelHeight) + let outW = max(1, Int((referencePixelHeight * aspect).rounded(.up))) + + guard let outBitmap = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: outW, + pixelsHigh: outH, + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0 + ) else { return nil } + + outBitmap.size = NSSize(width: outW, height: outH) + + NSGraphicsContext.saveGraphicsState() + guard let outCtx = NSGraphicsContext(bitmapImageRep: outBitmap) else { + NSGraphicsContext.restoreGraphicsState() + return nil + } + NSGraphicsContext.current = outCtx + outCtx.imageInterpolation = .none + outCtx.cgContext.interpolationQuality = .none + let nsImage = NSImage(cgImage: croppedCG, size: NSSize(width: bounds.width, height: bounds.height)) + nsImage.draw( + in: NSRect(x: 0, y: 0, width: outW, height: outH), + from: .zero, + operation: .copy, + fraction: 1 + ) + outCtx.flushGraphics() + NSGraphicsContext.restoreGraphicsState() + + guard let pngData = outBitmap.representation(using: .png, properties: [:]) else { return nil } + let base64 = pngData.base64EncodedString() + + // Adjust hotspot using the extracted source image scale so 2x cursor reps keep the correct anchor. + let hotspot = cursor.hotSpot + let hotspotPxX = hotspot.x * sourceScale * renderScale - CGFloat(bounds.x) + let hotspotPxY = hotspot.y * sourceScale * renderScale - CGFloat(bounds.y) + let scaleToOut = referencePixelHeight / CGFloat(bounds.height) + let outHotspotX = hotspotPxX * scaleToOut + let outHotspotY = hotspotPxY * scaleToOut + + return CursorAsset( + dataUrl: "data:image/png;base64,\(base64)", + hotspotX: Double(outHotspotX), + hotspotY: Double(outHotspotY), + width: Double(outW), + height: Double(outH) + ) +} + +let cursorMap: [String: NSCursor] = [ + "arrow": .arrow, + "text": .iBeam, + "pointer": .pointingHand, + "crosshair": .crosshair, + "open-hand": .openHand, + "closed-hand": .closedHand, + "resize-ew": .resizeLeftRight, + "resize-ns": .resizeUpDown, + "not-allowed": .operationNotAllowed, +] + +var assets: [String: CursorAsset] = [:] +for (key, cursor) in cursorMap { + if let asset = makeAsset(for: cursor) { + assets[key] = asset + } +} + +let encoder = JSONEncoder() +encoder.outputFormatting = [.sortedKeys] +let data = try encoder.encode(assets) +FileHandle.standardOutput.write(data) + diff --git a/electron/native/bin/darwin-arm64/openscreen-window-list b/electron/native/bin/darwin-arm64/openscreen-window-list new file mode 100755 index 000000000..4cf4957bc Binary files /dev/null and b/electron/native/bin/darwin-arm64/openscreen-window-list differ diff --git a/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor b/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor new file mode 100755 index 000000000..99f3555a8 Binary files /dev/null and b/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper new file mode 100755 index 000000000..b34ba7c36 Binary files /dev/null and b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-arm64/recordly-system-cursors b/electron/native/bin/darwin-arm64/recordly-system-cursors new file mode 100755 index 000000000..c3cfed074 Binary files /dev/null and b/electron/native/bin/darwin-arm64/recordly-system-cursors differ diff --git a/electron/native/bin/darwin-arm64/recordly-window-list b/electron/native/bin/darwin-arm64/recordly-window-list new file mode 100755 index 000000000..a07795920 Binary files /dev/null and b/electron/native/bin/darwin-arm64/recordly-window-list differ diff --git a/electron/native/bin/darwin-arm64/whisper-bench b/electron/native/bin/darwin-arm64/whisper-bench new file mode 100755 index 000000000..5d33dbf00 Binary files /dev/null and b/electron/native/bin/darwin-arm64/whisper-bench differ diff --git a/electron/native/bin/darwin-arm64/whisper-cli b/electron/native/bin/darwin-arm64/whisper-cli new file mode 100755 index 000000000..695c8c9a4 Binary files /dev/null and b/electron/native/bin/darwin-arm64/whisper-cli differ diff --git a/electron/native/bin/darwin-arm64/whisper-quantize b/electron/native/bin/darwin-arm64/whisper-quantize new file mode 100755 index 000000000..d51722d9b Binary files /dev/null and b/electron/native/bin/darwin-arm64/whisper-quantize differ diff --git a/electron/native/bin/darwin-arm64/whisper-runtime.json b/electron/native/bin/darwin-arm64/whisper-runtime.json new file mode 100644 index 000000000..380d1adf6 --- /dev/null +++ b/electron/native/bin/darwin-arm64/whisper-runtime.json @@ -0,0 +1,6 @@ +{ + "version": "v1.8.4", + "platform": "darwin", + "arch": "arm64", + "binary": "whisper-cli" +} diff --git a/electron/native/bin/darwin-arm64/whisper-server b/electron/native/bin/darwin-arm64/whisper-server new file mode 100755 index 000000000..790c1fe82 Binary files /dev/null and b/electron/native/bin/darwin-arm64/whisper-server differ diff --git a/electron/native/bin/darwin-arm64/whisper-vad-speech-segments b/electron/native/bin/darwin-arm64/whisper-vad-speech-segments new file mode 100755 index 000000000..65e963b39 Binary files /dev/null and b/electron/native/bin/darwin-arm64/whisper-vad-speech-segments differ diff --git a/electron/native/bin/darwin-x64/recordly-native-cursor-monitor b/electron/native/bin/darwin-x64/recordly-native-cursor-monitor new file mode 100755 index 000000000..b7c380f48 Binary files /dev/null and b/electron/native/bin/darwin-x64/recordly-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-x64/recordly-screencapturekit-helper b/electron/native/bin/darwin-x64/recordly-screencapturekit-helper new file mode 100755 index 000000000..17e171efe Binary files /dev/null and b/electron/native/bin/darwin-x64/recordly-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-x64/recordly-system-cursors b/electron/native/bin/darwin-x64/recordly-system-cursors new file mode 100755 index 000000000..a89e6df44 Binary files /dev/null and b/electron/native/bin/darwin-x64/recordly-system-cursors differ diff --git a/electron/native/bin/darwin-x64/recordly-window-list b/electron/native/bin/darwin-x64/recordly-window-list new file mode 100755 index 000000000..c2db47eb9 Binary files /dev/null and b/electron/native/bin/darwin-x64/recordly-window-list differ diff --git a/electron/native/bin/win32-x64/cursor-monitor.exe b/electron/native/bin/win32-x64/cursor-monitor.exe new file mode 100644 index 000000000..70738d118 Binary files /dev/null and b/electron/native/bin/win32-x64/cursor-monitor.exe differ diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json new file mode 100644 index 000000000..0172a0c30 --- /dev/null +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "platform": "win32", + "arch": "x64", + "helpers": { + "wgc-capture": { + "binaryName": "wgc-capture.exe", + "binarySha256": "a5a9c0c417144a834af3206b60bae32d0c21d55deb8a3f5446831621cfdfa7b3", + "sourceDir": "electron/native/wgc-capture", + "sourceFingerprint": "c65b6eb2230be7db9b49aac48eba52a9eac469028ebb242e10c16c51c86b3220", + "updatedAt": "2026-09-05T02:09:18.163Z" + }, + "cursor-monitor": { + "binaryName": "cursor-monitor.exe", + "binarySha256": "f1d8f30e8d7bee19ecea91c9a90a95ea4824138b8336b18030fa0602a641d70d", + "sourceDir": "electron/native/cursor-monitor", + "sourceFingerprint": "45bb72e4039e061a354af87317d7c42ec3d2118369b3f4379046ff497b701e72", + "updatedAt": "2026-07-11T11:58:56.534Z" + }, + "recordly-gpu-export": { + "binaryName": "recordly-gpu-export.exe", + "binarySha256": "49a2ac588206305d0129e6263ce4be49780c50a9dc4efc7df63aad09178a919f", + "sourceDir": "electron/native/gpu-export-probe", + "sourceFingerprint": "37a5842eba63cdeccfddd02cc278a98207248fb0aa6b56153fb85ed152c908c1", + "updatedAt": "2026-07-11T11:58:51.659Z" + }, + "recordly-nvidia-cuda-compositor": { + "binaryName": "recordly-nvidia-cuda-compositor.exe", + "binarySha256": "250a3f8cac7c6ea38a873434d23d4b2be7d6555e42cc0b405aa26f774169159c", + "sourceDir": "electron/native/nvidia-cuda-compositor", + "sourceFingerprint": "de1219228ce326e96d1f4815a3763b10d5f235cc1286bc6542c99707a85d5947", + "updatedAt": "2026-05-27T11:29:32.957Z" + } + } +} diff --git a/electron/native/bin/win32-x64/recordly-gpu-export.exe b/electron/native/bin/win32-x64/recordly-gpu-export.exe new file mode 100644 index 000000000..b9d1efc9e Binary files /dev/null and b/electron/native/bin/win32-x64/recordly-gpu-export.exe differ diff --git a/electron/native/bin/win32-x64/recordly-nvidia-cuda-compositor.exe b/electron/native/bin/win32-x64/recordly-nvidia-cuda-compositor.exe new file mode 100644 index 000000000..c23c14f90 Binary files /dev/null and b/electron/native/bin/win32-x64/recordly-nvidia-cuda-compositor.exe differ diff --git a/electron/native/bin/win32-x64/wgc-capture.exe b/electron/native/bin/win32-x64/wgc-capture.exe new file mode 100644 index 000000000..efa45c37d Binary files /dev/null and b/electron/native/bin/win32-x64/wgc-capture.exe differ diff --git a/electron/native/common/bt709_video.h b/electron/native/common/bt709_video.h new file mode 100644 index 000000000..f4758dcce --- /dev/null +++ b/electron/native/common/bt709_video.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +inline int clampVideoSample(int value, int minimum, int maximum) { + return value < minimum ? minimum : (value > maximum ? maximum : value); +} + +inline HRESULT setBt709LimitedVideoAttributes(IMFMediaType* mediaType) { + HRESULT result = mediaType->SetUINT32(MF_MT_VIDEO_PRIMARIES, MFVideoPrimaries_BT709); + if (FAILED(result)) return result; + result = mediaType->SetUINT32(MF_MT_TRANSFER_FUNCTION, MFVideoTransFunc_709); + if (FAILED(result)) return result; + result = mediaType->SetUINT32(MF_MT_YUV_MATRIX, MFVideoTransferMatrix_BT709); + if (FAILED(result)) return result; + return mediaType->SetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, MFNominalRange_16_235); +} + +inline void convertBgraToBt709LimitedNv12( + const uint8_t* bgra, + int bgraPitch, + int width, + int height, + std::vector& nv12Buffer) { + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + const uint8_t* pixel = bgra + y * bgraPitch + x * 4; + const int blue = pixel[0]; + const int green = pixel[1]; + const int red = pixel[2]; + const int luma = ((47 * red + 157 * green + 16 * blue + 128) >> 8) + 16; + nv12Buffer[y * width + x] = static_cast(clampVideoSample(luma, 16, 235)); + } + } + + uint8_t* uvPlane = nv12Buffer.data() + width * height; + for (int y = 0; y < height; y += 2) { + for (int x = 0; x < width; x += 2) { + int red = 0; + int green = 0; + int blue = 0; + for (int offsetY = 0; offsetY < 2; ++offsetY) { + for (int offsetX = 0; offsetX < 2; ++offsetX) { + const uint8_t* pixel = + bgra + (y + offsetY) * bgraPitch + (x + offsetX) * 4; + blue += pixel[0]; + green += pixel[1]; + red += pixel[2]; + } + } + red = (red + 2) / 4; + green = (green + 2) / 4; + blue = (blue + 2) / 4; + + // Coefficients sum to zero so neutral greys remain neutral after quantization. + const int chromaBlue = ((-26 * red - 87 * green + 113 * blue + 128) >> 8) + 128; + const int chromaRed = ((112 * red - 102 * green - 10 * blue + 128) >> 8) + 128; + const int uvIndex = (y / 2) * width + x; + uvPlane[uvIndex] = static_cast(clampVideoSample(chromaBlue, 16, 240)); + uvPlane[uvIndex + 1] = static_cast(clampVideoSample(chromaRed, 16, 240)); + } + } +} diff --git a/electron/native/cursor-monitor/CMakeLists.txt b/electron/native/cursor-monitor/CMakeLists.txt new file mode 100644 index 000000000..8155aed95 --- /dev/null +++ b/electron/native/cursor-monitor/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(cursor-monitor LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(cursor-monitor src/main.cpp) + +target_link_libraries(cursor-monitor PRIVATE user32) diff --git a/electron/native/cursor-monitor/src/main.cpp b/electron/native/cursor-monitor/src/main.cpp new file mode 100644 index 000000000..9a811f7be --- /dev/null +++ b/electron/native/cursor-monitor/src/main.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_running{true}; + +static void stdinListener() { + std::string line; + while (std::getline(std::cin, line)) { + if (line == "stop") { + g_running.store(false); + return; + } + } + g_running.store(false); +} + +int main() { + std::setvbuf(stdout, nullptr, _IONBF, 0); + + std::unordered_map cursorMap; + cursorMap[LoadCursor(NULL, IDC_ARROW)] = "arrow"; + cursorMap[LoadCursor(NULL, IDC_IBEAM)] = "text"; + cursorMap[LoadCursor(NULL, IDC_HAND)] = "pointer"; + cursorMap[LoadCursor(NULL, IDC_CROSS)] = "crosshair"; + cursorMap[LoadCursor(NULL, IDC_NO)] = "not-allowed"; + cursorMap[LoadCursor(NULL, IDC_SIZEWE)] = "resize-ew"; + cursorMap[LoadCursor(NULL, IDC_SIZENS)] = "resize-ns"; + cursorMap[LoadCursor(NULL, IDC_SIZEALL)] = "open-hand"; + cursorMap[LoadCursor(NULL, IDC_WAIT)] = "arrow"; + cursorMap[LoadCursor(NULL, IDC_APPSTARTING)] = "arrow"; + + std::thread listener(stdinListener); + listener.detach(); + + std::string lastType; + + while (g_running.load()) { + CURSORINFO ci = {}; + ci.cbSize = sizeof(ci); + + if (GetCursorInfo(&ci) && (ci.flags & CURSOR_SHOWING)) { + auto it = cursorMap.find(ci.hCursor); + std::string type = (it != cursorMap.end()) ? it->second : "arrow"; + + if (type != lastType) { + lastType = type; + std::cout << "STATE:" << type << std::endl; + } + } + + Sleep(50); + } + + return 0; +} diff --git a/electron/native/gpu-export-probe/CMakeLists.txt b/electron/native/gpu-export-probe/CMakeLists.txt new file mode 100644 index 000000000..31a718a0c --- /dev/null +++ b/electron/native/gpu-export-probe/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.20) +project(gpu-export-probe LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(gpu-export-probe + src/main.cpp +) + +set(NVIDIA_VIDEO_SDK_SAMPLES_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../.tmp/video-sdk-samples/Samples") +set(NVIDIA_NVENC_SDK_DIR "${NVIDIA_VIDEO_SDK_SAMPLES_DIR}/NvCodec") +if(EXISTS "${NVIDIA_NVENC_SDK_DIR}/NvEncoder/NvEncoderD3D11.cpp") + target_sources(gpu-export-probe PRIVATE + "${NVIDIA_NVENC_SDK_DIR}/NvEncoder/NvEncoder.cpp" + "${NVIDIA_NVENC_SDK_DIR}/NvEncoder/NvEncoderD3D11.cpp" + ) + target_include_directories(gpu-export-probe PRIVATE + "${NVIDIA_VIDEO_SDK_SAMPLES_DIR}" + "${NVIDIA_NVENC_SDK_DIR}" + ) + target_compile_definitions(gpu-export-probe PRIVATE RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK=1) +endif() + +target_compile_options(gpu-export-probe PRIVATE /EHsc /W4 /utf-8) + +target_link_libraries(gpu-export-probe PRIVATE + d3d11 + d3dcompiler + dxgi + mfplat + mfreadwrite + mf + mfuuid + ole32 + windowscodecs +) diff --git a/electron/native/gpu-export-probe/src/main.cpp b/electron/native/gpu-export-probe/src/main.cpp new file mode 100644 index 000000000..158f103f7 --- /dev/null +++ b/electron/native/gpu-export-probe/src/main.cpp @@ -0,0 +1,3390 @@ +#define NOMINMAX +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK +#include "NvEncoder/NvEncoderD3D11.h" +#endif + +#pragma comment(lib, "d3d11.lib") +#pragma comment(lib, "d3dcompiler.lib") +#pragma comment(lib, "dxgi.lib") +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfreadwrite.lib") +#pragma comment(lib, "mf.lib") +#pragma comment(lib, "mfuuid.lib") +#pragma comment(lib, "windowscodecs.lib") + +using Microsoft::WRL::ComPtr; + +namespace { + +struct Options { + std::wstring inputPath; + std::wstring outputPath = L"gpu-export-probe.mp4"; + UINT width = 1920; + UINT height = 1080; + UINT fps = 30; + double seconds = 60.0; + UINT bitrate = 12'000'000; + bool shaderComposite = false; + float radius = 32.0f; + float shadow = 36.0f; + float padding = 0.0f; + LONG contentLeft = -1; + LONG contentTop = -1; + LONG contentWidth = 0; + LONG contentHeight = 0; + LONG sourceCropLeft = 0; + LONG sourceCropTop = 0; + LONG sourceCropWidth = 0; + LONG sourceCropHeight = 0; + float backgroundR = 0.035f; + float backgroundG = 0.035f; + float backgroundB = 0.045f; + float backgroundBlurPx = 0.0f; + std::wstring backgroundImagePath; + std::wstring webcamInputPath; + LONG webcamLeft = -1; + LONG webcamTop = -1; + LONG webcamSize = 0; + float webcamRadius = 18.0f; + float webcamShadow = 0.0f; + bool webcamMirror = false; + double webcamTimeOffsetMs = 0.0; + std::wstring cursorTelemetryPath; + std::wstring cursorAtlasPath; + std::wstring cursorAtlasMetadataPath; + float cursorSize = 84.0f; + std::wstring zoomTelemetryPath; + std::wstring timelineMapPath; + bool preferHighPerformanceAdapter = false; + int adapterIndex = -1; + bool fastEncoderTuning = false; + UINT surfacePoolSize = 4; + bool nvencSdk = false; +}; + +struct ShaderConstants { + float outputWidth; + float outputHeight; + float radius; + float shadowSize; + float contentLeft; + float contentTop; + float contentRight; + float contentBottom; + float backgroundR; + float backgroundG; + float backgroundB; + float backgroundA; + float shadowR; + float shadowG; + float shadowB; + float shadowA; + float backgroundImageEnabled; + float backgroundImageWidth; + float backgroundImageHeight; + float webcamEnabled; + float webcamLeft; + float webcamTop; + float webcamRight; + float webcamBottom; + float webcamRadius; + float webcamShadowSize; + float webcamShadowA; + float webcamMirror; + float cursorEnabled; + float cursorX; + float cursorY; + float cursorSize; + float cursorAtlasEnabled; + float cursorAtlasLeft; + float cursorAtlasTop; + float cursorAtlasRight; + float cursorAtlasBottom; + float cursorAtlasAnchorX; + float cursorAtlasAnchorY; + float cursorAtlasAspect; + float cursorBounceScale; + float backgroundBlurPx; + float backgroundBlurPadding0; + float backgroundBlurPadding1; + float zoomEnabled; + float zoomScale; + float zoomX; + float zoomY; +}; + +struct CursorSample { + double timeMs = 0.0; + float cx = 0.0f; + float cy = 0.0f; + int cursorTypeIndex = 0; + float bounceScale = 1.0f; + bool visible = true; +}; + +struct CursorAtlasEntry { + float x = 0.0f; + float y = 0.0f; + float width = 1.0f; + float height = 1.0f; + float anchorX = 0.0f; + float anchorY = 0.0f; + float aspectRatio = 1.0f; + bool valid = false; +}; + +struct ZoomSample { + double timeMs = 0.0; + float scale = 1.0f; + float x = 0.0f; + float y = 0.0f; +}; + +struct TimelineSegment { + double sourceStartMs = 0.0; + double sourceEndMs = 0.0; + double outputStartMs = 0.0; + double outputEndMs = 0.0; + double speed = 1.0; +}; + +struct Timer { + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); + + double elapsedMs() const { + const auto now = std::chrono::steady_clock::now(); + return std::chrono::duration(now - start).count(); + } +}; + +std::wstring getArgValue(const std::vector& args, const std::wstring& key) { + for (size_t i = 0; i + 1 < args.size(); ++i) { + if (args[i] == key) { + return args[i + 1]; + } + } + return L""; +} + +UINT parseUIntArg(const std::vector& args, const std::wstring& key, UINT fallback) { + const auto value = getArgValue(args, key); + if (value.empty()) { + return fallback; + } + + try { + return static_cast(std::stoul(value)); + } catch (...) { + return fallback; + } +} + +LONG parseLongArg(const std::vector& args, const std::wstring& key, LONG fallback) { + const auto value = getArgValue(args, key); + if (value.empty()) { + return fallback; + } + + try { + return static_cast(std::stol(value)); + } catch (...) { + return fallback; + } +} + +float parseFloatArg(const std::vector& args, const std::wstring& key, float fallback) { + const auto value = getArgValue(args, key); + if (value.empty()) { + return fallback; + } + + try { + return std::stof(value); + } catch (...) { + return fallback; + } +} + +double parseDoubleArg(const std::vector& args, const std::wstring& key, double fallback) { + const auto value = getArgValue(args, key); + if (value.empty()) { + return fallback; + } + + try { + return std::stod(value); + } catch (...) { + return fallback; + } +} + +bool hasArg(const std::vector& args, const std::wstring& key) { + return std::find(args.begin(), args.end(), key) != args.end(); +} + +bool parseHexColor(const std::wstring& value, float& red, float& green, float& blue) { + std::wstring trimmed = value; + trimmed.erase( + std::remove_if(trimmed.begin(), trimmed.end(), [](wchar_t ch) { + return ch == L' ' || ch == L'\t' || ch == L'\r' || ch == L'\n'; + }), + trimmed.end()); + if (!trimmed.empty() && trimmed[0] == L'#') { + trimmed.erase(trimmed.begin()); + } + if (trimmed.size() != 6) { + return false; + } + + try { + const auto number = std::stoul(trimmed, nullptr, 16); + red = static_cast((number >> 16) & 0xff) / 255.0f; + green = static_cast((number >> 8) & 0xff) / 255.0f; + blue = static_cast(number & 0xff) / 255.0f; + return true; + } catch (...) { + return false; + } +} + +std::vector loadTimelineMap(const std::wstring& path) { + std::vector segments; + if (path.empty()) { + return segments; + } + + FILE* file = nullptr; + if (_wfopen_s(&file, path.c_str(), L"rb") != 0 || !file) { + std::cerr << "[gpu-export] Unable to open timeline map" << std::endl; + return segments; + } + + char line[512] = {}; + double expectedOutputStartMs = 0.0; + while (fgets(line, sizeof(line), file)) { + TimelineSegment segment; + if (sscanf_s( + line, + "%lf,%lf,%lf,%lf,%lf", + &segment.sourceStartMs, + &segment.sourceEndMs, + &segment.outputStartMs, + &segment.outputEndMs, + &segment.speed) != 5) { + segments.clear(); + break; + } + if ( + !std::isfinite(segment.sourceStartMs) || + !std::isfinite(segment.sourceEndMs) || + !std::isfinite(segment.outputStartMs) || + !std::isfinite(segment.outputEndMs) || + !std::isfinite(segment.speed) || + segment.sourceEndMs <= segment.sourceStartMs || + segment.outputEndMs <= segment.outputStartMs || + segment.speed <= 0.0 || + std::abs(segment.outputStartMs - expectedOutputStartMs) > 2.0) { + segments.clear(); + break; + } + expectedOutputStartMs = segment.outputEndMs; + segments.push_back(segment); + } + fclose(file); + return segments; +} + +bool sourceToOutputMs( + const std::vector& segments, + double sourceMs, + double& outputMs) { + if (segments.empty()) { + outputMs = sourceMs; + return true; + } + + constexpr double kToleranceMs = 1.0; + for (const auto& segment : segments) { + if (sourceMs + kToleranceMs < segment.sourceStartMs) { + return false; + } + if (sourceMs <= segment.sourceEndMs + kToleranceMs) { + const double clampedSourceMs = + std::min(segment.sourceEndMs, std::max(segment.sourceStartMs, sourceMs)); + outputMs = segment.outputStartMs + (clampedSourceMs - segment.sourceStartMs) / segment.speed; + outputMs = std::min(segment.outputEndMs, std::max(segment.outputStartMs, outputMs)); + return true; + } + } + + return false; +} + +double outputToSourceMs(const std::vector& segments, double outputMs) { + if (segments.empty()) { + return outputMs; + } + + for (const auto& segment : segments) { + if (outputMs <= segment.outputEndMs + 1.0) { + const double clampedOutputMs = + std::min(segment.outputEndMs, std::max(segment.outputStartMs, outputMs)); + return segment.sourceStartMs + (clampedOutputMs - segment.outputStartMs) * segment.speed; + } + } + + return segments.back().sourceEndMs; +} + +Options parseOptions(int argc, wchar_t** argv) { + std::vector args; + args.reserve(static_cast(argc)); + for (int i = 0; i < argc; ++i) { + args.emplace_back(argv[i]); + } + + Options options; + const auto input = getArgValue(args, L"--input"); + if (!input.empty()) { + options.inputPath = input; + } + const auto output = getArgValue(args, L"--output"); + if (!output.empty()) { + options.outputPath = output; + } + options.width = parseUIntArg(args, L"--width", options.width); + options.height = parseUIntArg(args, L"--height", options.height); + options.fps = parseUIntArg(args, L"--fps", options.fps); + options.seconds = parseDoubleArg(args, L"--seconds", options.seconds); + options.bitrate = parseUIntArg(args, L"--bitrate", options.bitrate); + options.shaderComposite = hasArg(args, L"--shader-composite"); + options.radius = parseFloatArg(args, L"--radius", options.radius); + options.shadow = parseFloatArg(args, L"--shadow", options.shadow); + options.padding = parseFloatArg(args, L"--padding", options.padding); + options.backgroundBlurPx = std::max( + 0.0f, + parseFloatArg(args, L"--background-blur", options.backgroundBlurPx)); + options.contentLeft = parseLongArg(args, L"--content-left", options.contentLeft); + options.contentTop = parseLongArg(args, L"--content-top", options.contentTop); + options.contentWidth = parseLongArg(args, L"--content-width", options.contentWidth); + options.contentHeight = parseLongArg(args, L"--content-height", options.contentHeight); + options.sourceCropLeft = parseLongArg(args, L"--source-crop-x", options.sourceCropLeft); + options.sourceCropTop = parseLongArg(args, L"--source-crop-y", options.sourceCropTop); + options.sourceCropWidth = parseLongArg(args, L"--source-crop-width", options.sourceCropWidth); + options.sourceCropHeight = parseLongArg(args, L"--source-crop-height", options.sourceCropHeight); + const auto backgroundColor = getArgValue(args, L"--background-color"); + if (!backgroundColor.empty()) { + parseHexColor( + backgroundColor, + options.backgroundR, + options.backgroundG, + options.backgroundB); + } + const auto backgroundImage = getArgValue(args, L"--background-image"); + if (!backgroundImage.empty()) { + options.backgroundImagePath = backgroundImage; + } + const auto webcamInput = getArgValue(args, L"--webcam-input"); + if (!webcamInput.empty()) { + options.webcamInputPath = webcamInput; + } + options.webcamLeft = parseLongArg(args, L"--webcam-left", options.webcamLeft); + options.webcamTop = parseLongArg(args, L"--webcam-top", options.webcamTop); + options.webcamSize = parseLongArg(args, L"--webcam-size", options.webcamSize); + options.webcamRadius = parseFloatArg(args, L"--webcam-radius", options.webcamRadius); + options.webcamShadow = parseFloatArg(args, L"--webcam-shadow", options.webcamShadow); + options.webcamMirror = hasArg(args, L"--webcam-mirror"); + options.webcamTimeOffsetMs = parseDoubleArg( + args, + L"--webcam-time-offset-ms", + options.webcamTimeOffsetMs); + const auto cursorTelemetry = getArgValue(args, L"--cursor-telemetry"); + if (!cursorTelemetry.empty()) { + options.cursorTelemetryPath = cursorTelemetry; + } + const auto cursorAtlas = getArgValue(args, L"--cursor-atlas"); + if (!cursorAtlas.empty()) { + options.cursorAtlasPath = cursorAtlas; + } + const auto cursorAtlasMetadata = getArgValue(args, L"--cursor-atlas-metadata"); + if (!cursorAtlasMetadata.empty()) { + options.cursorAtlasMetadataPath = cursorAtlasMetadata; + } + options.cursorSize = parseFloatArg(args, L"--cursor-size", options.cursorSize); + const auto zoomTelemetry = getArgValue(args, L"--zoom-telemetry"); + if (!zoomTelemetry.empty()) { + options.zoomTelemetryPath = zoomTelemetry; + } + const auto timelineMap = getArgValue(args, L"--timeline-map"); + if (!timelineMap.empty()) { + options.timelineMapPath = timelineMap; + } + options.preferHighPerformanceAdapter = hasArg(args, L"--prefer-high-performance-adapter"); + options.adapterIndex = static_cast(parseLongArg(args, L"--adapter-index", options.adapterIndex)); + options.fastEncoderTuning = hasArg(args, L"--fast-encoder-tuning"); + options.nvencSdk = hasArg(args, L"--nvenc-sdk"); + options.surfacePoolSize = parseUIntArg(args, L"--surface-pool-size", options.surfacePoolSize); + options.width = std::max(2, options.width & ~1U); + options.height = std::max(2, options.height & ~1U); + options.fps = std::max(1, options.fps); + options.seconds = std::max(0.001, options.seconds); + options.surfacePoolSize = std::min(32, std::max(4, options.surfacePoolSize)); + options.radius = std::max(0.0f, options.radius); + options.shadow = std::max(0.0f, options.shadow); + options.webcamSize = std::max(0, options.webcamSize & ~1L); + options.webcamRadius = std::max(0.0f, options.webcamRadius); + options.webcamShadow = std::max(0.0f, options.webcamShadow); + options.cursorSize = std::max(0.0f, options.cursorSize); + if (options.padding > 1.0f) { + options.padding /= 100.0f; + } + options.padding = std::min(0.45f, std::max(0.0f, options.padding)); + return options; +} + +std::string hrToHex(HRESULT hr) { + std::ostringstream stream; + stream << "0x" << std::hex << static_cast(hr); + return stream.str(); +} + +bool succeeded(HRESULT hr, const char* label) { + if (SUCCEEDED(hr)) { + return true; + } + std::cerr << "ERROR: " << label << " failed: " << hrToHex(hr) << std::endl; + return false; +} + +DWORD firstVideoStreamIndex() { + return static_cast(MF_SOURCE_READER_FIRST_VIDEO_STREAM); +} + +class GpuProbe { +public: + bool initialize(const Options& options) { + const Timer initializeTimer; + options_ = options; + loadCursorTelemetry(); + loadZoomTelemetry(); + timelineSegments_ = loadTimelineMap(options_.timelineMapPath); + if (!options_.timelineMapPath.empty() && timelineSegments_.empty()) { + std::cerr << "[gpu-export] Timeline map is invalid or empty" << std::endl; + return false; + } + + { + const Timer timer; + const HRESULT coInit = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + coInitialized_ = SUCCEEDED(coInit); + initCoInitializeMs_ = timer.elapsedMs(); + if (!coInitialized_ && coInit != RPC_E_CHANGED_MODE) { + return succeeded(coInit, "CoInitializeEx"); + } + } + + { + const Timer timer; + const HRESULT hr = MFStartup(MF_VERSION); + initMfStartupMs_ = timer.elapsedMs(); + if (!succeeded(hr, "MFStartup")) { + return false; + } + mfStarted_ = true; + } + + { + const Timer timer; + if (!createD3DDevice()) { + return false; + } + initD3DDeviceMs_ = timer.elapsedMs(); + } + + initSourceReaderMs_ = 0.0; + if (!options_.inputPath.empty()) { + const Timer timer; + if (!createSourceReader()) { + return false; + } + initSourceReaderMs_ = timer.elapsedMs(); + } + + initWebcamReaderMs_ = 0.0; + if (hasWebcamOverlay()) { + const Timer timer; + if (!createWebcamSourceReader()) { + return false; + } + initWebcamReaderMs_ = timer.elapsedMs(); + } + + { + const Timer timer; + if (!createVideoProcessor()) { + return false; + } + initVideoProcessorMs_ = timer.elapsedMs(); + } + + { + const Timer timer; + if (!createTextures()) { + return false; + } + initTexturesMs_ = timer.elapsedMs(); + } + + initShaderPipelineMs_ = 0.0; + if (options_.shaderComposite) { + const Timer timer; + if (!createShaderPipeline()) { + return false; + } + initShaderPipelineMs_ = timer.elapsedMs(); + } + + { + const Timer timer; + if (options_.nvencSdk ? !createNvencSdkEncoder() : !createSinkWriter()) { + return false; + } + initSinkWriterMs_ = timer.elapsedMs(); + } + + initializeMs_ = initializeTimer.elapsedMs(); + return true; + } + + bool run() { + if (!options_.inputPath.empty()) { + return runSourceVideo(); + } + + const UINT frameCount = + static_cast(std::ceil(static_cast(options_.fps) * options_.seconds)); + const Timer totalTimer; + double clearMs = 0; + double processMs = 0; + double writeMs = 0; + + for (UINT frameIndex = 0; frameIndex < frameCount; ++frameIndex) { + const Timer clearTimer; + renderSyntheticFrame(frameIndex); + clearMs += clearTimer.elapsedMs(); + + const Timer processTimer; + if (!convertBgraToNv12(frameIndex)) { + return false; + } + processMs += processTimer.elapsedMs(); + + const Timer writeTimer; + if (!writeFrame(frameIndex)) { + return false; + } + writeMs += writeTimer.elapsedMs(); + emitProgress(frameIndex + 1, frameCount); + } + emitProgress(frameCount, frameCount, true); + + const Timer finalizeTimer; + const bool finalized = options_.nvencSdk ? finalizeNvencSdk() : SUCCEEDED(sinkWriter_->Finalize()); + const double finalizeMs = finalizeTimer.elapsedMs(); + if (!finalized) { + if (!options_.nvencSdk) { + std::cerr << "ERROR: IMFSinkWriter::Finalize failed" << std::endl; + } + return false; + } + + const double totalMs = totalTimer.elapsedMs(); + const double realtime = (options_.seconds * 1000.0) / totalMs; + std::cout + << "{" + << "\"success\":true," + << "\"width\":" << options_.width << "," + << "\"height\":" << options_.height << "," + << "\"fps\":" << options_.fps << "," + << "\"surfacePoolSize\":" << options_.surfacePoolSize << "," + << "\"adapterIndex\":" << selectedAdapterIndex_ << "," + << "\"adapterVendorId\":" << selectedAdapterVendorId_ << "," + << "\"adapterDeviceId\":" << selectedAdapterDeviceId_ << "," + << "\"adapterDedicatedVideoMemoryMB\":" << selectedAdapterDedicatedVideoMemoryMB_ << "," + << "\"seconds\":" << options_.seconds << "," + << "\"frames\":" << frameCount << "," + << "\"initializeMs\":" << initializeMs_ << "," + << "\"initCoInitializeMs\":" << initCoInitializeMs_ << "," + << "\"initMfStartupMs\":" << initMfStartupMs_ << "," + << "\"initD3DDeviceMs\":" << initD3DDeviceMs_ << "," + << "\"initSourceReaderMs\":" << initSourceReaderMs_ << "," + << "\"initWebcamReaderMs\":" << initWebcamReaderMs_ << "," + << "\"initVideoProcessorMs\":" << initVideoProcessorMs_ << "," + << "\"initTexturesMs\":" << initTexturesMs_ << "," + << "\"initShaderPipelineMs\":" << initShaderPipelineMs_ << "," + << "\"initSinkWriterMs\":" << initSinkWriterMs_ << "," + << "\"encoderBackend\":\"" << (options_.nvencSdk ? "nvenc-sdk-d3d11" : "media-foundation") << "\"," + << "\"encoderTuningApplied\":" << (encoderTuningApplied_ ? "true" : "false") << "," + << "\"nvencOutputBytes\":" << nvencOutputBytes_ << "," + << "\"totalMs\":" << totalMs << "," + << "\"clearMs\":" << clearMs << "," + << "\"videoProcessMs\":" << processMs << "," + << "\"writeSampleMs\":" << writeMs << "," + << "\"finalizeMs\":" << finalizeMs << "," + << "\"realtimeMultiplier\":" << realtime + << "}" << std::endl; + return true; + } + + void emitProgress(UINT currentFrame, UINT totalFrames, bool force = false) { + if (totalFrames == 0) { + return; + } + + const UINT cadence = std::max(1, static_cast(options_.fps)); + if (!force && currentFrame < totalFrames && (currentFrame % cadence) != 0) { + return; + } + + const double percentage = + std::min(100.0, (static_cast(currentFrame) / totalFrames) * 100.0); + std::cerr + << "PROGRESS {" + << "\"currentFrame\":" << currentFrame << "," + << "\"totalFrames\":" << totalFrames << "," + << "\"percentage\":" << percentage + << "}" << std::endl; + } + + ~GpuProbe() { + if (nvencOutputFile_) { + std::fclose(nvencOutputFile_); + nvencOutputFile_ = nullptr; + } +#ifdef RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK + nvencEncoder_.reset(); +#endif + sinkWriter_.Reset(); + sourceReader_.Reset(); + webcamReader_.Reset(); + pendingWebcamSample_.Reset(); + nv12Textures_.clear(); + nv12OutputViews_.clear(); + bgraNv12OutputViews_.clear(); + compositorConstants_.Reset(); + samplerState_.Reset(); + pixelShader_.Reset(); + vertexShader_.Reset(); + backgroundShaderResourceView_.Reset(); + webcamShaderResourceView_.Reset(); + webcamOutputView_.Reset(); + webcamTexture_.Reset(); + contentShaderResourceView_.Reset(); + contentOutputView_.Reset(); + contentTexture_.Reset(); + bgraInputView_.Reset(); + bgraRenderTargetView_.Reset(); + bgraTexture_.Reset(); + bgraVideoProcessor_.Reset(); + bgraVideoProcessorEnumerator_.Reset(); + webcamVideoProcessor_.Reset(); + webcamVideoProcessorEnumerator_.Reset(); + videoProcessor_.Reset(); + videoProcessorEnumerator_.Reset(); + videoContext_.Reset(); + videoDevice_.Reset(); + deviceContext_.Reset(); + device_.Reset(); + deviceManager_.Reset(); + if (mfStarted_) { + MFShutdown(); + } + if (coInitialized_) { + CoUninitialize(); + } + } + +private: + void captureSelectedAdapterInfo(IDXGIAdapter1* adapter) { + if (!adapter) { + return; + } + + DXGI_ADAPTER_DESC1 desc = {}; + if (FAILED(adapter->GetDesc1(&desc))) { + return; + } + selectedAdapterVendorId_ = desc.VendorId; + selectedAdapterDeviceId_ = desc.DeviceId; + selectedAdapterDedicatedVideoMemoryMB_ = + static_cast(desc.DedicatedVideoMemory / (1024 * 1024)); + } + + ComPtr selectHighPerformanceAdapter() { + ComPtr factory; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); + if (FAILED(hr)) { + return nullptr; + } + + ComPtr bestAdapter; + SIZE_T bestDedicatedMemory = 0; + for (UINT index = 0;; ++index) { + ComPtr adapter; + hr = factory->EnumAdapters1(index, &adapter); + if (hr == DXGI_ERROR_NOT_FOUND) { + break; + } + if (FAILED(hr)) { + continue; + } + + DXGI_ADAPTER_DESC1 desc = {}; + if (FAILED(adapter->GetDesc1(&desc))) { + continue; + } + if ((desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0) { + continue; + } + if (!bestAdapter || desc.DedicatedVideoMemory > bestDedicatedMemory) { + bestDedicatedMemory = desc.DedicatedVideoMemory; + bestAdapter = adapter; + } + } + + return bestAdapter; + } + + ComPtr selectAdapterByIndex(UINT requestedIndex) { + ComPtr factory; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); + if (FAILED(hr)) { + return nullptr; + } + + ComPtr adapter; + hr = factory->EnumAdapters1(requestedIndex, &adapter); + if (FAILED(hr)) { + return nullptr; + } + return adapter; + } + + bool createD3DDevice() { + const D3D_FEATURE_LEVEL levels[] = { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + }; + D3D_FEATURE_LEVEL selectedLevel = D3D_FEATURE_LEVEL_11_0; + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT; + ComPtr preferredAdapter; + if (options_.adapterIndex >= 0) { + preferredAdapter = selectAdapterByIndex(static_cast(options_.adapterIndex)); + if (!preferredAdapter) { + std::cerr << "ERROR: adapter index " << options_.adapterIndex << " was not found" << std::endl; + return false; + } + selectedAdapterIndex_ = options_.adapterIndex; + } else if (options_.preferHighPerformanceAdapter) { + preferredAdapter = selectHighPerformanceAdapter(); + } + + HRESULT hr = D3D11CreateDevice( + preferredAdapter.Get(), + preferredAdapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE, + nullptr, + flags, + levels, + ARRAYSIZE(levels), + D3D11_SDK_VERSION, + &device_, + &selectedLevel, + &deviceContext_); + if (!succeeded(hr, "D3D11CreateDevice")) { + return false; + } + + ComPtr dxgiDevice; + if (SUCCEEDED(device_.As(&dxgiDevice))) { + ComPtr adapter; + if (SUCCEEDED(dxgiDevice->GetAdapter(&adapter))) { + ComPtr adapter1; + if (SUCCEEDED(adapter.As(&adapter1))) { + captureSelectedAdapterInfo(adapter1.Get()); + } + } + } + + hr = device_.As(&videoDevice_); + if (!succeeded(hr, "Query ID3D11VideoDevice")) { + return false; + } + hr = deviceContext_.As(&videoContext_); + if (!succeeded(hr, "Query ID3D11VideoContext")) { + return false; + } + + UINT resetToken = 0; + hr = MFCreateDXGIDeviceManager(&resetToken, &deviceManager_); + if (!succeeded(hr, "MFCreateDXGIDeviceManager")) { + return false; + } + hr = deviceManager_->ResetDevice(device_.Get(), resetToken); + if (!succeeded(hr, "IMFDXGIDeviceManager::ResetDevice")) { + return false; + } + + return true; + } + + bool createVideoProcessor() { + D3D11_VIDEO_PROCESSOR_CONTENT_DESC desc = {}; + desc.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE; + desc.InputFrameRate.Numerator = options_.fps; + desc.InputFrameRate.Denominator = 1; + desc.InputWidth = sourceWidth_; + desc.InputHeight = sourceHeight_; + desc.OutputFrameRate.Numerator = options_.fps; + desc.OutputFrameRate.Denominator = 1; + desc.OutputWidth = options_.width; + desc.OutputHeight = options_.height; + desc.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL; + + HRESULT hr = videoDevice_->CreateVideoProcessorEnumerator(&desc, &videoProcessorEnumerator_); + if (!succeeded(hr, "CreateVideoProcessorEnumerator")) { + return false; + } + hr = videoDevice_->CreateVideoProcessor(videoProcessorEnumerator_.Get(), 0, &videoProcessor_); + if (!succeeded(hr, "CreateVideoProcessor")) { + return false; + } + + RECT rect = getSourceCropRect(); + RECT outputRect = { + 0, + 0, + static_cast(options_.width), + static_cast(options_.height), + }; + RECT contentRect = getContentRect(); + videoContext_->VideoProcessorSetStreamSourceRect(videoProcessor_.Get(), 0, TRUE, &rect); + videoContext_->VideoProcessorSetStreamDestRect(videoProcessor_.Get(), 0, TRUE, &contentRect); + videoContext_->VideoProcessorSetOutputTargetRect(videoProcessor_.Get(), TRUE, &outputRect); + videoContext_->VideoProcessorSetStreamAutoProcessingMode(videoProcessor_.Get(), 0, FALSE); + D3D11_VIDEO_COLOR background = {}; + background.RGBA.A = 1.0f; + background.RGBA.R = 0.04f; + background.RGBA.G = 0.04f; + background.RGBA.B = 0.05f; + videoContext_->VideoProcessorSetOutputBackgroundColor( + videoProcessor_.Get(), + FALSE, + &background); + + D3D11_VIDEO_PROCESSOR_CONTENT_DESC bgraDesc = {}; + bgraDesc.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE; + bgraDesc.InputFrameRate.Numerator = options_.fps; + bgraDesc.InputFrameRate.Denominator = 1; + bgraDesc.InputWidth = options_.width; + bgraDesc.InputHeight = options_.height; + bgraDesc.OutputFrameRate.Numerator = options_.fps; + bgraDesc.OutputFrameRate.Denominator = 1; + bgraDesc.OutputWidth = options_.width; + bgraDesc.OutputHeight = options_.height; + bgraDesc.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL; + + hr = videoDevice_->CreateVideoProcessorEnumerator( + &bgraDesc, + &bgraVideoProcessorEnumerator_); + if (!succeeded(hr, "Create BGRA video processor enumerator")) { + return false; + } + hr = videoDevice_->CreateVideoProcessor( + bgraVideoProcessorEnumerator_.Get(), + 0, + &bgraVideoProcessor_); + if (!succeeded(hr, "Create BGRA video processor")) { + return false; + } + + RECT bgraRect = { + 0, + 0, + static_cast(options_.width), + static_cast(options_.height), + }; + videoContext_->VideoProcessorSetStreamSourceRect( + bgraVideoProcessor_.Get(), + 0, + TRUE, + &bgraRect); + videoContext_->VideoProcessorSetStreamDestRect( + bgraVideoProcessor_.Get(), + 0, + TRUE, + &bgraRect); + videoContext_->VideoProcessorSetOutputTargetRect( + bgraVideoProcessor_.Get(), + TRUE, + &bgraRect); + videoContext_->VideoProcessorSetStreamAutoProcessingMode( + bgraVideoProcessor_.Get(), + 0, + FALSE); + + if (hasWebcamOverlay()) { + D3D11_VIDEO_PROCESSOR_CONTENT_DESC webcamDesc = {}; + webcamDesc.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE; + webcamDesc.InputFrameRate.Numerator = options_.fps; + webcamDesc.InputFrameRate.Denominator = 1; + webcamDesc.InputWidth = webcamWidth_; + webcamDesc.InputHeight = webcamHeight_; + webcamDesc.OutputFrameRate.Numerator = options_.fps; + webcamDesc.OutputFrameRate.Denominator = 1; + webcamDesc.OutputWidth = options_.width; + webcamDesc.OutputHeight = options_.height; + webcamDesc.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL; + + hr = videoDevice_->CreateVideoProcessorEnumerator( + &webcamDesc, + &webcamVideoProcessorEnumerator_); + if (!succeeded(hr, "Create webcam video processor enumerator")) { + return false; + } + hr = videoDevice_->CreateVideoProcessor( + webcamVideoProcessorEnumerator_.Get(), + 0, + &webcamVideoProcessor_); + if (!succeeded(hr, "Create webcam video processor")) { + return false; + } + + RECT webcamSourceRect = { + 0, + 0, + static_cast(webcamWidth_), + static_cast(webcamHeight_), + }; + RECT webcamOutputRect = { + 0, + 0, + static_cast(options_.width), + static_cast(options_.height), + }; + RECT webcamDestRect = getWebcamRect(); + videoContext_->VideoProcessorSetStreamSourceRect( + webcamVideoProcessor_.Get(), + 0, + TRUE, + &webcamSourceRect); + videoContext_->VideoProcessorSetStreamDestRect( + webcamVideoProcessor_.Get(), + 0, + TRUE, + &webcamDestRect); + videoContext_->VideoProcessorSetOutputTargetRect( + webcamVideoProcessor_.Get(), + TRUE, + &webcamOutputRect); + videoContext_->VideoProcessorSetStreamAutoProcessingMode( + webcamVideoProcessor_.Get(), + 0, + FALSE); + } + return true; + } + + bool createTextures() { + HRESULT hr = S_OK; + if (options_.inputPath.empty() || options_.shaderComposite) { + D3D11_TEXTURE2D_DESC bgraDesc = {}; + bgraDesc.Width = options_.width; + bgraDesc.Height = options_.height; + bgraDesc.MipLevels = 1; + bgraDesc.ArraySize = 1; + bgraDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + bgraDesc.SampleDesc.Count = 1; + bgraDesc.Usage = D3D11_USAGE_DEFAULT; + bgraDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + hr = device_->CreateTexture2D(&bgraDesc, nullptr, &bgraTexture_); + if (!succeeded(hr, "Create BGRA texture")) { + return false; + } + hr = device_->CreateRenderTargetView(bgraTexture_.Get(), nullptr, &bgraRenderTargetView_); + if (!succeeded(hr, "Create BGRA render target view")) { + return false; + } + + D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC inputViewDesc = {}; + inputViewDesc.FourCC = 0; + inputViewDesc.ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D; + inputViewDesc.Texture2D.MipSlice = 0; + inputViewDesc.Texture2D.ArraySlice = 0; + hr = videoDevice_->CreateVideoProcessorInputView( + bgraTexture_.Get(), + bgraVideoProcessorEnumerator_.Get(), + &inputViewDesc, + &bgraInputView_); + if (!succeeded(hr, "Create video processor input view")) { + return false; + } + } + + if (options_.shaderComposite) { + D3D11_TEXTURE2D_DESC contentDesc = {}; + contentDesc.Width = options_.width; + contentDesc.Height = options_.height; + contentDesc.MipLevels = 1; + contentDesc.ArraySize = 1; + contentDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + contentDesc.SampleDesc.Count = 1; + contentDesc.Usage = D3D11_USAGE_DEFAULT; + contentDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + hr = device_->CreateTexture2D(&contentDesc, nullptr, &contentTexture_); + if (!succeeded(hr, "Create compositor content texture")) { + return false; + } + + D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC contentOutputDesc = {}; + contentOutputDesc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D; + contentOutputDesc.Texture2D.MipSlice = 0; + hr = videoDevice_->CreateVideoProcessorOutputView( + contentTexture_.Get(), + videoProcessorEnumerator_.Get(), + &contentOutputDesc, + &contentOutputView_); + if (!succeeded(hr, "Create compositor content output view")) { + return false; + } + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + hr = device_->CreateShaderResourceView( + contentTexture_.Get(), + &srvDesc, + &contentShaderResourceView_); + if (!succeeded(hr, "Create compositor content shader resource view")) { + return false; + } + + if (hasWebcamOverlay()) { + D3D11_TEXTURE2D_DESC webcamDesc = contentDesc; + hr = device_->CreateTexture2D(&webcamDesc, nullptr, &webcamTexture_); + if (!succeeded(hr, "Create compositor webcam texture")) { + return false; + } + + D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC webcamOutputDesc = {}; + webcamOutputDesc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D; + webcamOutputDesc.Texture2D.MipSlice = 0; + hr = videoDevice_->CreateVideoProcessorOutputView( + webcamTexture_.Get(), + webcamVideoProcessorEnumerator_.Get(), + &webcamOutputDesc, + &webcamOutputView_); + if (!succeeded(hr, "Create compositor webcam output view")) { + return false; + } + + hr = device_->CreateShaderResourceView( + webcamTexture_.Get(), + &srvDesc, + &webcamShaderResourceView_); + if (!succeeded(hr, "Create compositor webcam shader resource view")) { + return false; + } + } + } + + D3D11_TEXTURE2D_DESC nv12Desc = {}; + nv12Desc.Width = options_.width; + nv12Desc.Height = options_.height; + nv12Desc.MipLevels = 1; + nv12Desc.ArraySize = 1; + nv12Desc.Format = DXGI_FORMAT_NV12; + nv12Desc.SampleDesc.Count = 1; + nv12Desc.Usage = D3D11_USAGE_DEFAULT; + nv12Desc.BindFlags = D3D11_BIND_RENDER_TARGET; + nv12Desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + + D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC outputViewDesc = {}; + outputViewDesc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D; + outputViewDesc.Texture2D.MipSlice = 0; + + const size_t surfaceCount = static_cast(options_.surfacePoolSize); + nv12Textures_.reserve(surfaceCount); + nv12OutputViews_.reserve(surfaceCount); + bgraNv12OutputViews_.reserve(surfaceCount); + + for (size_t index = 0; index < surfaceCount; ++index) { + ComPtr texture; + hr = device_->CreateTexture2D(&nv12Desc, nullptr, &texture); + if (!succeeded(hr, "Create NV12 texture")) { + return false; + } + + ComPtr outputView; + hr = videoDevice_->CreateVideoProcessorOutputView( + texture.Get(), + videoProcessorEnumerator_.Get(), + &outputViewDesc, + &outputView); + if (!succeeded(hr, "Create video processor output view")) { + return false; + } + + ComPtr bgraOutputView; + hr = videoDevice_->CreateVideoProcessorOutputView( + texture.Get(), + bgraVideoProcessorEnumerator_.Get(), + &outputViewDesc, + &bgraOutputView); + if (!succeeded(hr, "Create BGRA video processor output view")) { + return false; + } + + nv12Textures_.push_back(texture); + nv12OutputViews_.push_back(outputView); + bgraNv12OutputViews_.push_back(bgraOutputView); + } + + return true; + } + + bool compileShader( + const char* source, + const char* entryPoint, + const char* target, + ID3DBlob** bytecode) { + ComPtr errors; + const HRESULT hr = D3DCompile( + source, + std::strlen(source), + nullptr, + nullptr, + nullptr, + entryPoint, + target, + D3DCOMPILE_ENABLE_STRICTNESS, + 0, + bytecode, + &errors); + if (FAILED(hr)) { + if (errors) { + std::cerr + << "ERROR: D3DCompile " + << target + << " failed: " + << static_cast(errors->GetBufferPointer()) + << std::endl; + } + return succeeded(hr, "D3DCompile"); + } + return true; + } + + bool createBgraShaderResource( + UINT width, + UINT height, + const std::uint8_t* pixels, + UINT stride, + ComPtr& shaderResourceView) { + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + D3D11_SUBRESOURCE_DATA data = {}; + data.pSysMem = pixels; + data.SysMemPitch = stride; + + ComPtr texture; + HRESULT hr = device_->CreateTexture2D(&desc, &data, &texture); + if (!succeeded(hr, "Create BGRA shader texture")) { + return false; + } + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = desc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + hr = device_->CreateShaderResourceView( + texture.Get(), + &srvDesc, + &shaderResourceView); + return succeeded(hr, "Create BGRA shader resource view"); + } + + bool loadWicBgraShaderResource( + const std::wstring& imagePath, + ComPtr& shaderResourceView, + UINT& width, + UINT& height, + const char* label) { + ComPtr factory; + HRESULT hr = CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&factory)); + if (!succeeded(hr, "Create WIC imaging factory")) { + return false; + } + + ComPtr decoder; + hr = factory->CreateDecoderFromFilename( + imagePath.c_str(), + nullptr, + GENERIC_READ, + WICDecodeMetadataCacheOnLoad, + &decoder); + if (!succeeded(hr, label)) { + return false; + } + + ComPtr frame; + hr = decoder->GetFrame(0, &frame); + if (!succeeded(hr, "Get WIC frame")) { + return false; + } + + ComPtr converter; + hr = factory->CreateFormatConverter(&converter); + if (!succeeded(hr, "Create WIC format converter")) { + return false; + } + hr = converter->Initialize( + frame.Get(), + GUID_WICPixelFormat32bppBGRA, + WICBitmapDitherTypeNone, + nullptr, + 0.0, + WICBitmapPaletteTypeCustom); + if (!succeeded(hr, "Initialize WIC format converter")) { + return false; + } + + hr = converter->GetSize(&width, &height); + if (!succeeded(hr, "Get WIC image size")) { + return false; + } + if (width == 0 || height == 0) { + std::cerr << "ERROR: WIC image has an invalid size." << std::endl; + return false; + } + + const UINT stride = width * 4; + std::vector pixels(static_cast(stride) * height); + hr = converter->CopyPixels( + nullptr, + stride, + static_cast(pixels.size()), + pixels.data()); + if (!succeeded(hr, "Copy WIC image pixels")) { + return false; + } + + return createBgraShaderResource( + width, + height, + pixels.data(), + stride, + shaderResourceView); + } + + bool createSolidBackgroundTexture() { + const std::uint8_t pixel[] = { + static_cast(std::round(options_.backgroundB * 255.0f)), + static_cast(std::round(options_.backgroundG * 255.0f)), + static_cast(std::round(options_.backgroundR * 255.0f)), + 255, + }; + backgroundImageWidth_ = 1; + backgroundImageHeight_ = 1; + hasBackgroundImage_ = false; + return createBgraShaderResource( + 1, + 1, + pixel, + 4, + backgroundShaderResourceView_); + } + + bool createBackgroundTexture() { + if (options_.backgroundImagePath.empty()) { + return createSolidBackgroundTexture(); + } + + if (!loadWicBgraShaderResource( + options_.backgroundImagePath, + backgroundShaderResourceView_, + backgroundImageWidth_, + backgroundImageHeight_, + "Create WIC background decoder")) { + return false; + } + + hasBackgroundImage_ = true; + return true; + } + + bool loadCursorAtlasMetadata() { + for (auto& entry : cursorAtlasEntries_) { + entry = CursorAtlasEntry{}; + } + if (options_.cursorAtlasMetadataPath.empty()) { + return false; + } + + FILE* file = nullptr; + if (_wfopen_s(&file, options_.cursorAtlasMetadataPath.c_str(), L"rb") != 0 || !file) { + std::cerr << "[gpu-export] Unable to open cursor atlas metadata file" << std::endl; + return false; + } + + char line[256]; + bool sawEntry = false; + while (std::fgets(line, sizeof(line), file)) { + int index = 0; + CursorAtlasEntry entry; + if (sscanf_s( + line, + "%d,%f,%f,%f,%f,%f,%f,%f", + &index, + &entry.x, + &entry.y, + &entry.width, + &entry.height, + &entry.anchorX, + &entry.anchorY, + &entry.aspectRatio) != 8) { + continue; + } + if ( + index < 0 || + index >= static_cast(cursorAtlasEntries_.size()) || + !std::isfinite(entry.x) || + !std::isfinite(entry.y) || + !std::isfinite(entry.width) || + !std::isfinite(entry.height) || + !std::isfinite(entry.anchorX) || + !std::isfinite(entry.anchorY) || + !std::isfinite(entry.aspectRatio) || + entry.width <= 0.0f || + entry.height <= 0.0f) { + continue; + } + entry.valid = true; + cursorAtlasEntries_[static_cast(index)] = entry; + sawEntry = true; + } + std::fclose(file); + return sawEntry; + } + + bool createCursorAtlasTexture() { + if (options_.cursorAtlasPath.empty() || options_.cursorAtlasMetadataPath.empty()) { + hasCursorAtlas_ = false; + return true; + } + + if (!loadCursorAtlasMetadata()) { + hasCursorAtlas_ = false; + return true; + } + + if (!loadWicBgraShaderResource( + options_.cursorAtlasPath, + cursorAtlasShaderResourceView_, + cursorAtlasWidth_, + cursorAtlasHeight_, + "Create WIC cursor atlas decoder")) { + hasCursorAtlas_ = false; + return true; + } + + hasCursorAtlas_ = true; + return true; + } + + bool createShaderPipeline() { + static const char* vertexShaderSource = R"( +struct VSOut { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; +}; + +VSOut main(uint vertexId : SV_VertexID) { + float2 positions[3] = { + float2(-1.0, -1.0), + float2(-1.0, 3.0), + float2( 3.0, -1.0) + }; + + VSOut output; + float2 position = positions[vertexId]; + output.position = float4(position, 0.0, 1.0); + output.uv = float2((position.x + 1.0) * 0.5, 1.0 - ((position.y + 1.0) * 0.5)); + return output; +} +)"; + + static const char* pixelShaderSource = R"( +cbuffer CompositorConstants : register(b0) { + float outputWidth; + float outputHeight; + float radius; + float shadowSize; + float contentLeft; + float contentTop; + float contentRight; + float contentBottom; + float backgroundR; + float backgroundG; + float backgroundB; + float backgroundA; + float shadowR; + float shadowG; + float shadowB; + float shadowA; + float backgroundImageEnabled; + float backgroundImageWidth; + float backgroundImageHeight; + float webcamEnabled; + float webcamLeft; + float webcamTop; + float webcamRight; + float webcamBottom; + float webcamRadius; + float webcamShadowSize; + float webcamShadowA; + float webcamMirror; + float cursorEnabled; + float cursorX; + float cursorY; + float cursorSize; + float cursorAtlasEnabled; + float cursorAtlasLeft; + float cursorAtlasTop; + float cursorAtlasRight; + float cursorAtlasBottom; + float cursorAtlasAnchorX; + float cursorAtlasAnchorY; + float cursorAtlasAspect; + float cursorBounceScale; + float backgroundBlurPx; + float backgroundBlurPadding0; + float backgroundBlurPadding1; + float zoomEnabled; + float zoomScale; + float zoomX; + float zoomY; +}; + +Texture2D contentTexture : register(t0); +Texture2D backgroundTexture : register(t1); +Texture2D webcamTexture : register(t2); +Texture2D cursorAtlasTexture : register(t3); +SamplerState linearSampler : register(s0); + +struct PSIn { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; +}; + +float roundedBoxDistance(float2 p, float2 halfSize, float cornerRadius) { + float2 q = abs(p) - halfSize + cornerRadius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - cornerRadius; +} + +float cross2(float2 a, float2 b) { + return a.x * b.y - a.y * b.x; +} + +float insideTriangle(float2 p, float2 a, float2 b, float2 c) { + float ab = cross2(b - a, p - a); + float bc = cross2(c - b, p - b); + float ca = cross2(a - c, p - c); + return (ab >= 0.0 && bc >= 0.0 && ca >= 0.0) || + (ab <= 0.0 && bc <= 0.0 && ca <= 0.0) ? 1.0 : 0.0; +} + +float cursorArrowMask(float2 p, float scale) { + float2 a = float2(0.0, 0.0) * scale; + float2 b = float2(0.0, 58.0) * scale; + float2 c = float2(15.0, 44.0) * scale; + float2 d = float2(25.0, 66.0) * scale; + float2 e = float2(37.0, 61.0) * scale; + float2 f = float2(27.0, 40.0) * scale; + float2 g = float2(45.0, 40.0) * scale; + + return max( + insideTriangle(p, a, b, c), + max( + insideTriangle(p, a, c, g), + max( + insideTriangle(p, c, d, e), + insideTriangle(p, c, e, f) + ) + ) + ); +} + +float sampleCursorAtlasAlpha(float2 cursorLocal, float cursorWidth, float cursorHeight) { + float2 cursorUv = float2( + cursorLocal.x / cursorWidth + cursorAtlasAnchorX, + cursorLocal.y / cursorHeight + cursorAtlasAnchorY + ); + if ( + cursorUv.x < 0.0 || cursorUv.x > 1.0 || + cursorUv.y < 0.0 || cursorUv.y > 1.0 + ) { + return 0.0; + } + + float2 atlasMin = float2(cursorAtlasLeft, cursorAtlasTop); + float2 atlasMax = float2(cursorAtlasRight, cursorAtlasBottom); + return cursorAtlasTexture.Sample(linearSampler, lerp(atlasMin, atlasMax, cursorUv)).a; +} + +float sampleCursorAtlasShadow(float2 cursorLocal, float cursorWidth, float cursorHeight) { + float2 shadowLocal = cursorLocal - float2(0.0, 2.0); + float alpha = sampleCursorAtlasAlpha(shadowLocal, cursorWidth, cursorHeight) * 0.20; + alpha += sampleCursorAtlasAlpha(shadowLocal - float2(1.5, 0.0), cursorWidth, cursorHeight) * 0.06; + alpha += sampleCursorAtlasAlpha(shadowLocal + float2(1.5, 0.0), cursorWidth, cursorHeight) * 0.06; + alpha += sampleCursorAtlasAlpha(shadowLocal - float2(0.0, 1.5), cursorWidth, cursorHeight) * 0.04; + alpha += sampleCursorAtlasAlpha(shadowLocal + float2(0.0, 1.5), cursorWidth, cursorHeight) * 0.04; + return saturate(alpha); +} + +float2 getBackgroundCoverUv(float2 uv) { + float2 backgroundUv = uv; + float outputAspect = outputWidth / outputHeight; + float backgroundAspect = backgroundImageWidth / backgroundImageHeight; + if (backgroundAspect > outputAspect) { + backgroundUv.x = 0.5 + ((backgroundUv.x - 0.5) * (outputAspect / backgroundAspect)); + } else { + backgroundUv.y = 0.5 + ((backgroundUv.y - 0.5) * (backgroundAspect / outputAspect)); + } + return saturate(backgroundUv); +} + +float4 sampleBackground(float2 uv) { + if (backgroundImageEnabled <= 0.5) { + return float4(backgroundR, backgroundG, backgroundB, backgroundA); + } + + float2 backgroundUv = getBackgroundCoverUv(uv); + float safeBlur = min(max(backgroundBlurPx, 0.0), 96.0); + if (safeBlur <= 0.001) { + return backgroundTexture.Sample(linearSampler, backgroundUv); + } + + float2 texel = float2(1.0 / max(outputWidth, 1.0), 1.0 / max(outputHeight, 1.0)); + float2 r1 = texel * safeBlur * 0.35; + float2 r2 = texel * safeBlur * 0.70; + float4 color = backgroundTexture.Sample(linearSampler, backgroundUv) * 0.20; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r1.x, 0.0))) * 0.10; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r1.x, 0.0))) * 0.10; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, r1.y))) * 0.10; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, -r1.y))) * 0.10; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r2.x, r2.y))) * 0.05; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r2.x, r2.y))) * 0.05; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r2.x, -r2.y))) * 0.05; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r2.x, -r2.y))) * 0.05; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r2.x, 0.0))) * 0.05; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r2.x, 0.0))) * 0.05; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, r2.y))) * 0.05; + color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, -r2.y))) * 0.05; + return color; +} + +float4 main(PSIn input) : SV_Target { + float2 outputSize = float2(outputWidth, outputHeight); + float2 pixel = input.uv * outputSize; + float safeZoomScale = max(zoomScale, 0.01); + float2 zoomOffset = float2(zoomX, zoomY); + float2 contentPixel = zoomEnabled > 0.5 + ? (pixel - zoomOffset) / safeZoomScale + : pixel; + float2 rectMin = float2(contentLeft, contentTop); + float2 rectMax = float2(contentRight, contentBottom); + float2 halfSize = max((rectMax - rectMin) * 0.5, float2(1.0, 1.0)); + float2 center = (rectMin + rectMax) * 0.5; + float distanceToRect = roundedBoxDistance(contentPixel - center, halfSize, radius); + + float contentAlpha = 1.0 - smoothstep(-0.75, 0.75, distanceToRect); + float outsideAlpha = smoothstep(-0.75, 0.75, distanceToRect); + float shadowAlpha = + (1.0 - smoothstep(0.0, max(shadowSize, 1.0), max(distanceToRect, 0.0))) * + outsideAlpha * + shadowA; + + float4 background = sampleBackground(input.uv); + float4 shadow = float4(shadowR, shadowG, shadowB, shadowAlpha); + float4 content = contentTexture.Sample(linearSampler, saturate(contentPixel / outputSize)); + + float3 withShadow = lerp(background.rgb, shadow.rgb, saturate(shadow.a)); + float3 rgb = lerp(withShadow, content.rgb, saturate(contentAlpha)); + if (webcamEnabled > 0.5) { + float2 webcamMin = float2(webcamLeft, webcamTop); + float2 webcamMax = float2(webcamRight, webcamBottom); + float webcamInfluence = max(webcamShadowSize, 1.0) + 2.0; + bool nearWebcam = + pixel.x >= webcamMin.x - webcamInfluence && + pixel.x <= webcamMax.x + webcamInfluence && + pixel.y >= webcamMin.y - webcamInfluence && + pixel.y <= webcamMax.y + webcamInfluence; + if (nearWebcam) { + float2 webcamHalfSize = max((webcamMax - webcamMin) * 0.5, float2(1.0, 1.0)); + float2 webcamCenter = (webcamMin + webcamMax) * 0.5; + float webcamDistance = + roundedBoxDistance(pixel - webcamCenter, webcamHalfSize, webcamRadius); + float webcamAlpha = 1.0 - smoothstep(-0.75, 0.75, webcamDistance); + float webcamOutsideAlpha = smoothstep(-0.75, 0.75, webcamDistance); + float webcamShadowAlpha = + (1.0 - smoothstep(0.0, max(webcamShadowSize, 1.0), max(webcamDistance, 0.0))) * + webcamOutsideAlpha * + webcamShadowA; + rgb = lerp(rgb, shadow.rgb, saturate(webcamShadowAlpha)); + + if (webcamAlpha > 0.001) { + float2 webcamUv = input.uv; + if (webcamMirror > 0.5) { + float mirroredX = webcamLeft + (webcamRight - pixel.x); + webcamUv.x = mirroredX / outputWidth; + } + float4 webcam = webcamTexture.Sample(linearSampler, saturate(webcamUv)); + rgb = lerp(rgb, webcam.rgb, saturate(webcamAlpha)); + } + } + } + if (cursorEnabled > 0.5) { + float safeBounceScale = max(cursorBounceScale, 0.1); + float cursorHeight = max(cursorSize, 1.0) * safeBounceScale; + float cursorWidth = cursorHeight * max(cursorAtlasAspect, 0.01); + float2 cursorPixel = float2(cursorX, cursorY); + if (zoomEnabled > 0.5) { + cursorPixel = cursorPixel * safeZoomScale + zoomOffset; + } + float2 cursorLocal = pixel - cursorPixel; + if (cursorAtlasEnabled > 0.5) { + float shadowAlpha = sampleCursorAtlasShadow(cursorLocal, cursorWidth, cursorHeight); + rgb = lerp(rgb, float3(0.0, 0.0, 0.0), shadowAlpha); + + float2 cursorUv = float2( + cursorLocal.x / cursorWidth + cursorAtlasAnchorX, + cursorLocal.y / cursorHeight + cursorAtlasAnchorY + ); + if ( + cursorUv.x >= 0.0 && cursorUv.x <= 1.0 && + cursorUv.y >= 0.0 && cursorUv.y <= 1.0 + ) { + float2 atlasMin = float2(cursorAtlasLeft, cursorAtlasTop); + float2 atlasMax = float2(cursorAtlasRight, cursorAtlasBottom); + float4 cursorSample = cursorAtlasTexture.Sample( + linearSampler, + lerp(atlasMin, atlasMax, cursorUv) + ); + rgb = lerp(rgb, cursorSample.rgb, saturate(cursorSample.a)); + } + } else { + float scale = max(cursorSize, 1.0) / 72.0; + float cursorExtent = max(cursorSize, 1.0) * safeBounceScale * 1.15 + 8.0; + if (abs(cursorLocal.x) <= cursorExtent && abs(cursorLocal.y) <= cursorExtent) { + float shadowMask = cursorArrowMask(cursorLocal - float2(3.0, 3.0), scale); + rgb = lerp(rgb, float3(0.0, 0.0, 0.0), shadowMask * 0.35); + float outlineMask = cursorArrowMask(cursorLocal / 1.08, scale * 1.08); + rgb = lerp(rgb, float3(0.0, 0.0, 0.0), outlineMask * 0.75); + float cursorMask = cursorArrowMask(cursorLocal, scale); + rgb = lerp(rgb, float3(1.0, 1.0, 1.0), cursorMask * 0.95); + } + } + } + return float4(rgb, 1.0); +} +)"; + + ComPtr vertexBytecode; + if (!compileShader(vertexShaderSource, "main", "vs_5_0", &vertexBytecode)) { + return false; + } + HRESULT hr = device_->CreateVertexShader( + vertexBytecode->GetBufferPointer(), + vertexBytecode->GetBufferSize(), + nullptr, + &vertexShader_); + if (!succeeded(hr, "CreateVertexShader")) { + return false; + } + + ComPtr pixelBytecode; + if (!compileShader(pixelShaderSource, "main", "ps_5_0", &pixelBytecode)) { + return false; + } + hr = device_->CreatePixelShader( + pixelBytecode->GetBufferPointer(), + pixelBytecode->GetBufferSize(), + nullptr, + &pixelShader_); + if (!succeeded(hr, "CreatePixelShader")) { + return false; + } + + D3D11_BUFFER_DESC constantDesc = {}; + constantDesc.ByteWidth = sizeof(ShaderConstants); + constantDesc.Usage = D3D11_USAGE_DEFAULT; + constantDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + hr = device_->CreateBuffer(&constantDesc, nullptr, &compositorConstants_); + if (!succeeded(hr, "Create compositor constant buffer")) { + return false; + } + + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + hr = device_->CreateSamplerState(&samplerDesc, &samplerState_); + if (!succeeded(hr, "Create compositor sampler state")) { + return false; + } + return createBackgroundTexture() && createCursorAtlasTexture(); + } + + bool createSinkWriter() { + ComPtr attributes; + HRESULT hr = MFCreateAttributes(&attributes, 4); + if (!succeeded(hr, "MFCreateAttributes")) { + return false; + } + attributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); + attributes->SetUINT32(MF_SINK_WRITER_DISABLE_THROTTLING, TRUE); + attributes->SetUnknown(MF_SINK_WRITER_D3D_MANAGER, deviceManager_.Get()); + + hr = MFCreateSinkWriterFromURL(options_.outputPath.c_str(), nullptr, attributes.Get(), &sinkWriter_); + if (!succeeded(hr, "MFCreateSinkWriterFromURL")) { + return false; + } + + ComPtr outputType; + hr = MFCreateMediaType(&outputType); + if (!succeeded(hr, "MFCreateMediaType output")) { + return false; + } + outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264); + outputType->SetUINT32(MF_MT_AVG_BITRATE, options_.bitrate); + outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + MFSetAttributeSize(outputType.Get(), MF_MT_FRAME_SIZE, options_.width, options_.height); + MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, options_.fps, 1); + MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + + hr = sinkWriter_->AddStream(outputType.Get(), &streamIndex_); + if (!succeeded(hr, "IMFSinkWriter::AddStream")) { + return false; + } + + ComPtr inputType; + hr = MFCreateMediaType(&inputType); + if (!succeeded(hr, "MFCreateMediaType input")) { + return false; + } + inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12); + inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + inputType->SetUINT32(MF_MT_DEFAULT_STRIDE, options_.width); + MFSetAttributeSize(inputType.Get(), MF_MT_FRAME_SIZE, options_.width, options_.height); + MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, options_.fps, 1); + MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + + ComPtr encoderAttributes; + if (options_.fastEncoderTuning && SUCCEEDED(MFCreateAttributes(&encoderAttributes, 8))) { + encoderAttributes->SetUINT32(CODECAPI_AVLowLatencyMode, TRUE); + encoderAttributes->SetUINT32(CODECAPI_AVEncCommonQualityVsSpeed, 0); + encoderAttributes->SetUINT32( + CODECAPI_AVEncCommonRateControlMode, + eAVEncCommonRateControlMode_LowDelayVBR); + encoderAttributes->SetUINT32(CODECAPI_AVEncCommonMeanBitRate, options_.bitrate); + encoderAttributes->SetUINT32( + CODECAPI_AVEncCommonMaxBitRate, + static_cast(std::min( + 0xffffffffu, + static_cast(options_.bitrate) * 3 / 2))); + encoderAttributes->SetUINT32(CODECAPI_AVEncMPVDefaultBPictureCount, 0); + encoderAttributes->SetUINT32(CODECAPI_AVEncH264CABACEnable, FALSE); + } + + hr = sinkWriter_->SetInputMediaType(streamIndex_, inputType.Get(), encoderAttributes.Get()); + encoderTuningApplied_ = SUCCEEDED(hr) && encoderAttributes; + if (FAILED(hr) && encoderAttributes) { + hr = sinkWriter_->SetInputMediaType(streamIndex_, inputType.Get(), nullptr); + encoderTuningApplied_ = false; + } + if (!succeeded(hr, "IMFSinkWriter::SetInputMediaType")) { + return false; + } + hr = sinkWriter_->BeginWriting(); + return succeeded(hr, "IMFSinkWriter::BeginWriting"); + } + + bool createNvencSdkEncoder() { +#ifdef RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK + try { + nvencEncoder_ = std::make_unique( + device_.Get(), + options_.width, + options_.height, + NV_ENC_BUFFER_FORMAT_NV12); + + NV_ENC_INITIALIZE_PARAMS initializeParams = {NV_ENC_INITIALIZE_PARAMS_VER}; + NV_ENC_CONFIG encodeConfig = {NV_ENC_CONFIG_VER}; + initializeParams.encodeConfig = &encodeConfig; + nvencEncoder_->CreateDefaultEncoderParams( + &initializeParams, + NV_ENC_CODEC_H264_GUID, + NV_ENC_PRESET_HP_GUID); + + initializeParams.frameRateNum = options_.fps; + initializeParams.frameRateDen = 1; + initializeParams.enableEncodeAsync = 1; + encodeConfig.profileGUID = NV_ENC_H264_PROFILE_HIGH_GUID; + encodeConfig.gopLength = options_.fps * 2; + encodeConfig.frameIntervalP = 1; + encodeConfig.rcParams.rateControlMode = NV_ENC_PARAMS_RC_VBR; + encodeConfig.rcParams.averageBitRate = options_.bitrate; + encodeConfig.rcParams.maxBitRate = static_cast(std::min( + 0xffffffffu, + static_cast(options_.bitrate) * 3 / 2)); + encodeConfig.rcParams.vbvBufferSize = options_.bitrate; + encodeConfig.rcParams.vbvInitialDelay = options_.bitrate; + encodeConfig.encodeCodecConfig.h264Config.idrPeriod = encodeConfig.gopLength; + + nvencEncoder_->CreateEncoder(&initializeParams); + + if (_wfopen_s(&nvencOutputFile_, options_.outputPath.c_str(), L"wb") != 0 || !nvencOutputFile_) { + std::cerr << "[gpu-export] Failed to open NVENC SDK output" << std::endl; + return false; + } + encoderTuningApplied_ = true; + return true; + } catch (const std::exception& error) { + std::cerr << "[gpu-export] NVENC SDK init failed: " << error.what() << std::endl; + return false; + } +#else + std::cerr << "[gpu-export] --nvenc-sdk requested, but this build was not compiled with NVENC SDK support" + << std::endl; + return false; +#endif + } + + bool createSourceReaderForPath( + const std::wstring& path, + ComPtr& reader, + UINT& width, + UINT& height, + const char* label) { + ComPtr attributes; + HRESULT hr = MFCreateAttributes(&attributes, 4); + if (!succeeded(hr, "MFCreateAttributes source reader")) { + return false; + } + const bool useD3DSourceReader = !options_.preferHighPerformanceAdapter || options_.nvencSdk; + if (useD3DSourceReader) { + attributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, deviceManager_.Get()); + } + attributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); + attributes->SetUINT32( + MF_SOURCE_READER_DISABLE_DXVA, + useD3DSourceReader ? FALSE : TRUE); + if (options_.preferHighPerformanceAdapter && !useD3DSourceReader) { + attributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE); + } + + hr = MFCreateSourceReaderFromURL(path.c_str(), attributes.Get(), &reader); + if (!succeeded(hr, label)) { + return false; + } + + ComPtr mediaType; + hr = MFCreateMediaType(&mediaType); + if (!succeeded(hr, "MFCreateMediaType source output")) { + return false; + } + mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + mediaType->SetGUID( + MF_MT_SUBTYPE, + (options_.preferHighPerformanceAdapter && !options_.nvencSdk) ? MFVideoFormat_RGB32 : MFVideoFormat_NV12); + hr = reader->SetCurrentMediaType( + firstVideoStreamIndex(), + nullptr, + mediaType.Get()); + if (!succeeded(hr, "IMFSourceReader::SetCurrentMediaType")) { + return false; + } + + ComPtr currentType; + hr = reader->GetCurrentMediaType(firstVideoStreamIndex(), ¤tType); + if (!succeeded(hr, "IMFSourceReader::GetCurrentMediaType")) { + return false; + } + + UINT32 detectedWidth = 0; + UINT32 detectedHeight = 0; + hr = MFGetAttributeSize(currentType.Get(), MF_MT_FRAME_SIZE, &detectedWidth, &detectedHeight); + if (!succeeded(hr, "MFGetAttributeSize source frame")) { + return false; + } + width = std::max(2, detectedWidth & ~1U); + height = std::max(2, detectedHeight & ~1U); + return true; + } + + bool createSourceReader() { + return createSourceReaderForPath( + options_.inputPath, + sourceReader_, + sourceWidth_, + sourceHeight_, + "MFCreateSourceReaderFromURL input"); + } + + bool createWebcamSourceReader() { + return createSourceReaderForPath( + options_.webcamInputPath, + webcamReader_, + webcamWidth_, + webcamHeight_, + "MFCreateSourceReaderFromURL webcam"); + } + + bool hasWebcamOverlay() const { + return !options_.webcamInputPath.empty() && + options_.webcamLeft >= 0 && + options_.webcamTop >= 0 && + options_.webcamSize >= 2; + } + + bool hasCursorOverlay() const { + return !cursorSamples_.empty() && options_.cursorSize > 0.0f; + } + + bool hasZoomOverlay() const { + return !zoomSamples_.empty(); + } + + void loadCursorTelemetry() { + cursorSamples_.clear(); + if (options_.cursorTelemetryPath.empty()) { + return; + } + + FILE* file = nullptr; + if (_wfopen_s(&file, options_.cursorTelemetryPath.c_str(), L"rb") != 0 || !file) { + std::cerr << "[gpu-export] Unable to open cursor telemetry file" << std::endl; + return; + } + + char line[256]; + while (std::fgets(line, sizeof(line), file)) { + double timeMs = 0.0; + float cx = 0.0f; + float cy = 0.0f; + int cursorTypeIndex = 0; + float bounceScale = 1.0f; + int visible = 1; + const int parsed = sscanf_s( + line, + "%lf,%f,%f,%d,%f,%d", + &timeMs, + &cx, + &cy, + &cursorTypeIndex, + &bounceScale, + &visible); + if (parsed < 3) { + continue; + } + if (!std::isfinite(timeMs) || !std::isfinite(cx) || !std::isfinite(cy)) { + continue; + } + cursorSamples_.push_back(CursorSample{ + std::max(0.0, timeMs), + std::min(1.0f, std::max(0.0f, cx)), + std::min(1.0f, std::max(0.0f, cy)), + std::min(8, std::max(0, cursorTypeIndex)), + std::isfinite(bounceScale) + ? std::min(2.0f, std::max(0.1f, bounceScale)) + : 1.0f, + parsed >= 6 ? visible != 0 : true, + }); + } + std::fclose(file); + + std::sort(cursorSamples_.begin(), cursorSamples_.end(), [](const auto& left, const auto& right) { + return left.timeMs < right.timeMs; + }); + } + + CursorSample getCursorSampleAt(double timeMs) const { + if (cursorSamples_.empty()) { + return {}; + } + if (timeMs <= cursorSamples_.front().timeMs) { + return cursorSamples_.front(); + } + if (timeMs >= cursorSamples_.back().timeMs) { + return cursorSamples_.back(); + } + + const auto upper = std::upper_bound( + cursorSamples_.begin(), + cursorSamples_.end(), + timeMs, + [](double value, const CursorSample& sample) { + return value < sample.timeMs; + }); + const auto& b = *upper; + const auto& a = *(upper - 1); + const double span = std::max(1.0, b.timeMs - a.timeMs); + const float t = static_cast((timeMs - a.timeMs) / span); + return CursorSample{ + timeMs, + a.cx + (b.cx - a.cx) * t, + a.cy + (b.cy - a.cy) * t, + a.cursorTypeIndex, + a.bounceScale + (b.bounceScale - a.bounceScale) * t, + a.visible && b.visible, + }; + } + + const CursorAtlasEntry* getCursorAtlasEntry(int cursorTypeIndex) const { + if (!hasCursorAtlas_ || cursorAtlasWidth_ == 0 || cursorAtlasHeight_ == 0) { + return nullptr; + } + + const size_t index = static_cast( + std::min(8, std::max(0, cursorTypeIndex))); + if (index >= cursorAtlasEntries_.size() || !cursorAtlasEntries_[index].valid) { + return nullptr; + } + + return &cursorAtlasEntries_[index]; + } + + void loadZoomTelemetry() { + zoomSamples_.clear(); + if (options_.zoomTelemetryPath.empty()) { + return; + } + + FILE* file = nullptr; + if (_wfopen_s(&file, options_.zoomTelemetryPath.c_str(), L"rb") != 0 || !file) { + std::cerr << "[gpu-export] Unable to open zoom telemetry file" << std::endl; + return; + } + + char line[256]; + while (std::fgets(line, sizeof(line), file)) { + double timeMs = 0.0; + float scale = 1.0f; + float x = 0.0f; + float y = 0.0f; + if (sscanf_s(line, "%lf,%f,%f,%f", &timeMs, &scale, &x, &y) != 4) { + continue; + } + if (!std::isfinite(timeMs) || !std::isfinite(scale) || !std::isfinite(x) || !std::isfinite(y)) { + continue; + } + zoomSamples_.push_back(ZoomSample{ + std::max(0.0, timeMs), + std::max(0.01f, scale), + x, + y, + }); + } + std::fclose(file); + + std::sort(zoomSamples_.begin(), zoomSamples_.end(), [](const auto& left, const auto& right) { + return left.timeMs < right.timeMs; + }); + } + + ZoomSample getZoomSampleAt(double timeMs) const { + if (zoomSamples_.empty()) { + return {}; + } + if (timeMs <= zoomSamples_.front().timeMs) { + return zoomSamples_.front(); + } + if (timeMs >= zoomSamples_.back().timeMs) { + return zoomSamples_.back(); + } + + const auto upper = std::upper_bound( + zoomSamples_.begin(), + zoomSamples_.end(), + timeMs, + [](double value, const ZoomSample& sample) { + return value < sample.timeMs; + }); + const auto& b = *upper; + const auto& a = *(upper - 1); + const double span = std::max(1.0, b.timeMs - a.timeMs); + const float t = static_cast((timeMs - a.timeMs) / span); + return ZoomSample{ + timeMs, + a.scale + (b.scale - a.scale) * t, + a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t, + }; + } + + RECT getWebcamRect() const { + if (!hasWebcamOverlay()) { + return {0, 0, 0, 0}; + } + + const LONG left = std::min( + std::max(0, options_.webcamLeft), + static_cast(options_.width) - 2); + const LONG top = std::min( + std::max(0, options_.webcamTop), + static_cast(options_.height) - 2); + const LONG size = std::min( + options_.webcamSize & ~1L, + std::min( + static_cast(options_.width) - left, + static_cast(options_.height) - top)); + const LONG safeSize = std::max(2, size); + return {left, top, left + safeSize, top + safeSize}; + } + + RECT getSourceCropRect() const { + if ( + options_.sourceCropWidth >= 2 && + options_.sourceCropHeight >= 2 && + sourceWidth_ >= 2 && + sourceHeight_ >= 2 + ) { + const LONG left = std::min( + std::max(0, options_.sourceCropLeft), + static_cast(sourceWidth_) - 2); + const LONG top = std::min( + std::max(0, options_.sourceCropTop), + static_cast(sourceHeight_) - 2); + const LONG width = (std::min( + options_.sourceCropWidth & ~1L, + static_cast(sourceWidth_) - left)) & ~1L; + const LONG height = (std::min( + options_.sourceCropHeight & ~1L, + static_cast(sourceHeight_) - top)) & ~1L; + return { + left, + top, + left + std::max(2, width), + top + std::max(2, height), + }; + } + + return { + 0, + 0, + static_cast(sourceWidth_), + static_cast(sourceHeight_), + }; + } + + RECT getContentRect() const { + if ( + options_.contentLeft >= 0 && + options_.contentTop >= 0 && + options_.contentWidth >= 2 && + options_.contentHeight >= 2 + ) { + const LONG left = std::min( + std::max(0, options_.contentLeft), + static_cast(options_.width) - 2); + const LONG top = std::min( + std::max(0, options_.contentTop), + static_cast(options_.height) - 2); + const LONG width = std::min( + options_.contentWidth & ~1L, + static_cast(options_.width) - left); + const LONG height = std::min( + options_.contentHeight & ~1L, + static_cast(options_.height) - top); + return { + left, + top, + left + std::max(2, width), + top + std::max(2, height), + }; + } + + const double availableWidth = + static_cast(options_.width) * (1.0 - (2.0 * options_.padding)); + const double availableHeight = + static_cast(options_.height) * (1.0 - (2.0 * options_.padding)); + const double scale = std::min( + availableWidth / static_cast(sourceWidth_), + availableHeight / static_cast(sourceHeight_)); + const LONG contentWidth = static_cast( + std::max(2, static_cast(sourceWidth_ * scale) & ~1U)); + const LONG contentHeight = static_cast( + std::max(2, static_cast(sourceHeight_ * scale) & ~1U)); + const LONG x = (static_cast(options_.width) - contentWidth) / 2; + const LONG y = (static_cast(options_.height) - contentHeight) / 2; + return {x, y, x + contentWidth, y + contentHeight}; + } + + void renderSyntheticFrame(UINT frameIndex) { + const float t = static_cast(frameIndex % options_.fps) / static_cast(options_.fps); + const float color[4] = { + 0.05f + 0.45f * t, + 0.18f, + 0.42f + 0.3f * (1.0f - t), + 1.0f, + }; + deviceContext_->ClearRenderTargetView(bgraRenderTargetView_.Get(), color); + } + + bool convertBgraToNv12(UINT frameIndex) { + const size_t surfaceIndex = static_cast(frameIndex) % nv12OutputViews_.size(); + D3D11_VIDEO_PROCESSOR_STREAM stream = {}; + stream.Enable = TRUE; + stream.OutputIndex = 0; + stream.InputFrameOrField = 0; + stream.PastFrames = 0; + stream.FutureFrames = 0; + stream.pInputSurface = bgraInputView_.Get(); + + const HRESULT hr = videoContext_->VideoProcessorBlt( + bgraVideoProcessor_.Get(), + bgraNv12OutputViews_[surfaceIndex].Get(), + frameIndex, + 1, + &stream); + if (!succeeded(hr, "VideoProcessorBlt")) { + return false; + } + return true; + } + + bool convertSourceTextureToNv12( + ID3D11Texture2D* texture, + UINT subresourceIndex, + UINT frameIndex) { + ComPtr inputView; + D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC inputViewDesc = {}; + inputViewDesc.FourCC = 0; + inputViewDesc.ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D; + inputViewDesc.Texture2D.MipSlice = 0; + inputViewDesc.Texture2D.ArraySlice = subresourceIndex; + + HRESULT hr = videoDevice_->CreateVideoProcessorInputView( + texture, + videoProcessorEnumerator_.Get(), + &inputViewDesc, + &inputView); + if (!succeeded(hr, "Create source video processor input view")) { + return false; + } + + const size_t surfaceIndex = static_cast(frameIndex) % nv12OutputViews_.size(); + D3D11_VIDEO_PROCESSOR_STREAM stream = {}; + stream.Enable = TRUE; + stream.OutputIndex = 0; + stream.InputFrameOrField = 0; + stream.pInputSurface = inputView.Get(); + + hr = videoContext_->VideoProcessorBlt( + videoProcessor_.Get(), + nv12OutputViews_[surfaceIndex].Get(), + frameIndex, + 1, + &stream); + if (!succeeded(hr, "VideoProcessorBlt source")) { + return false; + } + return true; + } + + bool convertSourceTextureToBgra( + ID3D11Texture2D* texture, + UINT subresourceIndex, + UINT frameIndex) { + ComPtr inputView; + D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC inputViewDesc = {}; + inputViewDesc.FourCC = 0; + inputViewDesc.ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D; + inputViewDesc.Texture2D.MipSlice = 0; + inputViewDesc.Texture2D.ArraySlice = subresourceIndex; + + HRESULT hr = videoDevice_->CreateVideoProcessorInputView( + texture, + videoProcessorEnumerator_.Get(), + &inputViewDesc, + &inputView); + if (!succeeded(hr, "Create source BGRA video processor input view")) { + return false; + } + + D3D11_VIDEO_PROCESSOR_STREAM stream = {}; + stream.Enable = TRUE; + stream.OutputIndex = 0; + stream.InputFrameOrField = 0; + stream.pInputSurface = inputView.Get(); + + hr = videoContext_->VideoProcessorBlt( + videoProcessor_.Get(), + contentOutputView_.Get(), + frameIndex, + 1, + &stream); + if (!succeeded(hr, "VideoProcessorBlt source BGRA")) { + return false; + } + return true; + } + + bool ensureNv12UploadTexture( + ComPtr& texture, + UINT width, + UINT height, + const char* label) { + if (texture) { + return true; + } + + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_NV12; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_DECODER | D3D11_BIND_SHADER_RESOURCE; + + const HRESULT hr = device_->CreateTexture2D(&desc, nullptr, &texture); + return succeeded(hr, label); + } + + bool uploadNv12BufferToTexture( + IMFMediaBuffer* buffer, + ComPtr& texture, + UINT width, + UINT height, + const char* label) { + if (!ensureNv12UploadTexture(texture, width, height, label)) { + return false; + } + + const DWORD expectedLength = width * height * 3 / 2; + ComPtr buffer2D; + HRESULT hr = buffer->QueryInterface(IID_PPV_ARGS(&buffer2D)); + if (SUCCEEDED(hr)) { + DWORD contiguousLength = 0; + hr = buffer2D->GetContiguousLength(&contiguousLength); + if (SUCCEEDED(hr) && contiguousLength >= expectedLength) { + uploadPaddedScratch_.resize(contiguousLength); + hr = buffer2D->ContiguousCopyTo(uploadPaddedScratch_.data(), contiguousLength); + if (!succeeded(hr, "IMF2DBuffer::ContiguousCopyTo")) { + return false; + } + + const UINT nv12Rows = height + (height / 2); + const UINT sourcePitch = + contiguousLength % nv12Rows == 0 + ? contiguousLength / nv12Rows + : width; + if (sourcePitch < width) { + std::cerr << "ERROR: Unsupported contiguous NV12 pitch." << std::endl; + return false; + } + + uploadScratch_.resize(expectedLength); + const BYTE* yPlane = uploadPaddedScratch_.data(); + const BYTE* uvPlane = uploadPaddedScratch_.data() + (sourcePitch * height); + BYTE* yOut = uploadScratch_.data(); + BYTE* uvOut = uploadScratch_.data() + (width * height); + for (UINT row = 0; row < height; row += 1) { + std::memcpy(yOut + (row * width), yPlane + (row * sourcePitch), width); + } + for (UINT row = 0; row < height / 2; row += 1) { + std::memcpy(uvOut + (row * width), uvPlane + (row * sourcePitch), width); + } + + deviceContext_->UpdateSubresource( + texture.Get(), + 0, + nullptr, + uploadScratch_.data(), + width, + expectedLength); + return true; + } + + BYTE* scanline0 = nullptr; + LONG pitch = 0; + hr = buffer2D->Lock2D(&scanline0, &pitch); + if (!succeeded(hr, "IMF2DBuffer::Lock2D")) { + return false; + } + if (pitch <= 0 || static_cast(pitch) < width) { + buffer2D->Unlock2D(); + std::cerr << "ERROR: Unsupported NV12 pitch." << std::endl; + return false; + } + + uploadScratch_.resize(expectedLength); + const BYTE* yPlane = scanline0; + const BYTE* uvPlane = scanline0 + (pitch * height); + BYTE* yOut = uploadScratch_.data(); + BYTE* uvOut = uploadScratch_.data() + (width * height); + for (UINT row = 0; row < height; row += 1) { + std::memcpy(yOut + (row * width), yPlane + (row * pitch), width); + } + for (UINT row = 0; row < height / 2; row += 1) { + std::memcpy(uvOut + (row * width), uvPlane + (row * pitch), width); + } + buffer2D->Unlock2D(); + + deviceContext_->UpdateSubresource( + texture.Get(), + 0, + nullptr, + uploadScratch_.data(), + width, + expectedLength); + return true; + } + + BYTE* data = nullptr; + DWORD maxLength = 0; + DWORD currentLength = 0; + hr = buffer->Lock(&data, &maxLength, ¤tLength); + if (!succeeded(hr, "IMFMediaBuffer::Lock")) { + return false; + } + if (currentLength < expectedLength) { + buffer->Unlock(); + std::cerr << "ERROR: NV12 buffer shorter than expected." << std::endl; + return false; + } + deviceContext_->UpdateSubresource(texture.Get(), 0, nullptr, data, width, expectedLength); + buffer->Unlock(); + return true; + } + + bool ensureBgraUploadTexture( + ComPtr& texture, + UINT width, + UINT height, + const char* label) { + if (texture) { + return true; + } + + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + const HRESULT hr = device_->CreateTexture2D(&desc, nullptr, &texture); + return succeeded(hr, label); + } + + bool uploadBgraBufferToTexture( + IMFMediaBuffer* buffer, + ComPtr& texture, + UINT width, + UINT height, + const char* label) { + if (!ensureBgraUploadTexture(texture, width, height, label)) { + return false; + } + + const DWORD rowBytes = width * 4; + const DWORD expectedLength = rowBytes * height; + ComPtr buffer2D; + HRESULT hr = buffer->QueryInterface(IID_PPV_ARGS(&buffer2D)); + if (SUCCEEDED(hr)) { + BYTE* scanline0 = nullptr; + LONG pitch = 0; + hr = buffer2D->Lock2D(&scanline0, &pitch); + if (!succeeded(hr, "IMF2DBuffer::Lock2D BGRA")) { + return false; + } + + const LONG absPitch = pitch < 0 ? -pitch : pitch; + if (absPitch < static_cast(rowBytes)) { + buffer2D->Unlock2D(); + std::cerr << "ERROR: Unsupported BGRA pitch." << std::endl; + return false; + } + + uploadScratch_.resize(expectedLength); + for (UINT row = 0; row < height; row += 1) { + const BYTE* sourceRow = pitch > 0 + ? scanline0 + (row * pitch) + : scanline0 + ((height - 1 - row) * absPitch); + std::memcpy(uploadScratch_.data() + (row * rowBytes), sourceRow, rowBytes); + } + buffer2D->Unlock2D(); + + deviceContext_->UpdateSubresource( + texture.Get(), + 0, + nullptr, + uploadScratch_.data(), + rowBytes, + expectedLength); + return true; + } + + BYTE* data = nullptr; + DWORD maxLength = 0; + DWORD currentLength = 0; + hr = buffer->Lock(&data, &maxLength, ¤tLength); + if (!succeeded(hr, "IMFMediaBuffer::Lock BGRA")) { + return false; + } + if (currentLength < expectedLength) { + buffer->Unlock(); + std::cerr << "ERROR: BGRA buffer shorter than expected." << std::endl; + return false; + } + deviceContext_->UpdateSubresource( + texture.Get(), + 0, + nullptr, + data, + rowBytes, + expectedLength); + buffer->Unlock(); + return true; + } + + bool convertWebcamTextureToBgra( + ID3D11Texture2D* texture, + UINT subresourceIndex, + UINT frameIndex) { + ComPtr inputView; + D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC inputViewDesc = {}; + inputViewDesc.FourCC = 0; + inputViewDesc.ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D; + inputViewDesc.Texture2D.MipSlice = 0; + inputViewDesc.Texture2D.ArraySlice = subresourceIndex; + + HRESULT hr = videoDevice_->CreateVideoProcessorInputView( + texture, + webcamVideoProcessorEnumerator_.Get(), + &inputViewDesc, + &inputView); + if (!succeeded(hr, "Create webcam video processor input view")) { + return false; + } + + D3D11_VIDEO_PROCESSOR_STREAM stream = {}; + stream.Enable = TRUE; + stream.OutputIndex = 0; + stream.InputFrameOrField = 0; + stream.pInputSurface = inputView.Get(); + + hr = videoContext_->VideoProcessorBlt( + webcamVideoProcessor_.Get(), + webcamOutputView_.Get(), + frameIndex, + 1, + &stream); + if (!succeeded(hr, "VideoProcessorBlt webcam BGRA")) { + return false; + } + webcamFrameReady_ = true; + return true; + } + + bool convertWebcamSampleToBgra( + IMFSample* sample, + UINT frameIndex) { + ComPtr buffer; + HRESULT hr = sample->GetBufferByIndex(0, &buffer); + if (!succeeded(hr, "IMFSample::GetBufferByIndex webcam")) { + return false; + } + + ComPtr dxgiBuffer; + hr = buffer.As(&dxgiBuffer); + if (FAILED(hr)) { + if (options_.preferHighPerformanceAdapter) { + if (!uploadBgraBufferToTexture( + buffer.Get(), + webcamUploadBgraTexture_, + webcamWidth_, + webcamHeight_, + "Create webcam BGRA upload texture")) { + return false; + } + return convertWebcamTextureToBgra(webcamUploadBgraTexture_.Get(), 0, frameIndex); + } else { + if (!uploadNv12BufferToTexture( + buffer.Get(), + webcamUploadTexture_, + webcamWidth_, + webcamHeight_, + "Create webcam NV12 upload texture")) { + return false; + } + return convertWebcamTextureToBgra(webcamUploadTexture_.Get(), 0, frameIndex); + } + } + + ComPtr webcamTexture; + hr = dxgiBuffer->GetResource(IID_PPV_ARGS(&webcamTexture)); + if (!succeeded(hr, "IMFDXGIBuffer::GetResource webcam")) { + return false; + } + UINT subresourceIndex = 0; + dxgiBuffer->GetSubresourceIndex(&subresourceIndex); + return convertWebcamTextureToBgra(webcamTexture.Get(), subresourceIndex, frameIndex); + } + + bool readWebcamFrameForTimestamp(LONGLONG outputTimestamp, UINT frameIndex) { + if (!hasWebcamOverlay() || webcamEnded_) { + return true; + } + + const LONGLONG targetTimestamp = std::max( + 0, + outputTimestamp - static_cast(options_.webcamTimeOffsetMs * 10'000.0)); + + if (pendingWebcamSample_) { + const LONGLONG pendingTimestamp = std::max( + 0, + pendingWebcamTimestamp_ - webcamFirstTimestamp_); + if (pendingTimestamp > targetTimestamp && webcamFrameReady_) { + return true; + } + if (!convertWebcamSampleToBgra(pendingWebcamSample_.Get(), frameIndex)) { + return false; + } + pendingWebcamSample_.Reset(); + if (pendingTimestamp >= targetTimestamp) { + return true; + } + } + + while (true) { + DWORD streamIndex = 0; + DWORD flags = 0; + LONGLONG timestamp = 0; + ComPtr sample; + + const HRESULT hr = webcamReader_->ReadSample( + firstVideoStreamIndex(), + 0, + &streamIndex, + &flags, + ×tamp, + &sample); + if (!succeeded(hr, "IMFSourceReader::ReadSample webcam")) { + return false; + } + if (flags & MF_SOURCE_READERF_ENDOFSTREAM) { + webcamEnded_ = true; + return true; + } + if (!sample) { + return true; + } + if (webcamFirstTimestamp_ < 0) { + webcamFirstTimestamp_ = timestamp; + } + + const LONGLONG adjustedTimestamp = std::max( + 0, + timestamp - webcamFirstTimestamp_); + if (adjustedTimestamp > targetTimestamp && webcamFrameReady_) { + pendingWebcamSample_ = sample; + pendingWebcamTimestamp_ = timestamp; + return true; + } + if (!convertWebcamSampleToBgra(sample.Get(), frameIndex)) { + return false; + } + if (adjustedTimestamp >= targetTimestamp) { + return true; + } + } + } + + bool renderShaderComposite(LONGLONG outputTimestamp) { + const RECT contentRect = getContentRect(); + const RECT webcamRect = getWebcamRect(); + const bool webcamEnabled = hasWebcamOverlay() && webcamFrameReady_; + const bool cursorEnabled = hasCursorOverlay(); + const bool zoomEnabled = hasZoomOverlay(); + const ZoomSample zoom = zoomEnabled + ? getZoomSampleAt(static_cast(outputTimestamp) / 10'000.0) + : ZoomSample{}; + const CursorSample cursor = cursorEnabled + ? getCursorSampleAt(static_cast(outputTimestamp) / 10'000.0) + : CursorSample{}; + const bool cursorVisible = cursorEnabled && cursor.visible; + const CursorAtlasEntry* cursorAtlasEntry = cursorVisible + ? getCursorAtlasEntry(cursor.cursorTypeIndex) + : nullptr; + const bool cursorAtlasEnabled = cursorAtlasEntry != nullptr; + const float cursorX = cursorVisible + ? static_cast(contentRect.left) + + cursor.cx * static_cast(contentRect.right - contentRect.left) + : 0.0f; + const float cursorY = cursorVisible + ? static_cast(contentRect.top) + + cursor.cy * static_cast(contentRect.bottom - contentRect.top) + : 0.0f; + const ShaderConstants constants = { + static_cast(options_.width), + static_cast(options_.height), + std::min(options_.radius, static_cast( + std::min(contentRect.right - contentRect.left, contentRect.bottom - contentRect.top)) * 0.5f), + options_.shadow, + static_cast(contentRect.left), + static_cast(contentRect.top), + static_cast(contentRect.right), + static_cast(contentRect.bottom), + options_.backgroundR, + options_.backgroundG, + options_.backgroundB, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.42f, + hasBackgroundImage_ ? 1.0f : 0.0f, + static_cast(backgroundImageWidth_), + static_cast(backgroundImageHeight_), + webcamEnabled ? 1.0f : 0.0f, + static_cast(webcamRect.left), + static_cast(webcamRect.top), + static_cast(webcamRect.right), + static_cast(webcamRect.bottom), + webcamEnabled ? std::min(options_.webcamRadius, static_cast( + std::min(webcamRect.right - webcamRect.left, webcamRect.bottom - webcamRect.top)) * 0.5f) : 0.0f, + webcamEnabled ? options_.webcamShadow : 0.0f, + webcamEnabled ? 0.42f : 0.0f, + options_.webcamMirror ? 1.0f : 0.0f, + cursorVisible ? 1.0f : 0.0f, + cursorX, + cursorY, + options_.cursorSize, + cursorAtlasEnabled ? 1.0f : 0.0f, + cursorAtlasEnabled + ? cursorAtlasEntry->x / static_cast(cursorAtlasWidth_) + : 0.0f, + cursorAtlasEnabled + ? cursorAtlasEntry->y / static_cast(cursorAtlasHeight_) + : 0.0f, + cursorAtlasEnabled + ? (cursorAtlasEntry->x + cursorAtlasEntry->width) / + static_cast(cursorAtlasWidth_) + : 1.0f, + cursorAtlasEnabled + ? (cursorAtlasEntry->y + cursorAtlasEntry->height) / + static_cast(cursorAtlasHeight_) + : 1.0f, + cursorAtlasEnabled ? cursorAtlasEntry->anchorX : 0.0f, + cursorAtlasEnabled ? cursorAtlasEntry->anchorY : 0.0f, + cursorAtlasEnabled ? cursorAtlasEntry->aspectRatio : 1.0f, + cursorEnabled ? cursor.bounceScale : 1.0f, + hasBackgroundImage_ ? options_.backgroundBlurPx : 0.0f, + 0.0f, + 0.0f, + zoomEnabled ? 1.0f : 0.0f, + zoomEnabled ? zoom.scale : 1.0f, + zoomEnabled ? zoom.x : 0.0f, + zoomEnabled ? zoom.y : 0.0f, + }; + + deviceContext_->UpdateSubresource(compositorConstants_.Get(), 0, nullptr, &constants, 0, 0); + + const float clearColor[4] = {constants.backgroundR, constants.backgroundG, constants.backgroundB, 1.0f}; + deviceContext_->ClearRenderTargetView(bgraRenderTargetView_.Get(), clearColor); + + D3D11_VIEWPORT viewport = {}; + viewport.Width = static_cast(options_.width); + viewport.Height = static_cast(options_.height); + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + + ID3D11RenderTargetView* renderTargets[] = {bgraRenderTargetView_.Get()}; + deviceContext_->OMSetRenderTargets(1, renderTargets, nullptr); + deviceContext_->RSSetViewports(1, &viewport); + deviceContext_->IASetInputLayout(nullptr); + deviceContext_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + deviceContext_->VSSetShader(vertexShader_.Get(), nullptr, 0); + deviceContext_->PSSetShader(pixelShader_.Get(), nullptr, 0); + ID3D11Buffer* constantBuffers[] = {compositorConstants_.Get()}; + deviceContext_->PSSetConstantBuffers(0, 1, constantBuffers); + ID3D11ShaderResourceView* shaderResources[] = { + contentShaderResourceView_.Get(), + backgroundShaderResourceView_.Get(), + webcamShaderResourceView_ ? webcamShaderResourceView_.Get() : contentShaderResourceView_.Get(), + cursorAtlasShaderResourceView_ + ? cursorAtlasShaderResourceView_.Get() + : backgroundShaderResourceView_.Get(), + }; + deviceContext_->PSSetShaderResources(0, 4, shaderResources); + ID3D11SamplerState* samplers[] = {samplerState_.Get()}; + deviceContext_->PSSetSamplers(0, 1, samplers); + deviceContext_->Draw(3, 0); + + ID3D11ShaderResourceView* nullResources[] = {nullptr, nullptr, nullptr, nullptr}; + deviceContext_->PSSetShaderResources(0, 4, nullResources); + return true; + } + + bool writeNvencSdkPackets(const std::vector>& packets) { + if (!nvencOutputFile_) { + return false; + } + for (const auto& packet : packets) { + if (packet.empty()) { + continue; + } + const size_t written = std::fwrite(packet.data(), 1, packet.size(), nvencOutputFile_); + if (written != packet.size()) { + std::cerr << "[gpu-export] Failed to write NVENC SDK packet" << std::endl; + return false; + } + nvencOutputBytes_ += packet.size(); + } + return true; + } + + bool writeNvencSdkFrame(UINT frameIndex) { +#ifdef RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK + if (!nvencEncoder_) { + std::cerr << "[gpu-export] NVENC SDK encoder is not initialized" << std::endl; + return false; + } + try { + const size_t surfaceIndex = static_cast(frameIndex) % nv12Textures_.size(); + const NvEncInputFrame* inputFrame = nvencEncoder_->GetNextInputFrame(); + auto* encoderTexture = reinterpret_cast(inputFrame->inputPtr); + deviceContext_->CopyResource(encoderTexture, nv12Textures_[surfaceIndex].Get()); + deviceContext_->Flush(); + + std::vector> packets; + nvencEncoder_->EncodeFrame(packets); + return writeNvencSdkPackets(packets); + } catch (const std::exception& error) { + std::cerr << "[gpu-export] NVENC SDK encode failed: " << error.what() << std::endl; + return false; + } +#else + (void)frameIndex; + return false; +#endif + } + + bool finalizeNvencSdk() { +#ifdef RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK + if (!nvencEncoder_) { + return true; + } + try { + std::vector> packets; + nvencEncoder_->EndEncode(packets); + if (!writeNvencSdkPackets(packets)) { + return false; + } + nvencEncoder_->DestroyEncoder(); + nvencEncoder_.reset(); + if (nvencOutputFile_) { + std::fclose(nvencOutputFile_); + nvencOutputFile_ = nullptr; + } + return true; + } catch (const std::exception& error) { + std::cerr << "[gpu-export] NVENC SDK finalize failed: " << error.what() << std::endl; + return false; + } +#else + return false; +#endif + } + + bool writeFrame( + UINT frameIndex, + LONGLONG sampleTimeOverride = -1, + LONGLONG sampleDurationOverride = -1) { + if (options_.nvencSdk) { + return writeNvencSdkFrame(frameIndex); + } + + const size_t surfaceIndex = static_cast(frameIndex) % nv12Textures_.size(); + ComPtr buffer; + HRESULT hr = MFCreateDXGISurfaceBuffer( + __uuidof(ID3D11Texture2D), + nv12Textures_[surfaceIndex].Get(), + 0, + FALSE, + &buffer); + if (!succeeded(hr, "MFCreateDXGISurfaceBuffer")) { + return false; + } + const DWORD nv12ByteLength = options_.width * options_.height * 3 / 2; + hr = buffer->SetCurrentLength(nv12ByteLength); + if (!succeeded(hr, "IMFMediaBuffer::SetCurrentLength")) { + return false; + } + + ComPtr sample; + hr = MFCreateSample(&sample); + if (!succeeded(hr, "MFCreateSample")) { + return false; + } + hr = sample->AddBuffer(buffer.Get()); + if (!succeeded(hr, "IMFSample::AddBuffer")) { + return false; + } + + const LONGLONG defaultSampleTime = + static_cast(frameIndex) * 10'000'000LL / options_.fps; + const LONGLONG defaultSampleDuration = 10'000'000LL / options_.fps; + const LONGLONG sampleTime = + sampleTimeOverride >= 0 ? sampleTimeOverride : defaultSampleTime; + const LONGLONG sampleDuration = + sampleDurationOverride > 0 ? sampleDurationOverride : defaultSampleDuration; + sample->SetSampleTime(sampleTime); + sample->SetSampleDuration(sampleDuration); + + hr = sinkWriter_->WriteSample(streamIndex_, sample.Get()); + return succeeded(hr, "IMFSinkWriter::WriteSample"); + } + + bool runSourceVideo() { + const UINT maxFrames = std::max( + static_cast(std::ceil(static_cast(options_.fps) * options_.seconds * 4.0)), + static_cast(std::ceil(options_.seconds * 240.0))); + const Timer totalTimer; + double readMs = 0; + double processMs = 0; + double writeMs = 0; + UINT frameIndex = 0; + bool sawDxgiSurface = false; + LONGLONG firstSourceTimestamp = -1; + LONGLONG lastOutputTimestamp = 0; + const LONGLONG maxOutputTimestamp = + static_cast(options_.seconds * 10'000'000.0); + const LONGLONG outputFrameDuration = 10'000'000LL / options_.fps; + LONGLONG nextOutputTimestamp = 0; + const UINT expectedOutputFrames = std::max( + 1, + static_cast(std::ceil(options_.seconds * static_cast(options_.fps)))); + + while (frameIndex < maxFrames && frameIndex < expectedOutputFrames) { + DWORD streamIndex = 0; + DWORD flags = 0; + LONGLONG timestamp = 0; + ComPtr sample; + + const Timer readTimer; + HRESULT hr = sourceReader_->ReadSample( + firstVideoStreamIndex(), + 0, + &streamIndex, + &flags, + ×tamp, + &sample); + readMs += readTimer.elapsedMs(); + if (!succeeded(hr, "IMFSourceReader::ReadSample")) { + return false; + } + if (flags & MF_SOURCE_READERF_ENDOFSTREAM) { + break; + } + if (!sample) { + continue; + } + if (firstSourceTimestamp < 0) { + firstSourceTimestamp = timestamp; + } + const LONGLONG sourceTimestamp = std::max(0, timestamp - firstSourceTimestamp); + const double sourceTimestampMs = static_cast(sourceTimestamp) / 10'000.0; + double mappedOutputMs = sourceTimestampMs; + if (!sourceToOutputMs(timelineSegments_, mappedOutputMs, mappedOutputMs)) { + continue; + } + const LONGLONG outputTimestamp = + static_cast(std::llround(mappedOutputMs * 10'000.0)); + if ( + (timelineSegments_.empty() && outputTimestamp >= maxOutputTimestamp) || + (!timelineSegments_.empty() && outputTimestamp > maxOutputTimestamp + outputFrameDuration) + ) { + break; + } + const LONGLONG sampleWindowEnd = outputTimestamp + (outputFrameDuration / 2); + const bool timelineSampleNearEnd = + !timelineSegments_.empty() && + sourceTimestampMs >= + (timelineSegments_.back().sourceEndMs - std::max(1.0, 2000.0 / options_.fps)); + const UINT expectedFramesForSample = timelineSegments_.empty() + ? expectedOutputFrames + : timelineSampleNearEnd + ? expectedOutputFrames + : std::min( + expectedOutputFrames, + static_cast(std::floor( + (static_cast(outputTimestamp) / 10'000'000.0) * + static_cast(options_.fps))) + 1); + if (sampleWindowEnd < nextOutputTimestamp) { + continue; + } + + ComPtr buffer; + hr = sample->GetBufferByIndex(0, &buffer); + if (!succeeded(hr, "IMFSample::GetBufferByIndex")) { + return false; + } + + ComPtr dxgiBuffer; + hr = buffer.As(&dxgiBuffer); + if (FAILED(hr)) { + ID3D11Texture2D* uploadedSourceTexture = nullptr; + if (options_.preferHighPerformanceAdapter) { + if (!uploadBgraBufferToTexture( + buffer.Get(), + sourceUploadBgraTexture_, + sourceWidth_, + sourceHeight_, + "Create source BGRA upload texture")) { + return false; + } + uploadedSourceTexture = sourceUploadBgraTexture_.Get(); + } else { + if (!uploadNv12BufferToTexture( + buffer.Get(), + sourceUploadTexture_, + sourceWidth_, + sourceHeight_, + "Create source NV12 upload texture")) { + return false; + } + uploadedSourceTexture = sourceUploadTexture_.Get(); + } + + while ( + frameIndex < maxFrames && + frameIndex < expectedOutputFrames && + ( + timelineSegments_.empty() + ? (nextOutputTimestamp <= sampleWindowEnd && + nextOutputTimestamp < maxOutputTimestamp) + : (frameIndex < expectedFramesForSample) + ) + ) { + const LONGLONG overlayTimestamp = timelineSegments_.empty() + ? nextOutputTimestamp + : static_cast(std::llround( + outputToSourceMs( + timelineSegments_, + static_cast(nextOutputTimestamp) / 10'000.0) * + 10'000.0)); + const Timer processTimer; + if (options_.shaderComposite) { + if (!convertSourceTextureToBgra(uploadedSourceTexture, 0, frameIndex)) { + return false; + } + if (!readWebcamFrameForTimestamp(overlayTimestamp, frameIndex)) { + return false; + } + if (!renderShaderComposite(overlayTimestamp)) { + return false; + } + if (!convertBgraToNv12(frameIndex)) { + return false; + } + } else if (!convertSourceTextureToNv12(uploadedSourceTexture, 0, frameIndex)) { + return false; + } + processMs += processTimer.elapsedMs(); + + const Timer writeTimer; + lastOutputTimestamp = nextOutputTimestamp; + if (!writeFrame(frameIndex, nextOutputTimestamp, outputFrameDuration)) { + return false; + } + writeMs += writeTimer.elapsedMs(); + frameIndex++; + nextOutputTimestamp += outputFrameDuration; + emitProgress(std::min(frameIndex, expectedOutputFrames), expectedOutputFrames); + } + + continue; + } + sawDxgiSurface = true; + + ComPtr sourceTexture; + hr = dxgiBuffer->GetResource(IID_PPV_ARGS(&sourceTexture)); + if (!succeeded(hr, "IMFDXGIBuffer::GetResource")) { + return false; + } + UINT subresourceIndex = 0; + dxgiBuffer->GetSubresourceIndex(&subresourceIndex); + + while ( + frameIndex < maxFrames && + frameIndex < expectedOutputFrames && + ( + timelineSegments_.empty() + ? (nextOutputTimestamp <= sampleWindowEnd && + nextOutputTimestamp < maxOutputTimestamp) + : (frameIndex < expectedFramesForSample) + ) + ) { + const LONGLONG overlayTimestamp = timelineSegments_.empty() + ? nextOutputTimestamp + : static_cast(std::llround( + outputToSourceMs( + timelineSegments_, + static_cast(nextOutputTimestamp) / 10'000.0) * + 10'000.0)); + const Timer processTimer; + if (options_.shaderComposite) { + if (!convertSourceTextureToBgra(sourceTexture.Get(), subresourceIndex, frameIndex)) { + return false; + } + if (!readWebcamFrameForTimestamp(overlayTimestamp, frameIndex)) { + return false; + } + if (!renderShaderComposite(overlayTimestamp)) { + return false; + } + if (!convertBgraToNv12(frameIndex)) { + return false; + } + } else { + if (!convertSourceTextureToNv12(sourceTexture.Get(), subresourceIndex, frameIndex)) { + return false; + } + } + processMs += processTimer.elapsedMs(); + + const Timer writeTimer; + lastOutputTimestamp = nextOutputTimestamp; + if (!writeFrame(frameIndex, nextOutputTimestamp, outputFrameDuration)) { + return false; + } + writeMs += writeTimer.elapsedMs(); + ++frameIndex; + emitProgress(std::min(frameIndex, expectedOutputFrames), expectedOutputFrames); + nextOutputTimestamp += outputFrameDuration; + } + } + emitProgress(std::min(frameIndex, expectedOutputFrames), expectedOutputFrames, true); + + const Timer finalizeTimer; + const bool finalized = options_.nvencSdk ? finalizeNvencSdk() : SUCCEEDED(sinkWriter_->Finalize()); + const double finalizeMs = finalizeTimer.elapsedMs(); + if (!finalized) { + if (!options_.nvencSdk) { + std::cerr << "ERROR: IMFSinkWriter::Finalize failed" << std::endl; + } + return false; + } + + const double totalMs = totalTimer.elapsedMs(); + const double mediaMs = + (static_cast(lastOutputTimestamp) / 10'000.0) + + (1000.0 / static_cast(options_.fps)); + const double realtime = mediaMs / totalMs; + std::cout + << "{" + << "\"success\":true," + << "\"mode\":\"source-video\"," + << "\"shaderComposite\":" << (options_.shaderComposite ? "true" : "false") << "," + << "\"webcamOverlay\":" << (hasWebcamOverlay() ? "true" : "false") << "," + << "\"cursorOverlay\":" << (hasCursorOverlay() ? "true" : "false") << "," + << "\"cursorAtlas\":" << (hasCursorAtlas_ ? "true" : "false") << "," + << "\"zoomOverlay\":" << (hasZoomOverlay() ? "true" : "false") << "," + << "\"timelineMap\":" << (!timelineSegments_.empty() ? "true" : "false") << "," + << "\"timelineSegments\":" << timelineSegments_.size() << "," + << "\"gpuDecodeSurface\":" << (sawDxgiSurface ? "true" : "false") << "," + << "\"sourceWidth\":" << sourceWidth_ << "," + << "\"sourceHeight\":" << sourceHeight_ << "," + << "\"width\":" << options_.width << "," + << "\"height\":" << options_.height << "," + << "\"fps\":" << options_.fps << "," + << "\"surfacePoolSize\":" << options_.surfacePoolSize << "," + << "\"adapterIndex\":" << selectedAdapterIndex_ << "," + << "\"adapterVendorId\":" << selectedAdapterVendorId_ << "," + << "\"adapterDeviceId\":" << selectedAdapterDeviceId_ << "," + << "\"adapterDedicatedVideoMemoryMB\":" << selectedAdapterDedicatedVideoMemoryMB_ << "," + << "\"frames\":" << frameIndex << "," + << "\"mediaMs\":" << mediaMs << "," + << "\"initializeMs\":" << initializeMs_ << "," + << "\"initCoInitializeMs\":" << initCoInitializeMs_ << "," + << "\"initMfStartupMs\":" << initMfStartupMs_ << "," + << "\"initD3DDeviceMs\":" << initD3DDeviceMs_ << "," + << "\"initSourceReaderMs\":" << initSourceReaderMs_ << "," + << "\"initWebcamReaderMs\":" << initWebcamReaderMs_ << "," + << "\"initVideoProcessorMs\":" << initVideoProcessorMs_ << "," + << "\"initTexturesMs\":" << initTexturesMs_ << "," + << "\"initShaderPipelineMs\":" << initShaderPipelineMs_ << "," + << "\"initSinkWriterMs\":" << initSinkWriterMs_ << "," + << "\"encoderBackend\":\"" << (options_.nvencSdk ? "nvenc-sdk-d3d11" : "media-foundation") << "\"," + << "\"encoderTuningApplied\":" << (encoderTuningApplied_ ? "true" : "false") << "," + << "\"nvencOutputBytes\":" << nvencOutputBytes_ << "," + << "\"totalMs\":" << totalMs << "," + << "\"readMs\":" << readMs << "," + << "\"videoProcessMs\":" << processMs << "," + << "\"writeSampleMs\":" << writeMs << "," + << "\"finalizeMs\":" << finalizeMs << "," + << "\"realtimeMultiplier\":" << realtime + << "}" << std::endl; + return true; + } + + Options options_; + UINT sourceWidth_ = 1920; + UINT sourceHeight_ = 1080; + double initializeMs_ = 0.0; + double initCoInitializeMs_ = 0.0; + double initMfStartupMs_ = 0.0; + double initD3DDeviceMs_ = 0.0; + double initSourceReaderMs_ = 0.0; + double initWebcamReaderMs_ = 0.0; + double initVideoProcessorMs_ = 0.0; + double initTexturesMs_ = 0.0; + double initShaderPipelineMs_ = 0.0; + double initSinkWriterMs_ = 0.0; + UINT selectedAdapterVendorId_ = 0; + UINT selectedAdapterDeviceId_ = 0; + UINT64 selectedAdapterDedicatedVideoMemoryMB_ = 0; + int selectedAdapterIndex_ = -1; + bool coInitialized_ = false; + bool mfStarted_ = false; + DWORD streamIndex_ = 0; + ComPtr device_; + ComPtr deviceContext_; + ComPtr videoDevice_; + ComPtr videoContext_; + ComPtr deviceManager_; + ComPtr sourceReader_; + ComPtr webcamReader_; + ComPtr videoProcessorEnumerator_; + ComPtr videoProcessor_; + ComPtr bgraVideoProcessorEnumerator_; + ComPtr bgraVideoProcessor_; + ComPtr webcamVideoProcessorEnumerator_; + ComPtr webcamVideoProcessor_; + ComPtr bgraTexture_; + ComPtr bgraRenderTargetView_; + ComPtr bgraInputView_; + ComPtr contentTexture_; + ComPtr contentOutputView_; + ComPtr contentShaderResourceView_; + ComPtr webcamTexture_; + ComPtr webcamOutputView_; + ComPtr webcamShaderResourceView_; + ComPtr backgroundShaderResourceView_; + ComPtr cursorAtlasShaderResourceView_; + ComPtr vertexShader_; + ComPtr pixelShader_; + ComPtr samplerState_; + ComPtr compositorConstants_; + UINT backgroundImageWidth_ = 1; + UINT backgroundImageHeight_ = 1; + bool hasBackgroundImage_ = false; + UINT cursorAtlasWidth_ = 1; + UINT cursorAtlasHeight_ = 1; + bool hasCursorAtlas_ = false; + std::array cursorAtlasEntries_; + UINT webcamWidth_ = 640; + UINT webcamHeight_ = 480; + bool webcamFrameReady_ = false; + bool webcamEnded_ = false; + LONGLONG webcamFirstTimestamp_ = -1; + LONGLONG pendingWebcamTimestamp_ = 0; + ComPtr pendingWebcamSample_; + std::vector cursorSamples_; + std::vector zoomSamples_; + ComPtr sourceUploadTexture_; + ComPtr webcamUploadTexture_; + ComPtr sourceUploadBgraTexture_; + ComPtr webcamUploadBgraTexture_; + std::vector uploadScratch_; + std::vector uploadPaddedScratch_; + std::vector timelineSegments_; + std::vector> nv12Textures_; + std::vector> nv12OutputViews_; + std::vector> bgraNv12OutputViews_; + ComPtr sinkWriter_; +#ifdef RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK + std::unique_ptr nvencEncoder_; +#endif + FILE* nvencOutputFile_ = nullptr; + uint64_t nvencOutputBytes_ = 0; + bool encoderTuningApplied_ = false; +}; + +} // namespace + +int wmain(int argc, wchar_t** argv) { + const Options options = parseOptions(argc, argv); + GpuProbe probe; + if (!probe.initialize(options)) { + std::cerr << "{\"success\":false,\"phase\":\"initialize\"}" << std::endl; + return 1; + } + if (!probe.run()) { + std::cerr << "{\"success\":false,\"phase\":\"run\"}" << std::endl; + return 1; + } + return 0; +} diff --git a/electron/native/nvidia-cuda-compositor/CMakeLists.txt b/electron/native/nvidia-cuda-compositor/CMakeLists.txt new file mode 100644 index 000000000..e79d9630c --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.24) + +project(recordly_nvidia_cuda_compositor LANGUAGES CXX CUDA) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CUDA_STANDARD 17) +set(CMAKE_CUDA_STANDARD_REQUIRED ON) + +set(RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT + "${CMAKE_CURRENT_LIST_DIR}/../../../.tmp/video-sdk-samples" + CACHE PATH + "Path to the NVIDIA Video Codec SDK samples checkout" +) +set(NVIDIA_SAMPLES_DIR "${RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT}/Samples") +set(NVCODEC_DIR "${NVIDIA_SAMPLES_DIR}/NvCodec") + +if(NOT EXISTS "${NVCODEC_DIR}") + message(FATAL_ERROR "NVIDIA Video Codec SDK NvCodec directory not found: ${NVCODEC_DIR}. Set RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT.") +endif() + +add_executable(recordly-nvidia-cuda-compositor + src/main.cu + "${NVCODEC_DIR}/NvDecoder/NvDecoder.cpp" + "${NVCODEC_DIR}/NvEncoder/NvEncoder.cpp" + "${NVCODEC_DIR}/NvEncoder/NvEncoderCuda.cpp" +) + +target_include_directories(recordly-nvidia-cuda-compositor PRIVATE + "${NVIDIA_SAMPLES_DIR}" + "${NVCODEC_DIR}" +) + +target_compile_definitions(recordly-nvidia-cuda-compositor PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN +) + +find_package(CUDAToolkit REQUIRED) +target_link_libraries(recordly-nvidia-cuda-compositor PRIVATE + CUDA::cuda_driver + "${NVCODEC_DIR}/Lib/x64/nvcuvid.lib" +) + +set_target_properties(recordly-nvidia-cuda-compositor PROPERTIES + CUDA_SEPARABLE_COMPILATION OFF +) diff --git a/electron/native/nvidia-cuda-compositor/render-tahoe-cursor-atlas.cjs b/electron/native/nvidia-cuda-compositor/render-tahoe-cursor-atlas.cjs new file mode 100644 index 000000000..329831bc6 --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/render-tahoe-cursor-atlas.cjs @@ -0,0 +1,145 @@ +const { app, BrowserWindow, ipcMain } = require("electron"); +const path = require("node:path"); + +const drawHeight = 256; +const padding = 2; +const cursorTypes = [ + "arrow", + "text", + "pointer", + "crosshair", + "open-hand", + "closed-hand", + "resize-ew", + "resize-ns", + "not-allowed", +]; +const tahoeAssets = { + arrow: ["pointer-1__14-6.svg", 0.14, 0.06], + text: ["ibeam-1__50-44.svg", 0.5, 0.44], + pointer: ["../golden-gate/pointinghand.svg", 12 / 32, 8 / 32], + crosshair: ["crosshair-1__50-50.svg", 0.5, 0.5], + "open-hand": ["../golden-gate/openhand.svg", 15 / 32, 16 / 32], + "closed-hand": ["../golden-gate/closedhand.svg", 15 / 32, 16 / 32], + "resize-ew": ["resizeeastwest-1__50-50.svg", 0.5, 0.5], + "resize-ns": ["resizenorthsouth-1__50-49.svg", 0.5, 0.49], + "not-allowed": ["notallowed-1__23-0.svg", 0.23, 0], +}; + +function arg(name, fallback = "") { + const index = process.argv.indexOf(name); + return index >= 0 && index + 1 < process.argv.length ? process.argv[index + 1] : fallback; +} + +const repoRoot = arg("--repo-root"); +const atlasRgbaPath = arg("--output-rgba"); +const atlasMetadataPath = arg("--output-metadata"); + +if (!repoRoot || !atlasRgbaPath || !atlasMetadataPath) { + console.error( + "Usage: electron render-tahoe-cursor-atlas.cjs --repo-root --output-rgba --output-metadata ", + ); + process.exit(1); +} + +const assets = cursorTypes.map((type, index) => { + const [fileName, anchorX, anchorY] = tahoeAssets[type]; + return { + type, + index, + filePath: path.join(repoRoot, "src", "assets", "cursors", "tahoe", fileName), + anchorX, + anchorY, + }; +}); + +app.disableHardwareAcceleration(); + +app.whenReady().then(async () => { + const window = new BrowserWindow({ + show: false, + width: 1, + height: 1, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + backgroundThrottling: false, + }, + }); + + ipcMain.once("atlas-ready", (_event, result) => { + console.log(JSON.stringify(result)); + app.quit(); + }); + + const html = ` + + +`; + + await window.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html)); +}); diff --git a/electron/native/nvidia-cuda-compositor/run-mp4-pipeline.mjs b/electron/native/nvidia-cuda-compositor/run-mp4-pipeline.mjs new file mode 100644 index 000000000..43324b051 --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/run-mp4-pipeline.mjs @@ -0,0 +1,1600 @@ +import { spawn, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); +const repoRoot = resolve(scriptDir, "..", "..", ".."); +const unpackAsarPath = (candidate) => candidate.replace(/\.asar([/\\])/, ".asar.unpacked$1"); + +function resolvePackageBinary(moduleName) { + try { + const moduleExports = require(moduleName); + const candidate = + typeof moduleExports === "string" + ? moduleExports + : typeof moduleExports?.path === "string" + ? moduleExports.path + : typeof moduleExports?.default === "string" + ? moduleExports.default + : typeof moduleExports?.default?.path === "string" + ? moduleExports.default.path + : ""; + if (!candidate) { + return ""; + } + + const unpacked = unpackAsarPath(candidate); + return existsSync(unpacked) ? unpacked : candidate; + } catch { + return ""; + } +} + +function resolveToolCommand(envNames, moduleName, fallbackName) { + for (const envName of envNames) { + const configured = process.env[envName]; + if (configured) { + return configured; + } + } + + return resolvePackageBinary(moduleName) || fallbackName; +} + +const ffmpegCommand = resolveToolCommand(["RECORDLY_FFMPEG_EXE"], "ffmpeg-static", "ffmpeg"); +const ffprobeCommand = resolveToolCommand(["RECORDLY_FFPROBE_EXE"], "ffprobe-static", "ffprobe"); + +const cursorTypes = [ + "arrow", + "text", + "pointer", + "crosshair", + "open-hand", + "closed-hand", + "resize-ew", + "resize-ns", + "not-allowed", +]; +const cursorTypeIndexes = new Map(cursorTypes.map((type, index) => [type, index])); + +function fail(message) { + throw new Error(message); +} + +function getArg(name, fallback = "") { + const index = process.argv.indexOf(name); + if (index === -1) { + return fallback; + } + if (index + 1 >= process.argv.length) { + fail(`Missing value for ${name}`); + } + return process.argv[index + 1]; +} + +function getNumberArg(name, fallback) { + const value = getArg(name, ""); + if (!value) { + return fallback; + } + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0) { + fail(`Invalid ${name}: ${value}`); + } + return parsed; +} + +function getNonNegativeNumberArg(name, fallback) { + const value = getArg(name, ""); + if (!value) { + return fallback; + } + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0) { + fail(`Invalid ${name}: ${value}`); + } + return parsed; +} + +function getFiniteNumberArg(name, fallback) { + const value = getArg(name, ""); + if (!value) { + return fallback; + } + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + fail(`Invalid ${name}: ${value}`); + } + return parsed; +} + +function hasArg(name) { + return process.argv.includes(name); +} + +function shouldRaiseChildPriority() { + return process.env.RECORDLY_NVIDIA_CUDA_EXPORT_HIGH_PRIORITY !== "0"; +} + +function raiseChildPriority(child, label) { + if (!shouldRaiseChildPriority() || !child.pid) { + return false; + } + + try { + os.setPriority(child.pid, os.constants.priority.PRIORITY_HIGH); + return true; + } catch (error) { + console.warn(`[nvidia-cuda-export] Failed to raise ${label} priority: ${error}`); + return false; + } +} + +function run(command, args, options = {}) { + const startedAt = performance.now(); + const result = spawnSync(command, args, { + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + windowsHide: true, + ...options, + }); + const elapsedMs = performance.now() - startedAt; + if (result.error) { + fail(`${command} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const stderr = result.stderr?.trim(); + const stdout = result.stdout?.trim(); + fail( + `${command} exited with ${result.status}` + + (stderr ? `\nSTDERR:\n${stderr}` : "") + + (stdout ? `\nSTDOUT:\n${stdout}` : ""), + ); + } + return { + elapsedMs, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + }; +} + +function runAsync(command, args, options = {}) { + const startedAt = performance.now(); + + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn(command, args, { + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"], + ...options, + }); + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + child.once("error", (error) => { + rejectPromise(new Error(`${command} failed to start: ${error.message}`)); + }); + child.once("close", (status, signal) => { + const elapsedMs = performance.now() - startedAt; + if (status !== 0) { + const suffix = signal ? ` (signal ${signal})` : ""; + rejectPromise( + new Error( + `${command} exited with ${status}${suffix}` + + (stderr.trim() ? `\nSTDERR:\n${stderr.trim()}` : "") + + (stdout.trim() ? `\nSTDOUT:\n${stdout.trim()}` : ""), + ), + ); + return; + } + + resolvePromise({ elapsedMs, stdout, stderr }); + }); + }); +} + +function parseProgressTimeSeconds(text) { + const matches = [...text.matchAll(/\btime=(\d+):(\d+):(\d+(?:\.\d+)?)/g)]; + const latest = matches.at(-1); + if (!latest) { + return null; + } + + const hours = Number(latest[1]); + const minutes = Number(latest[2]); + const seconds = Number(latest[3]); + if (![hours, minutes, seconds].every(Number.isFinite)) { + return null; + } + + return hours * 3600 + minutes * 60 + seconds; +} + +function runWithProgress(command, args, progressOptions, options = {}) { + const startedAt = performance.now(); + const { totalDurationSec, totalFrames, startPercentage, endPercentage, stage } = + progressOptions; + + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn(command, args, { + windowsHide: true, + ...options, + }); + let stdout = ""; + let stderr = ""; + let lastEmittedPercentage = startPercentage; + + const maybeEmitProgress = (text) => { + if (!Number.isFinite(totalDurationSec) || totalDurationSec <= 0) { + return; + } + const elapsedSec = parseProgressTimeSeconds(text); + if (elapsedSec === null) { + return; + } + const ratio = Math.max(0, Math.min(1, elapsedSec / totalDurationSec)); + const percentage = startPercentage + (endPercentage - startPercentage) * ratio; + if (percentage >= lastEmittedPercentage + 0.25 || percentage >= endPercentage) { + lastEmittedPercentage = Math.min(endPercentage, percentage); + emitPreparationProgress(totalFrames, lastEmittedPercentage, stage); + } + }; + + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + const text = chunk.toString(); + stderr += text; + maybeEmitProgress(text); + }); + child.once("error", (error) => { + rejectPromise(new Error(`${command} failed to start: ${error.message}`)); + }); + child.once("close", (status, signal) => { + const elapsedMs = performance.now() - startedAt; + if (status !== 0) { + const suffix = signal ? ` (signal ${signal})` : ""; + rejectPromise( + new Error( + `${command} exited with ${status}${suffix}` + + (stderr.trim() ? `\nSTDERR:\n${stderr.trim()}` : "") + + (stdout.trim() ? `\nSTDOUT:\n${stdout.trim()}` : ""), + ), + ); + return; + } + + emitPreparationProgress(totalFrames, endPercentage, stage); + resolvePromise({ elapsedMs, stdout, stderr }); + }); + }); +} + +function emitPreparationProgress(totalFrames, percentage, stage) { + const progressStage = stage ?? "preparing"; + const finalizing = progressStage === "finalizing"; + const payload = { + currentFrame: finalizing ? Math.max(1, Math.floor(totalFrames)) : 0, + totalFrames: Math.max(1, Math.floor(totalFrames)), + percentage: Number(Math.min(99, Math.max(0, percentage)).toFixed(2)), + stage: progressStage, + }; + process.stderr.write(`PROGRESS ${JSON.stringify(payload)}\n`); +} + +function parseFfmpegStatsFrameCount(stderr) { + const matches = [...String(stderr ?? "").matchAll(/frame=\s*(\d+)/g)]; + if (!matches.length) { + return 0; + } + const frameCount = Number(matches[matches.length - 1][1]); + return Number.isFinite(frameCount) && frameCount > 0 ? Math.floor(frameCount) : 0; +} + +const gpuSampleFields = [ + "timestamp", + "temperature.gpu", + "power.draw", + "pstate", + "utilization.gpu", + "utilization.decoder", + "utilization.encoder", + "clocks.sm", + "clocks.mem", +]; + +const gpuThrottleReasonFields = [ + { key: "gpuIdle", query: "clocks_throttle_reasons.gpu_idle" }, + { + key: "applicationsClocksSetting", + query: "clocks_throttle_reasons.applications_clocks_setting", + }, + { key: "swPowerCap", query: "clocks_throttle_reasons.sw_power_cap" }, + { key: "hwSlowdown", query: "clocks_throttle_reasons.hw_slowdown" }, + { key: "hwThermalSlowdown", query: "clocks_throttle_reasons.hw_thermal_slowdown" }, + { + key: "hwPowerBrakeSlowdown", + query: "clocks_throttle_reasons.hw_power_brake_slowdown", + }, + { key: "swThermalSlowdown", query: "clocks_throttle_reasons.sw_thermal_slowdown" }, + { key: "syncBoost", query: "clocks_throttle_reasons.sync_boost" }, +]; + +function runNvidiaSmiGpuQuery(fields) { + const result = spawnSync( + "nvidia-smi", + [`--query-gpu=${fields.join(",")}`, "--format=csv,noheader,nounits"], + { encoding: "utf8", windowsHide: true }, + ); + if (result.status !== 0 || !result.stdout.trim()) { + return null; + } + return result.stdout + .trim() + .split(",") + .map((part) => part.trim()); +} + +function parseNvidiaSmiNumber(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function parseNvidiaSmiActiveFlag(value) { + if (/^active$/i.test(value)) { + return true; + } + if (/^not active$/i.test(value)) { + return false; + } + return null; +} + +function sampleGpu() { + const throttleFields = [ + "clocks_throttle_reasons.active", + ...gpuThrottleReasonFields.map((field) => field.query), + ]; + const parts = + runNvidiaSmiGpuQuery([...gpuSampleFields, ...throttleFields]) ?? + runNvidiaSmiGpuQuery(gpuSampleFields); + if (!parts) { + return null; + } + + const sample = { + timestamp: parts[0], + temperatureC: parseNvidiaSmiNumber(parts[1]), + powerW: parseNvidiaSmiNumber(parts[2]), + pstate: parts[3], + gpuUtilizationPct: parseNvidiaSmiNumber(parts[4]), + decoderUtilizationPct: parseNvidiaSmiNumber(parts[5]), + encoderUtilizationPct: parseNvidiaSmiNumber(parts[6]), + smClockMhz: parseNvidiaSmiNumber(parts[7]), + memoryClockMhz: parseNvidiaSmiNumber(parts[8]), + }; + if (parts.length >= gpuSampleFields.length + throttleFields.length) { + const throttleStart = gpuSampleFields.length; + const clockThrottleReasons = {}; + for (const [index, field] of gpuThrottleReasonFields.entries()) { + const active = parseNvidiaSmiActiveFlag(parts[throttleStart + 1 + index]); + if (active !== null) { + clockThrottleReasons[field.key] = active; + } + } + sample.clockThrottleReasonsActiveMask = parts[throttleStart]; + sample.clockThrottleReasons = clockThrottleReasons; + sample.activeClockThrottleReasons = Object.entries(clockThrottleReasons) + .filter(([, active]) => active) + .map(([key]) => key); + } + return sample; +} + +function summarizeGpuSamples(samples) { + if (!samples.length) { + return null; + } + const numeric = [ + "temperatureC", + "powerW", + "gpuUtilizationPct", + "decoderUtilizationPct", + "encoderUtilizationPct", + "smClockMhz", + "memoryClockMhz", + ]; + const summary = { + samples: samples.length, + pstateValues: [...new Set(samples.map((sample) => sample.pstate))], + }; + for (const key of numeric) { + const values = samples.map((sample) => sample[key]).filter(Number.isFinite); + if (!values.length) { + continue; + } + summary[key] = { + min: Math.min(...values), + max: Math.max(...values), + avg: Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(2)), + }; + } + const activeMasks = [ + ...new Set( + samples + .map((sample) => sample.clockThrottleReasonsActiveMask) + .filter((mask) => typeof mask === "string" && mask.length > 0), + ), + ]; + if (activeMasks.length) { + summary.clockThrottleReasonsActiveMasks = activeMasks; + } + const throttleReasonCounts = {}; + for (const sample of samples) { + for (const reason of sample.activeClockThrottleReasons ?? []) { + throttleReasonCounts[reason] = (throttleReasonCounts[reason] ?? 0) + 1; + } + } + if (Object.keys(throttleReasonCounts).length) { + summary.clockThrottleReasonCounts = throttleReasonCounts; + } + return summary; +} + +function cursorBounceScale(interactionType, ageMs, durationMs = 180) { + if (!["click", "double-click", "right-click", "middle-click"].includes(interactionType)) { + return 1; + } + if (ageMs < 0 || ageMs > durationMs) { + return 1; + } + const progress = 1 - ageMs / durationMs; + return Math.max(0.72, 1 - Math.sin(progress * Math.PI) * 0.08); +} + +function latestClickSample(samples, sampleIndex) { + for (let index = sampleIndex; index >= 0; index -= 1) { + const sample = samples[index]; + if ( + ["click", "double-click", "right-click", "middle-click"].includes( + sample?.interactionType, + ) + ) { + return sample; + } + } + return null; +} + +function writeCursorSamples(cursorPayload, outputPath) { + const samples = Array.isArray(cursorPayload.samples) ? cursorPayload.samples : []; + const cursorLines = samples + .map((sample, index) => { + if ( + !Number.isFinite(sample?.timeMs) || + !Number.isFinite(sample?.cx) || + !Number.isFinite(sample?.cy) + ) { + return null; + } + const clickSample = latestClickSample(samples, index); + const bounceScale = Number.isFinite(sample.bounceScale) + ? sample.bounceScale + : clickSample + ? cursorBounceScale( + clickSample.interactionType, + sample.timeMs - clickSample.timeMs, + ) + : 1; + return [ + sample.timeMs, + sample.cx, + sample.cy, + cursorTypeIndexes.get(sample.cursorType) ?? + (Number.isFinite(sample.cursorTypeIndex) + ? Math.max(0, Math.min(8, Math.round(sample.cursorTypeIndex))) + : 0), + Number(bounceScale.toFixed(4)), + sample.visible === false ? 0 : 1, + ].join("\t"); + }) + .filter(Boolean) + .join("\n"); + writeFileSync(outputPath, cursorLines ? `${cursorLines}\n` : ""); + return samples.length; +} + +function renderTahoeCursorAtlas(workDir) { + const rgbaPath = join(workDir, "tahoe-cursor-atlas.rgba"); + const metadataPath = join(workDir, "tahoe-cursor-atlas.tsv"); + const electronPath = require("electron"); + const render = run( + electronPath, + [ + join(scriptDir, "render-tahoe-cursor-atlas.cjs"), + "--repo-root", + repoRoot, + "--output-rgba", + rgbaPath, + "--output-metadata", + metadataPath, + ], + { + env: { + ...process.env, + ELECTRON_DISABLE_SECURITY_WARNINGS: "1", + }, + }, + ); + const resultLine = render.stdout.trim().split(/\r?\n/).filter(Boolean).pop(); + if (!resultLine) { + fail("Cursor atlas renderer did not report a result"); + } + const result = JSON.parse(resultLine); + if (result.error) { + fail(`Cursor atlas renderer failed: ${result.error}`); + } + return { + rgbaPath, + metadataPath, + width: result.width, + height: result.height, + entries: result.entries, + elapsedMs: render.elapsedMs, + }; +} + +function prepareExternalCursorAtlas(workDir, pngPath, metadataPath) { + const startedAt = performance.now(); + const resolvedPngPath = resolve(pngPath); + const resolvedMetadataPath = resolve(metadataPath); + if (!existsSync(resolvedPngPath)) { + fail(`Cursor atlas PNG does not exist: ${resolvedPngPath}`); + } + if (!existsSync(resolvedMetadataPath)) { + fail(`Cursor atlas metadata does not exist: ${resolvedMetadataPath}`); + } + + const json = ffprobeJson([ + "-select_streams", + "v:0", + "-show_entries", + "stream=width,height", + resolvedPngPath, + ]); + const stream = json.streams?.[0]; + const width = Number(stream?.width); + const height = Number(stream?.height); + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + fail(`Invalid cursor atlas dimensions: ${resolvedPngPath}`); + } + + const rgbaPath = join(workDir, "external-cursor-atlas.rgba"); + run(ffmpegCommand, [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + resolvedPngPath, + "-f", + "rawvideo", + "-pix_fmt", + "rgba", + rgbaPath, + ]); + const entries = readFileSync(resolvedMetadataPath, "utf8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean).length; + if (entries === 0) { + fail(`Cursor atlas metadata is empty: ${resolvedMetadataPath}`); + } + + return { + rgbaPath, + metadataPath: resolvedMetadataPath, + width, + height, + entries, + elapsedMs: performance.now() - startedAt, + }; +} + +async function runWithGpuMonitor(command, args, sampleIntervalMs) { + const samples = []; + const startedAt = performance.now(); + const shouldSampleGpu = Number.isFinite(sampleIntervalMs) && sampleIntervalMs > 0; + let stdout = ""; + let stderr = ""; + + const child = spawn(command, args, { + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"], + }); + const priorityBoosted = raiseChildPriority(child, "native CUDA encoder"); + + const collectSample = () => { + if (!shouldSampleGpu) { + return; + } + const sample = sampleGpu(); + if (sample) { + samples.push({ + elapsedMs: Number((performance.now() - startedAt).toFixed(2)), + ...sample, + }); + } + }; + collectSample(); + const interval = shouldSampleGpu ? setInterval(collectSample, sampleIntervalMs) : null; + + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + const text = chunk.toString(); + stderr += text; + process.stderr.write(text); + }); + + const status = await new Promise((resolve, reject) => { + child.on("error", reject); + child.on("close", resolve); + }); + if (interval) { + clearInterval(interval); + } + collectSample(); + + const elapsedMs = performance.now() - startedAt; + if (status !== 0) { + fail( + `${command} exited with ${status}` + + (stderr.trim() ? `\nSTDERR:\n${stderr.trim()}` : "") + + (stdout.trim() ? `\nSTDOUT:\n${stdout.trim()}` : ""), + ); + } + return { + elapsedMs, + stdout, + stderr, + gpuSamples: samples, + gpuSummary: summarizeGpuSamples(samples), + priorityBoosted, + }; +} + +function ffprobeJson(args) { + const result = run(ffprobeCommand, ["-v", "error", ...args, "-of", "json"]); + return JSON.parse(result.stdout); +} + +async function ffprobeCsvAsync(args) { + const result = await runAsync(ffprobeCommand, ["-v", "error", ...args, "-of", "csv=p=0"]); + return result.stdout; +} + +function getVideoInfo(inputPath) { + const json = ffprobeJson([ + "-select_streams", + "v:0", + "-show_entries", + "stream=codec_name,width,height,duration,avg_frame_rate,nb_frames", + inputPath, + ]); + let stream = json.streams?.[0]; + if (!stream) { + fail(`No video stream found in ${inputPath}`); + } + if (stream.codec_name !== "h264") { + fail(`The NVIDIA CUDA compositor currently expects H.264 input, got ${stream.codec_name}`); + } + const durationSec = Number(stream.duration); + if (!Number.isFinite(durationSec) || durationSec <= 0) { + fail("ffprobe did not return a valid video duration"); + } + let sourceFrames = Number(stream.nb_frames); + if (!Number.isFinite(sourceFrames) || sourceFrames <= 0) { + const countedJson = ffprobeJson([ + "-count_frames", + "-select_streams", + "v:0", + "-show_entries", + "stream=codec_name,width,height,duration,avg_frame_rate,nb_frames,nb_read_frames", + inputPath, + ]); + stream = countedJson.streams?.[0] ?? stream; + sourceFrames = Number(stream.nb_read_frames || stream.nb_frames); + } + if (!Number.isFinite(sourceFrames) || sourceFrames <= 0) { + fail("ffprobe did not return a valid source frame count"); + } + return { + codec: stream.codec_name, + width: Number(stream.width), + height: Number(stream.height), + durationSec, + sourceFrames, + avgFrameRate: stream.avg_frame_rate, + }; +} + +function normalizeMonotonicTimestamps(timestamps) { + if (timestamps.length < 2) { + return []; + } + + const first = timestamps[0]; + const normalized = timestamps + .map((value) => Math.max(0, value - first)) + .filter((value, index, values) => index === 0 || value >= values[index - 1]); + return normalized.length === timestamps.length ? normalized : []; +} + +function parseTimestampCsv(csv) { + return csv + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const columns = line.split(","); + const value = columns + .map((column) => Number(column.trim())) + .find((candidate) => Number.isFinite(candidate)); + return value ?? Number.NaN; + }) + .filter((value) => Number.isFinite(value)); +} + +function readTimelineSegments(timelineMapPath) { + if (!timelineMapPath) { + return []; + } + const resolvedPath = resolve(timelineMapPath); + if (!existsSync(resolvedPath)) { + fail(`Timeline map does not exist: ${resolvedPath}`); + } + + const segments = readFileSync(resolvedPath, "utf8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const [sourceStartMs, sourceEndMs, outputStartMs, outputEndMs, speed] = line + .split(",") + .map((value) => Number(value.trim())); + return { sourceStartMs, sourceEndMs, outputStartMs, outputEndMs, speed }; + }); + + let outputCursorMs = 0; + for (const segment of segments) { + if ( + !Number.isFinite(segment.sourceStartMs) || + !Number.isFinite(segment.sourceEndMs) || + !Number.isFinite(segment.outputStartMs) || + !Number.isFinite(segment.outputEndMs) || + !Number.isFinite(segment.speed) || + segment.sourceEndMs <= segment.sourceStartMs || + segment.outputEndMs <= segment.outputStartMs || + segment.speed <= 0 + ) { + fail(`Invalid timeline map segment in ${resolvedPath}`); + } + if (Math.abs(segment.outputStartMs - outputCursorMs) > 2) { + fail(`Timeline map output ranges must be contiguous: ${resolvedPath}`); + } + outputCursorMs = segment.outputEndMs; + } + + return segments; +} + +async function getVideoPacketPtsAsync(inputPath, durationSec) { + const csv = await ffprobeCsvAsync([ + "-select_streams", + "v:0", + "-read_intervals", + `%+${durationSec}`, + "-show_packets", + "-show_entries", + "packet=pts_time,dts_time", + inputPath, + ]); + return normalizeMonotonicTimestamps(parseTimestampCsv(csv)); +} + +async function getVideoFramePtsAsync(inputPath, durationSec) { + const csv = await ffprobeCsvAsync([ + "-select_streams", + "v:0", + "-read_intervals", + `%+${durationSec}`, + "-show_frames", + "-show_entries", + "frame=best_effort_timestamp_time", + inputPath, + ]); + return normalizeMonotonicTimestamps(parseTimestampCsv(csv)); +} + +async function writeFramePtsSidecarAsync(inputPath, durationSec, outputPath) { + const startedAt = performance.now(); + let source = "packet-pts"; + let timestamps = await getVideoPacketPtsAsync(inputPath, durationSec); + if (timestamps.length === 0) { + source = "frame-pts"; + timestamps = await getVideoFramePtsAsync(inputPath, durationSec); + } + const elapsedMs = performance.now() - startedAt; + if (timestamps.length === 0) { + return { path: null, frames: 0, elapsedMs, source: "none" }; + } + + writeFileSync(outputPath, timestamps.map((value) => value.toFixed(9)).join("\n")); + return { path: outputPath, frames: timestamps.length, elapsedMs, source }; +} + +function roundedRectMaskExpression({ x, y, width, height, radius }) { + const right = x + width; + const bottom = y + height; + const cornerRight = right - radius; + const cornerBottom = bottom - radius; + const radiusSquared = radius * radius; + const centerBand = `between(X,${x + radius},${cornerRight})*between(Y,${y},${bottom})`; + const middleBand = `between(X,${x},${right})*between(Y,${y + radius},${cornerBottom})`; + const topLeft = `lte((X-${x + radius})*(X-${x + radius})+(Y-${y + radius})*(Y-${y + radius}),${radiusSquared})*lte(X,${x + radius})*lte(Y,${y + radius})`; + const topRight = `lte((X-${cornerRight})*(X-${cornerRight})+(Y-${y + radius})*(Y-${y + radius}),${radiusSquared})*gte(X,${cornerRight})*lte(Y,${y + radius})`; + const bottomLeft = `lte((X-${x + radius})*(X-${x + radius})+(Y-${cornerBottom})*(Y-${cornerBottom}),${radiusSquared})*lte(X,${x + radius})*gte(Y,${cornerBottom})`; + const bottomRight = `lte((X-${cornerRight})*(X-${cornerRight})+(Y-${cornerBottom})*(Y-${cornerBottom}),${radiusSquared})*gte(X,${cornerRight})*gte(Y,${cornerBottom})`; + return `${centerBand}+${middleBand}+${topLeft}+${topRight}+${bottomLeft}+${bottomRight}`; +} + +function createBackgroundFilter(outputSize, shadowOptions, blurPx = 0) { + const safeBlurPx = Math.max(0, Math.min(96, Math.round(Number.isFinite(blurPx) ? blurPx : 0))); + const scaled = `[0:v]scale=${outputSize.width}:${outputSize.height}:force_original_aspect_ratio=increase,crop=${outputSize.width}:${outputSize.height},format=rgba[bg_scaled]`; + const blurFilter = + safeBlurPx > 0 + ? `;[bg_scaled]boxblur=luma_radius=${safeBlurPx}:luma_power=1:chroma_radius=${safeBlurPx}:chroma_power=1:alpha_radius=${safeBlurPx}:alpha_power=1[bg]` + : ";[bg_scaled]null[bg]"; + if (!shadowOptions) { + return { + filterArgs: [ + "-filter_complex", + `${scaled}${blurFilter};[bg]format=nv12[out]`, + "-map", + "[out]", + ], + bakedShadow: false, + backgroundBlur: safeBlurPx, + }; + } + + const shadowAlpha = Math.round(255 * Math.min(0.5, shadowOptions.intensityPct / 200)); + const shadowBlur = Math.max(12, Math.round(shadowOptions.radius * 1.5)); + const mask = roundedRectMaskExpression({ + x: shadowOptions.x, + y: shadowOptions.y, + width: shadowOptions.width, + height: shadowOptions.height, + radius: shadowOptions.radius, + }); + const shadow = `[1:v]format=rgba,geq=r='0':g='0':b='0':a='if(${mask},${shadowAlpha},0)',boxblur=luma_radius=${shadowBlur}:luma_power=1:chroma_radius=${shadowBlur}:chroma_power=1:alpha_radius=${shadowBlur}:alpha_power=1[shadow]`; + return { + filterArgs: [ + "-f", + "lavfi", + "-i", + `color=c=black@0.0:s=${outputSize.width}x${outputSize.height}:d=1`, + "-filter_complex", + `${scaled}${blurFilter};${shadow};[bg][shadow]overlay=format=auto,format=nv12[out]`, + "-map", + "[out]", + ], + bakedShadow: true, + shadowAlpha, + shadowBlur, + backgroundBlur: safeBlurPx, + }; +} + +function parseProbeSummary(stdout) { + const lines = stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + const jsonLine = lines.find((line) => line.startsWith("{") && line.includes('"success"')); + if (!jsonLine) { + fail(`Native probe did not emit a JSON summary:\n${stdout}`); + } + try { + return JSON.parse(jsonLine); + } catch { + // The helper currently prints raw Windows paths with backslashes. + // Keep parsing resilient while this remains a throwaway benchmark tool. + return JSON.parse(jsonLine.replace(/,"outputPath":".*"\}$/, "}")); + } +} + +const inputPath = resolve(getArg("--input")); +const outputPath = resolve( + getArg("--output", join(scriptDir, "recordly-nvdec-nvenc-mp4-output.mp4")), +); +const requestedOutputWidth = Math.round(getNumberArg("--width", 0)); +const requestedOutputHeight = Math.round(getNumberArg("--height", 0)); +const fps = Math.round(getNumberArg("--fps", 30)); +const bitrateMbps = Math.round(getNumberArg("--bitrate-mbps", 18)); +const encodingMode = getArg("--encoding-mode", "balanced"); +if (!["fast", "balanced", "quality"].includes(encodingMode)) { + throw new Error(`Unsupported --encoding-mode: ${encodingMode}`); +} +const workDir = resolve(getArg("--work-dir", join(scriptDir, "mp4-work"))); +const reuseIntermediates = hasArg("--reuse-intermediates"); +const reuseDemux = hasArg("--reuse-demux") || reuseIntermediates; +const sampleGpuDuringEncode = hasArg("--sample-gpu"); +const gpuSampleIntervalMs = Math.round(getNumberArg("--gpu-sample-interval-ms", 1000)); +const streamSync = hasArg("--stream-sync"); +const prewarmMs = Math.round(getNumberArg("--prewarm-ms", 0)); +const maxOutputFrames = Math.round(getNumberArg("--max-output-frames", 0)); +const requestedDurationSec = getNumberArg("--duration-sec", 0); +const chunkMb = Math.round(getNumberArg("--chunk-mb", 4)); +const skipMux = hasArg("--skip-mux"); +const videoOnly = hasArg("--video-only"); +const contentX = Math.round(getNonNegativeNumberArg("--content-x", 0)); +const contentY = Math.round(getNonNegativeNumberArg("--content-y", 0)); +const contentWidth = Math.round(getNumberArg("--content-width", 0)); +const contentHeight = Math.round(getNumberArg("--content-height", 0)); +const sourceCropX = Math.round(getNonNegativeNumberArg("--source-crop-x", 0)); +const sourceCropY = Math.round(getNonNegativeNumberArg("--source-crop-y", 0)); +const sourceCropWidth = Math.round(getNumberArg("--source-crop-width", 0)); +const sourceCropHeight = Math.round(getNumberArg("--source-crop-height", 0)); +const radius = Math.round(getNonNegativeNumberArg("--radius", 0)); +const backgroundY = Math.round(getNonNegativeNumberArg("--background-y", 16)); +const backgroundU = Math.round(getNonNegativeNumberArg("--background-u", 128)); +const backgroundV = Math.round(getNonNegativeNumberArg("--background-v", 128)); +const backgroundImage = getArg("--background-image", ""); +const backgroundBlurPx = getNonNegativeNumberArg("--background-blur", 0); +const backgroundNv12 = getArg("--background-nv12", ""); +const shadowOffsetY = Math.round(getNonNegativeNumberArg("--shadow-offset-y", 0)); +const shadowIntensityPct = Math.round(getNonNegativeNumberArg("--shadow-intensity-pct", 0)); +const webcamInput = getArg("--webcam-input", ""); +const webcamX = Math.round(getNonNegativeNumberArg("--webcam-x", 0)); +const webcamY = Math.round(getNonNegativeNumberArg("--webcam-y", 0)); +const webcamSize = Math.round(getNumberArg("--webcam-size", 0)); +const webcamRadius = Math.round(getNonNegativeNumberArg("--webcam-radius", 0)); +const webcamTimeOffsetMs = getFiniteNumberArg("--webcam-time-offset-ms", 0); +const webcamMirror = hasArg("--webcam-mirror"); +const webcamStream = hasArg("--webcam-stream"); +const cursorJson = getArg("--cursor-json", ""); +const cursorHeight = Math.round(getNumberArg("--cursor-height", 0)); +const cursorStyle = getArg("--cursor-style", "vector"); +const cursorAtlasPng = getArg("--cursor-atlas-png", ""); +const cursorAtlasMetadata = getArg("--cursor-atlas-metadata", ""); +const zoomTelemetry = getArg("--zoom-telemetry", ""); +const timelineMap = getArg("--timeline-map", ""); + +if (!existsSync(inputPath)) { + fail(`Input does not exist: ${inputPath}`); +} +mkdirSync(workDir, { recursive: true }); +mkdirSync(dirname(outputPath), { recursive: true }); + +function resolveNativeProbePath() { + const configuredPath = process.env.RECORDLY_NVIDIA_CUDA_EXPORT_EXE; + const platformArch = process.arch === "arm64" ? "win32-arm64" : "win32-x64"; + const candidates = [ + configuredPath, + join(scriptDir, "build", "Release", "recordly-nvidia-cuda-compositor.exe"), + join( + repoRoot, + "electron", + "native", + "bin", + platformArch, + "recordly-nvidia-cuda-compositor.exe", + ), + // Backward-compatible legacy helper path while old work dirs are being retired. + join(scriptDir, "build", "Release", "recordly-nvdec-nvenc-probe.exe"), + ].filter(Boolean); + + for (const candidate of candidates) { + const unpackedCandidate = unpackAsarPath(candidate); + if (unpackedCandidate !== candidate && existsSync(unpackedCandidate)) { + return unpackedCandidate; + } + if (existsSync(candidate)) { + return candidate; + } + } + + fail(`Native NVIDIA CUDA compositor is not built. Checked: ${candidates.join(", ")}`); +} +const nativeProbe = resolveNativeProbePath(); + +const baseName = basename(inputPath).replace(/\.[^.]+$/, ""); +const webcamBaseName = webcamInput + ? basename(webcamInput).replace(/\.[^.]+$/, "") + : `${baseName}.webcam`; +const annexBPath = join(workDir, `${baseName}.annexb.h264`); +const webcamAnnexBPath = join(workDir, `${webcamBaseName}.annexb.h264`); +const cursorSamplesPath = join(workDir, `${baseName}.cursor.tsv`); +const encodedPath = join(workDir, `${baseName}.mapped-callback.h264`); +const shouldBakeStaticShadow = + Boolean(backgroundImage) && + contentWidth > 0 && + contentHeight > 0 && + shadowOffsetY > 0 && + shadowIntensityPct > 0; +const backgroundSuffixParts = []; +if (shouldBakeStaticShadow) { + backgroundSuffixParts.push(`shadow-${shadowOffsetY}-${shadowIntensityPct}`); +} +if (backgroundBlurPx > 0) { + backgroundSuffixParts.push(`blur-${Math.round(backgroundBlurPx)}`); +} +const backgroundSuffix = backgroundSuffixParts.length ? `.${backgroundSuffixParts.join(".")}` : ""; +const generatedBackgroundNv12Path = join(workDir, `${baseName}${backgroundSuffix}.background.nv12`); +const generatedWebcamNv12Path = join( + workDir, + `${baseName}.webcam-${webcamSize}${webcamMirror ? "-mirror" : ""}.nv12`, +); +const sourcePtsPath = join(workDir, `${baseName}.source-pts.csv`); + +const videoInfo = getVideoInfo(inputPath); +const webcamInfo = webcamInput ? getVideoInfo(webcamInput) : null; +if (requestedOutputWidth > 0 !== requestedOutputHeight > 0) { + fail("--width and --height must be specified together"); +} +if ( + requestedOutputWidth > 0 && + (requestedOutputWidth % 2 !== 0 || requestedOutputHeight % 2 !== 0) +) { + fail("--width and --height must be even numbers for NV12 encoding"); +} +const outputWidth = requestedOutputWidth > 0 ? requestedOutputWidth : videoInfo.width; +const outputHeight = requestedOutputHeight > 0 ? requestedOutputHeight : videoInfo.height; +const timelineSegments = readTimelineSegments(timelineMap); +const timelineOutputDurationSec = timelineSegments.length + ? Math.max(...timelineSegments.map((segment) => segment.outputEndMs)) / 1000 + : 0; +const durationSec = + requestedDurationSec > 0 + ? timelineSegments.length + ? requestedDurationSec + : Math.min(videoInfo.durationSec, requestedDurationSec) + : timelineSegments.length + ? timelineOutputDurationSec + : videoInfo.durationSec; +if (timelineSegments.length && Math.abs(durationSec - timelineOutputDurationSec) > 0.05) { + fail( + `Timeline map output duration ${timelineOutputDurationSec.toFixed( + 3, + )}s does not match requested duration ${durationSec.toFixed(3)}s`, + ); +} +const timelineSourceDurationSec = timelineSegments.length + ? Math.max(...timelineSegments.map((segment) => segment.sourceEndMs)) / 1000 + : durationSec; +if (timelineSourceDurationSec > videoInfo.durationSec + 0.1) { + fail( + `Timeline map source duration ${timelineSourceDurationSec.toFixed( + 3, + )}s exceeds input duration ${videoInfo.durationSec.toFixed(3)}s`, + ); +} +const sourceDurationSec = Math.min(videoInfo.durationSec, timelineSourceDurationSec); +const webcamSourceDurationSec = webcamInfo + ? Math.min(sourceDurationSec, webcamInfo.durationSec) + : sourceDurationSec; +const targetFrames = Math.ceil(durationSec * fps); +emitPreparationProgress(targetFrames, 1); +const pipelineStartedAt = performance.now(); +const zeroElapsed = () => ({ elapsedMs: 0, stdout: "", stderr: "" }); +let sourceWindowFrames = Math.max( + 1, + Math.min( + videoInfo.sourceFrames, + Math.ceil((videoInfo.sourceFrames * sourceDurationSec) / videoInfo.durationSec), + ), +); +let webcamSourceWindowFrames = webcamInfo + ? Math.max( + 1, + Math.min( + webcamInfo.sourceFrames, + Math.ceil((webcamInfo.sourceFrames * sourceDurationSec) / webcamInfo.durationSec), + ), + ) + : 0; +const backgroundNv12Path = backgroundImage ? generatedBackgroundNv12Path : backgroundNv12; +const backgroundFilter = createBackgroundFilter( + { width: outputWidth, height: outputHeight }, + shouldBakeStaticShadow + ? { + x: contentX, + y: contentY + shadowOffsetY, + width: contentWidth, + height: contentHeight, + radius: radius + 8, + intensityPct: shadowIntensityPct, + } + : null, + backgroundBlurPx, +); + +const backgroundConvertPromise = + backgroundImage && !(reuseIntermediates && existsSync(backgroundNv12Path)) + ? runAsync(ffmpegCommand, [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + resolve(backgroundImage), + ...backgroundFilter.filterArgs, + "-frames:v", + "1", + "-f", + "rawvideo", + backgroundNv12Path, + ]) + : Promise.resolve(zeroElapsed()); + +const webcamConvertPromise = + webcamInput && + !webcamStream && + webcamSize > 0 && + !(reuseIntermediates && existsSync(generatedWebcamNv12Path)) + ? runAsync(ffmpegCommand, [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + resolve(webcamInput), + "-vf", + `${webcamMirror ? "hflip," : ""}scale=${webcamSize}:${webcamSize}:force_original_aspect_ratio=increase,crop=${webcamSize}:${webcamSize},format=nv12`, + "-frames:v", + "1", + "-f", + "rawvideo", + generatedWebcamNv12Path, + ]) + : Promise.resolve(zeroElapsed()); + +const webcamDemuxPromise = + webcamInput && webcamStream && !(reuseDemux && existsSync(webcamAnnexBPath)) + ? runWithProgress( + ffmpegCommand, + [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-stats", + "-i", + resolve(webcamInput), + "-t", + String(sourceDurationSec), + "-map", + "0:v:0", + "-c:v", + "copy", + "-bsf:v", + "h264_mp4toannexb", + "-an", + webcamAnnexBPath, + ], + { + totalDurationSec: sourceDurationSec, + totalFrames: targetFrames, + startPercentage: 1, + endPercentage: 2, + }, + ) + : Promise.resolve(zeroElapsed()); + +const demuxPromise = + reuseDemux && existsSync(annexBPath) + ? Promise.resolve(zeroElapsed()) + : runWithProgress( + ffmpegCommand, + [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-stats", + "-i", + inputPath, + "-t", + String(sourceDurationSec), + "-map", + "0:v:0", + "-c:v", + "copy", + "-bsf:v", + "h264_mp4toannexb", + "-an", + annexBPath, + ], + { + totalDurationSec: sourceDurationSec, + totalFrames: targetFrames, + startPercentage: 1, + endPercentage: 2, + }, + ); +const sourcePtsPromise = writeFramePtsSidecarAsync(inputPath, sourceDurationSec, sourcePtsPath); + +if (cursorJson) { + const cursorPayload = JSON.parse(readFileSync(resolve(cursorJson), "utf8")); + writeCursorSamples(cursorPayload, cursorSamplesPath); +} +const cursorAtlas = + cursorJson && cursorHeight > 0 && cursorAtlasPng && cursorAtlasMetadata + ? prepareExternalCursorAtlas(workDir, cursorAtlasPng, cursorAtlasMetadata) + : cursorJson && cursorHeight > 0 && cursorStyle === "tahoe" + ? renderTahoeCursorAtlas(workDir) + : null; + +const [backgroundConvert, webcamConvert, webcamDemux, demux, sourcePts] = await Promise.all([ + backgroundConvertPromise, + webcamConvertPromise, + webcamDemuxPromise, + demuxPromise, + sourcePtsPromise, +]); +const preparationWallMs = performance.now() - pipelineStartedAt; +const demuxFrameCount = parseFfmpegStatsFrameCount(demux.stderr); +if (demuxFrameCount > 0) { + sourceWindowFrames = demuxFrameCount; +} +const webcamDemuxFrameCount = parseFfmpegStatsFrameCount(webcamDemux.stderr); +if (webcamDemuxFrameCount > 0) { + webcamSourceWindowFrames = webcamDemuxFrameCount; +} +if (timelineSegments.length && (!sourcePts.path || sourcePts.frames < sourceWindowFrames)) { + fail("Timeline-map CUDA export requires source frame PTS for the full source window"); +} +emitPreparationProgress(targetFrames, 3); + +const encodeArgs = [ + "--input", + annexBPath, + "--output", + encodedPath, + "--fps", + String(fps), + "--input-frames", + String(sourceWindowFrames), + "--target-frames", + String(targetFrames), + "--bitrate-mbps", + String(bitrateMbps), + "--encoding-mode", + encodingMode, + "--callback-encode", + "--chunk-mb", + String(chunkMb), +]; +if (requestedOutputWidth > 0 && requestedOutputHeight > 0) { + encodeArgs.push("--width", String(outputWidth), "--height", String(outputHeight)); +} +if (sourcePts.path && sourcePts.frames >= sourceWindowFrames) { + encodeArgs.push("--source-pts", sourcePts.path); +} +if (timelineMap) { + encodeArgs.push("--timeline-map", resolve(timelineMap)); +} +if (maxOutputFrames > 0) { + encodeArgs.push("--max-frames", String(maxOutputFrames)); +} +if (cursorJson && cursorHeight > 0) { + encodeArgs.push("--cursor-samples", cursorSamplesPath, "--cursor-height", String(cursorHeight)); + if (cursorAtlas) { + encodeArgs.push( + "--cursor-atlas-rgba", + cursorAtlas.rgbaPath, + "--cursor-atlas-metadata", + cursorAtlas.metadataPath, + "--cursor-atlas-width", + String(cursorAtlas.width), + "--cursor-atlas-height", + String(cursorAtlas.height), + ); + } +} +if (streamSync) { + encodeArgs.push("--stream-sync"); +} +if (prewarmMs > 0) { + encodeArgs.push("--prewarm-ms", String(prewarmMs)); +} +if (contentWidth > 0 && contentHeight > 0) { + encodeArgs.push( + "--content-x", + String(contentX), + "--content-y", + String(contentY), + "--content-width", + String(contentWidth), + "--content-height", + String(contentHeight), + "--radius", + String(radius), + "--background-y", + String(backgroundY), + "--background-u", + String(backgroundU), + "--background-v", + String(backgroundV), + ); + if (backgroundNv12Path) { + encodeArgs.push("--background-nv12", backgroundNv12Path); + } + if (sourceCropWidth >= 2 && sourceCropHeight >= 2) { + encodeArgs.push( + "--source-crop-x", + String(sourceCropX), + "--source-crop-y", + String(sourceCropY), + "--source-crop-width", + String(sourceCropWidth), + "--source-crop-height", + String(sourceCropHeight), + ); + } + if (!shouldBakeStaticShadow && shadowOffsetY > 0 && shadowIntensityPct > 0) { + encodeArgs.push( + "--shadow-offset-y", + String(shadowOffsetY), + "--shadow-intensity-pct", + String(shadowIntensityPct), + ); + } + if (webcamInput && webcamSize > 0) { + encodeArgs.push( + "--webcam-x", + String(webcamX), + "--webcam-y", + String(webcamY), + "--webcam-size", + String(webcamSize), + "--webcam-radius", + String(webcamRadius), + "--webcam-time-offset-ms", + String(webcamTimeOffsetMs), + ); + if (webcamMirror) { + encodeArgs.push("--webcam-mirror"); + } + if (webcamStream) { + encodeArgs.push( + "--webcam-annexb", + webcamAnnexBPath, + "--webcam-input-frames", + String(webcamSourceWindowFrames), + "--webcam-target-frames", + String(targetFrames), + "--webcam-source-duration-ms", + String(webcamSourceDurationSec * 1000), + "--webcam-source-width", + String(webcamInfo.width), + "--webcam-source-height", + String(webcamInfo.height), + ); + } else { + encodeArgs.push("--webcam-nv12", generatedWebcamNv12Path); + } + } +} +if (zoomTelemetry) { + encodeArgs.push("--zoom-samples", resolve(zoomTelemetry)); +} +const encode = + reuseIntermediates && existsSync(encodedPath) + ? { elapsedMs: 0, stdout: "", gpuSummary: null } + : await runWithGpuMonitor( + nativeProbe, + encodeArgs, + sampleGpuDuringEncode ? gpuSampleIntervalMs : 0, + ); +const nativeSummary = encode.stdout ? parseProbeSummary(encode.stdout) : null; + +const mux = skipMux + ? { elapsedMs: 0 } + : videoOnly + ? await runWithProgress( + ffmpegCommand, + [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-stats", + "-framerate", + String(fps), + "-i", + encodedPath, + "-map", + "0:v:0", + "-c:v", + "copy", + outputPath, + ], + { + totalDurationSec: durationSec, + totalFrames: targetFrames, + startPercentage: 96, + endPercentage: 97.25, + stage: "finalizing", + }, + ) + : await runWithProgress( + ffmpegCommand, + [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-stats", + "-framerate", + String(fps), + "-i", + encodedPath, + "-i", + inputPath, + "-map", + "0:v:0", + "-map", + "1:a?", + "-c:v", + "copy", + "-c:a", + "copy", + "-t", + String(durationSec), + outputPath, + ], + { + totalDurationSec: durationSec, + totalFrames: targetFrames, + startPercentage: 96, + endPercentage: 99, + stage: "finalizing", + }, + ); + +const finalOutputPath = skipMux ? encodedPath : outputPath; +const outputInfo = skipMux + ? { streams: [] } + : ffprobeJson([ + "-show_entries", + "stream=index,codec_type,codec_name,width,height,duration,avg_frame_rate,nb_frames", + finalOutputPath, + ]); +const outputStreams = outputInfo.streams ?? []; +const outputVideo = outputStreams.find((stream) => stream.codec_type === "video") ?? null; +const outputAudio = outputStreams.find((stream) => stream.codec_type === "audio") ?? null; + +console.log( + JSON.stringify( + { + success: true, + inputPath, + outputPath: finalOutputPath, + requestedOutputPath: outputPath, + encodedPath, + fps, + bitrateMbps, + encodingMode, + streamSync, + prewarmMs, + maxOutputFrames, + chunkMb, + skipMux, + videoOnly, + durationSec, + sourceDurationSec, + timelineMap: timelineSegments.length + ? { + inputPath: resolve(timelineMap), + segments: timelineSegments.length, + sourceDurationSec, + outputDurationSec: durationSec, + } + : null, + staticLayout: + contentWidth > 0 && contentHeight > 0 + ? { + contentX, + contentY, + contentWidth, + contentHeight, + radius, + backgroundY, + backgroundU, + backgroundV, + shadowOffsetY, + shadowIntensityPct, + shadowBakedIntoBackground: shouldBakeStaticShadow, + backgroundShadowAlpha: backgroundFilter.shadowAlpha ?? null, + backgroundShadowBlur: backgroundFilter.shadowBlur ?? null, + webcam: + webcamInput && webcamSize > 0 + ? { + inputPath: resolve(webcamInput), + x: webcamX, + y: webcamY, + size: webcamSize, + radius: webcamRadius, + timeOffsetMs: webcamTimeOffsetMs, + sourceDurationSec: webcamSourceDurationSec, + mirror: webcamMirror, + staticFrameOnly: !webcamStream, + stream: webcamStream, + } + : null, + cursor: + cursorJson && cursorHeight > 0 + ? { + inputPath: resolve(cursorJson), + height: cursorHeight, + style: cursorStyle, + atlas: cursorAtlas + ? { + width: cursorAtlas.width, + height: cursorAtlas.height, + entries: cursorAtlas.entries, + } + : null, + } + : null, + zoom: zoomTelemetry + ? { + inputPath: resolve(zoomTelemetry), + } + : null, + } + : null, + gpuSampleIntervalMs: sampleGpuDuringEncode ? gpuSampleIntervalMs : null, + videoInfo, + sourceWindowFrames, + sourcePtsFrames: sourcePts.frames, + sourcePtsSource: sourcePts.source, + targetFrames, + timingsMs: { + preparationWall: Number(preparationWallMs.toFixed(2)), + demux: Number(demux.elapsedMs.toFixed(2)), + backgroundConvert: Number(backgroundConvert.elapsedMs.toFixed(2)), + cursorAtlas: Number((cursorAtlas?.elapsedMs ?? 0).toFixed(2)), + webcamConvert: Number(webcamConvert.elapsedMs.toFixed(2)), + webcamDemux: Number(webcamDemux.elapsedMs.toFixed(2)), + sourcePtsProbe: Number(sourcePts.elapsedMs.toFixed(2)), + nativeEncode: Number(encode.elapsedMs.toFixed(2)), + mux: Number(mux.elapsedMs.toFixed(2)), + endToEnd: Number((performance.now() - pipelineStartedAt).toFixed(2)), + }, + nativeSummary, + nativeProcessPriorityBoosted: encode.priorityBoosted ?? false, + gpuSamples: encode.gpuSamples ?? [], + gpuSummary: encode.gpuSummary ?? null, + outputVideo, + outputAudio, + outputStreams, + }, + null, + 2, + ), +); diff --git a/electron/native/nvidia-cuda-compositor/src/main.cu b/electron/native/nvidia-cuda-compositor/src/main.cu new file mode 100644 index 000000000..28c72123d --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/src/main.cu @@ -0,0 +1,3313 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "NvDecoder/NvDecoder.h" +#include "NvEncoder/NvEncoderCuda.h" +#include "Utils/Logger.h" + +simplelogger::Logger* logger = simplelogger::LoggerFactory::CreateConsoleLogger(ERROR); + +namespace { + +struct TimelineSegment { + double sourceStartMs = 0.0; + double sourceEndMs = 0.0; + double outputStartMs = 0.0; + double outputEndMs = 0.0; + double speed = 1.0; +}; + +struct Options { + std::string inputPath; + std::string outputPath = "recordly-nvidia-cuda-compositor.h264"; + std::string sourcePtsPath; + std::string timelineMapPath; + std::vector timelineSegments; + int width = 0; + int height = 0; + int fps = 30; + int maxFrames = 0; + int inputFrames = 0; + int targetFrames = 0; + int bitrateMbps = 18; + std::string encodingMode = "balanced"; + bool postSelect = false; + bool callbackEncode = false; + bool streamSync = false; + int prewarmMs = 0; + int chunkMb = 4; + int contentX = 0; + int contentY = 0; + int contentWidth = 0; + int contentHeight = 0; + int sourceCropX = 0; + int sourceCropY = 0; + int sourceCropWidth = 0; + int sourceCropHeight = 0; + int radius = 0; + int backgroundY = 16; + int backgroundU = 128; + int backgroundV = 128; + std::string backgroundNv12Path; + int shadowOffsetY = 0; + int shadowIntensityPct = 0; + std::string webcamNv12Path; + std::string webcamAnnexbPath; + int webcamInputFrames = 0; + int webcamTargetFrames = 0; + double webcamSourceDurationMs = 0.0; + int webcamSourceWidth = 0; + int webcamSourceHeight = 0; + int webcamX = 0; + int webcamY = 0; + int webcamSize = 0; + int webcamRadius = 0; + double webcamTimeOffsetMs = 0.0; + bool webcamMirror = false; + std::string cursorSamplesPath; + int cursorHeight = 0; + std::string cursorAtlasRgbaPath; + std::string cursorAtlasMetadataPath; + int cursorAtlasWidth = 0; + int cursorAtlasHeight = 0; + std::string zoomSamplesPath; +}; + +constexpr int kMaxCursorAtlasEntries = 16; +constexpr int kWebcamPrefetchOutputFrames = 900; + +[[noreturn]] void fail(const std::string& message) { + throw std::runtime_error(message); +} + +void checkCuda(cudaError_t status, const char* expression) { + if (status != cudaSuccess) { + std::ostringstream stream; + stream << expression << " failed: " << cudaGetErrorString(status); + fail(stream.str()); + } +} + +void checkCu(CUresult status, const char* expression) { + if (status != CUDA_SUCCESS) { + const char* name = nullptr; + const char* message = nullptr; + cuGetErrorName(status, &name); + cuGetErrorString(status, &message); + std::ostringstream stream; + stream << expression << " failed: " << (name ? name : "CUDA_ERROR") + << " (" << (message ? message : "no detail") << ")"; + fail(stream.str()); + } +} + +int parsePositiveInt(const char* value, const char* name) { + char* end = nullptr; + const long parsed = std::strtol(value, &end, 10); + if (!end || *end != '\0' || parsed <= 0 || parsed > 1000000) { + std::ostringstream stream; + stream << "Invalid " << name << ": " << value; + fail(stream.str()); + } + return static_cast(parsed); +} + +int parseNonNegativeInt(const char* value, const char* name) { + char* end = nullptr; + const long parsed = std::strtol(value, &end, 10); + if (!end || *end != '\0' || parsed < 0 || parsed > 1000000) { + std::ostringstream stream; + stream << "Invalid " << name << ": " << value; + fail(stream.str()); + } + return static_cast(parsed); +} + +double parseFiniteDouble(const char* value, const char* name) { + char* end = nullptr; + const double parsed = std::strtod(value, &end); + if (!end || *end != '\0' || !std::isfinite(parsed)) { + std::ostringstream stream; + stream << "Invalid " << name << ": " << value; + fail(stream.str()); + } + return parsed; +} + +Options parseOptions(int argc, char** argv) { + Options options; + for (int index = 1; index < argc; ++index) { + const std::string arg = argv[index]; + auto requireValue = [&](const char* name) -> const char* { + if (index + 1 >= argc) { + std::ostringstream stream; + stream << "Missing value for " << name; + fail(stream.str()); + } + return argv[++index]; + }; + + if (arg == "--input") { + options.inputPath = requireValue("--input"); + } else if (arg == "--output") { + options.outputPath = requireValue("--output"); + } else if (arg == "--source-pts") { + options.sourcePtsPath = requireValue("--source-pts"); + } else if (arg == "--width") { + options.width = parsePositiveInt(requireValue("--width"), "--width"); + } else if (arg == "--height") { + options.height = parsePositiveInt(requireValue("--height"), "--height"); + } else if (arg == "--timeline-map") { + options.timelineMapPath = requireValue("--timeline-map"); + } else if (arg == "--fps") { + options.fps = parsePositiveInt(requireValue("--fps"), "--fps"); + } else if (arg == "--max-frames") { + options.maxFrames = parsePositiveInt(requireValue("--max-frames"), "--max-frames"); + } else if (arg == "--input-frames") { + options.inputFrames = parsePositiveInt(requireValue("--input-frames"), "--input-frames"); + } else if (arg == "--target-frames") { + options.targetFrames = parsePositiveInt(requireValue("--target-frames"), "--target-frames"); + } else if (arg == "--bitrate-mbps") { + options.bitrateMbps = parsePositiveInt(requireValue("--bitrate-mbps"), "--bitrate-mbps"); + } else if (arg == "--encoding-mode") { + options.encodingMode = requireValue("--encoding-mode"); + if ( + options.encodingMode != "fast" && + options.encodingMode != "balanced" && + options.encodingMode != "quality") { + fail("Unsupported --encoding-mode: " + options.encodingMode); + } + } else if (arg == "--post-select") { + options.postSelect = true; + } else if (arg == "--callback-encode") { + options.callbackEncode = true; + } else if (arg == "--stream-sync") { + options.streamSync = true; + } else if (arg == "--prewarm-ms") { + options.prewarmMs = parsePositiveInt(requireValue("--prewarm-ms"), "--prewarm-ms"); + } else if (arg == "--chunk-mb") { + options.chunkMb = parsePositiveInt(requireValue("--chunk-mb"), "--chunk-mb"); + } else if (arg == "--content-x") { + options.contentX = parseNonNegativeInt(requireValue("--content-x"), "--content-x"); + } else if (arg == "--content-y") { + options.contentY = parseNonNegativeInt(requireValue("--content-y"), "--content-y"); + } else if (arg == "--content-width") { + options.contentWidth = parsePositiveInt(requireValue("--content-width"), "--content-width"); + } else if (arg == "--content-height") { + options.contentHeight = parsePositiveInt(requireValue("--content-height"), "--content-height"); + } else if (arg == "--source-crop-x") { + options.sourceCropX = parseNonNegativeInt(requireValue("--source-crop-x"), "--source-crop-x"); + } else if (arg == "--source-crop-y") { + options.sourceCropY = parseNonNegativeInt(requireValue("--source-crop-y"), "--source-crop-y"); + } else if (arg == "--source-crop-width") { + options.sourceCropWidth = parsePositiveInt(requireValue("--source-crop-width"), "--source-crop-width"); + } else if (arg == "--source-crop-height") { + options.sourceCropHeight = parsePositiveInt(requireValue("--source-crop-height"), "--source-crop-height"); + } else if (arg == "--radius") { + options.radius = parseNonNegativeInt(requireValue("--radius"), "--radius"); + } else if (arg == "--background-y") { + options.backgroundY = parseNonNegativeInt(requireValue("--background-y"), "--background-y"); + } else if (arg == "--background-u") { + options.backgroundU = parseNonNegativeInt(requireValue("--background-u"), "--background-u"); + } else if (arg == "--background-v") { + options.backgroundV = parseNonNegativeInt(requireValue("--background-v"), "--background-v"); + } else if (arg == "--background-nv12") { + options.backgroundNv12Path = requireValue("--background-nv12"); + } else if (arg == "--shadow-offset-y") { + options.shadowOffsetY = parseNonNegativeInt(requireValue("--shadow-offset-y"), "--shadow-offset-y"); + } else if (arg == "--shadow-intensity-pct") { + options.shadowIntensityPct = parseNonNegativeInt(requireValue("--shadow-intensity-pct"), "--shadow-intensity-pct"); + } else if (arg == "--webcam-nv12") { + options.webcamNv12Path = requireValue("--webcam-nv12"); + } else if (arg == "--webcam-annexb") { + options.webcamAnnexbPath = requireValue("--webcam-annexb"); + } else if (arg == "--webcam-input-frames") { + options.webcamInputFrames = parsePositiveInt(requireValue("--webcam-input-frames"), "--webcam-input-frames"); + } else if (arg == "--webcam-target-frames") { + options.webcamTargetFrames = + parsePositiveInt(requireValue("--webcam-target-frames"), "--webcam-target-frames"); + } else if (arg == "--webcam-source-duration-ms") { + options.webcamSourceDurationMs = + parseFiniteDouble(requireValue("--webcam-source-duration-ms"), "--webcam-source-duration-ms"); + } else if (arg == "--webcam-source-width") { + options.webcamSourceWidth = parsePositiveInt(requireValue("--webcam-source-width"), "--webcam-source-width"); + } else if (arg == "--webcam-source-height") { + options.webcamSourceHeight = + parsePositiveInt(requireValue("--webcam-source-height"), "--webcam-source-height"); + } else if (arg == "--webcam-x") { + options.webcamX = parseNonNegativeInt(requireValue("--webcam-x"), "--webcam-x"); + } else if (arg == "--webcam-y") { + options.webcamY = parseNonNegativeInt(requireValue("--webcam-y"), "--webcam-y"); + } else if (arg == "--webcam-size") { + options.webcamSize = parsePositiveInt(requireValue("--webcam-size"), "--webcam-size"); + } else if (arg == "--webcam-radius") { + options.webcamRadius = parseNonNegativeInt(requireValue("--webcam-radius"), "--webcam-radius"); + } else if (arg == "--webcam-time-offset-ms") { + options.webcamTimeOffsetMs = + parseFiniteDouble(requireValue("--webcam-time-offset-ms"), "--webcam-time-offset-ms"); + } else if (arg == "--webcam-mirror") { + options.webcamMirror = true; + } else if (arg == "--cursor-samples") { + options.cursorSamplesPath = requireValue("--cursor-samples"); + } else if (arg == "--cursor-height") { + options.cursorHeight = parsePositiveInt(requireValue("--cursor-height"), "--cursor-height"); + } else if (arg == "--cursor-atlas-rgba") { + options.cursorAtlasRgbaPath = requireValue("--cursor-atlas-rgba"); + } else if (arg == "--cursor-atlas-metadata") { + options.cursorAtlasMetadataPath = requireValue("--cursor-atlas-metadata"); + } else if (arg == "--cursor-atlas-width") { + options.cursorAtlasWidth = parsePositiveInt(requireValue("--cursor-atlas-width"), "--cursor-atlas-width"); + } else if (arg == "--cursor-atlas-height") { + options.cursorAtlasHeight = + parsePositiveInt(requireValue("--cursor-atlas-height"), "--cursor-atlas-height"); + } else if (arg == "--zoom-samples") { + options.zoomSamplesPath = requireValue("--zoom-samples"); + } else if (arg == "--help") { + std::cout << "Usage: recordly-nvidia-cuda-compositor --input input.annexb.h264 " + "[--output out.h264] [--source-pts source-pts.csv] [--width N --height N] [--fps 30] " + "[--max-frames N] [--bitrate-mbps N] [--encoding-mode fast|balanced|quality] " + "[--post-select] [--callback-encode] [--stream-sync] [--prewarm-ms N] [--chunk-mb N] " + "[--content-x N --content-y N --content-width N --content-height N --radius N] " + "[--background-nv12 background.nv12] [--shadow-offset-y N --shadow-intensity-pct N] " + "[--webcam-nv12 webcam.nv12 --webcam-x N --webcam-y N --webcam-size N --webcam-radius N] " + "[--webcam-annexb webcam.h264 --webcam-input-frames N --webcam-target-frames N " + "--webcam-source-duration-ms N --webcam-time-offset-ms N] " + "[--cursor-samples cursor.tsv --cursor-height N] " + "[--cursor-atlas-rgba cursor.rgba --cursor-atlas-metadata cursor.tsv " + "--cursor-atlas-width N --cursor-atlas-height N] " + "[--zoom-samples zoom.csv]\n"; + std::exit(0); + } else { + std::ostringstream stream; + stream << "Unknown argument: " << arg; + fail(stream.str()); + } + } + if (options.inputPath.empty()) { + fail("--input is required"); + } + if ((options.width > 0) != (options.height > 0)) { + fail("--width and --height must be specified together"); + } + if (options.width > 0 && (options.width % 2 != 0 || options.height % 2 != 0)) { + fail("--width and --height must be even numbers for NV12 encoding"); + } + return options; +} + +bool shouldEncodeFrame(int sourceFrameIndex, int encodedFrames, const Options& options) { + if (options.inputFrames <= 0 || options.targetFrames <= 0) { + return true; + } + if (encodedFrames >= options.targetFrames) { + return false; + } + + const int expectedEncodedFrames = + ((sourceFrameIndex + 1) * options.targetFrames + options.inputFrames - 1) / options.inputFrames; + return encodedFrames < expectedEncodedFrames; +} + +bool hasStaticLayout(const Options& options) { + return options.contentWidth > 0 && options.contentHeight > 0; +} + +bool hasWebcamOverlay(const Options& options) { + return (!options.webcamNv12Path.empty() || !options.webcamAnnexbPath.empty()) && options.webcamSize > 0; +} + +int outputWidthForSource(const Options& options, int sourceWidth) { + return options.width > 0 ? options.width : sourceWidth; +} + +int outputHeightForSource(const Options& options, int sourceHeight) { + return options.height > 0 ? options.height : sourceHeight; +} + +std::vector loadFramePts(const std::string& path) { + std::vector timestamps; + if (path.empty()) { + return timestamps; + } + + std::ifstream input(path); + if (!input) { + fail("Failed to open source PTS sidecar: " + path); + } + + std::string line; + double lastTimestamp = -std::numeric_limits::infinity(); + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + char* end = nullptr; + const double timestamp = std::strtod(line.c_str(), &end); + if (!end || *end != '\0' || !std::isfinite(timestamp) || timestamp < lastTimestamp) { + fail("Invalid source PTS sidecar entry: " + line); + } + timestamps.push_back(timestamp); + lastTimestamp = timestamp; + } + + return timestamps; +} + +std::vector loadTimelineMap(const std::string& path) { + std::vector segments; + if (path.empty()) { + return segments; + } + + std::ifstream input(path); + if (!input) { + fail("Failed to open timeline map: " + path); + } + + std::string line; + double expectedOutputStartMs = 0.0; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + TimelineSegment segment; + if (std::sscanf( + line.c_str(), + "%lf,%lf,%lf,%lf,%lf", + &segment.sourceStartMs, + &segment.sourceEndMs, + &segment.outputStartMs, + &segment.outputEndMs, + &segment.speed) != 5) { + fail("Invalid timeline map row: " + line); + } + if ( + !std::isfinite(segment.sourceStartMs) || + !std::isfinite(segment.sourceEndMs) || + !std::isfinite(segment.outputStartMs) || + !std::isfinite(segment.outputEndMs) || + !std::isfinite(segment.speed) || + segment.sourceEndMs <= segment.sourceStartMs || + segment.outputEndMs <= segment.outputStartMs || + segment.speed <= 0.0 || + std::abs(segment.outputStartMs - expectedOutputStartMs) > 2.0) { + fail("Invalid timeline map segment: " + line); + } + expectedOutputStartMs = segment.outputEndMs; + segments.push_back(segment); + } + + if (!path.empty() && segments.empty()) { + fail("Timeline map is empty: " + path); + } + return segments; +} + +bool sourceToOutputMs( + const std::vector& segments, + double sourceMs, + double& outputMs) { + if (segments.empty()) { + outputMs = sourceMs; + return true; + } + + constexpr double kToleranceMs = 1.0; + for (const auto& segment : segments) { + if (sourceMs + kToleranceMs < segment.sourceStartMs) { + return false; + } + if (sourceMs <= segment.sourceEndMs + kToleranceMs) { + const double clampedSourceMs = + std::min(segment.sourceEndMs, std::max(segment.sourceStartMs, sourceMs)); + outputMs = segment.outputStartMs + (clampedSourceMs - segment.sourceStartMs) / segment.speed; + outputMs = std::min(segment.outputEndMs, std::max(segment.outputStartMs, outputMs)); + return true; + } + } + + return false; +} + +double outputToSourceMs(const std::vector& segments, double outputMs) { + if (segments.empty()) { + return outputMs; + } + + for (const auto& segment : segments) { + if (outputMs <= segment.outputEndMs + 1.0) { + const double clampedOutputMs = + std::min(segment.outputEndMs, std::max(segment.outputStartMs, outputMs)); + return segment.sourceStartMs + (clampedOutputMs - segment.outputStartMs) * segment.speed; + } + } + + return segments.back().sourceEndMs; +} + +int webcamFrameIndexForSourceTimeMs(double sourceTimeMs, const Options& options) { + const double adjustedSourceMs = std::max(0.0, sourceTimeMs - options.webcamTimeOffsetMs); + if (options.webcamInputFrames > 0) { + const double durationMs = options.webcamSourceDurationMs > 0.0 + ? options.webcamSourceDurationMs + : (options.webcamTargetFrames > 0 && options.fps > 0 + ? (static_cast(options.webcamTargetFrames) * 1000.0) / options.fps + : 0.0); + if (durationMs > 0.0) { + const double ratio = std::min(1.0, std::max(0.0, adjustedSourceMs / durationMs)); + const int index = ratio >= 1.0 + ? options.webcamInputFrames - 1 + : static_cast(std::floor(ratio * options.webcamInputFrames)); + return std::max(0, std::min(options.webcamInputFrames - 1, index)); + } + } + return std::max(0, static_cast(std::floor(adjustedSourceMs * options.fps / 1000.0))); +} + +int webcamFrameIndexForOutputFrame(int outputFrameIndex, const Options& options) { + const double outputTimeMs = + static_cast(std::max(0, outputFrameIndex)) * 1000.0 / static_cast(options.fps); + return webcamFrameIndexForSourceTimeMs( + outputToSourceMs(options.timelineSegments, outputTimeMs), + options); +} + +int maxSelectedFramesForTimeline(int targetFrames, int maxFrames) { + if (targetFrames <= 0) { + return maxFrames > 0 ? maxFrames : std::numeric_limits::max(); + } + return maxFrames > 0 ? std::min(maxFrames, targetFrames) : targetFrames; +} + +int expectedOutputFramesForSourceFrame( + int sourceFrameIndex, + int inputFrames, + int targetFrames, + int maxFrames, + int fps, + const std::vector* sourcePts, + const std::vector* timelineSegments) { + const int maxOutputFrames = maxSelectedFramesForTimeline(targetFrames, maxFrames); + if (sourcePts && sourceFrameIndex >= 0 && sourceFrameIndex < static_cast(sourcePts->size())) { + const bool hasTimelineMap = timelineSegments && !timelineSegments->empty(); + if (!hasTimelineMap && inputFrames > 0 && sourceFrameIndex + 1 >= inputFrames) { + return maxOutputFrames; + } + if (hasTimelineMap && inputFrames > 0 && sourceFrameIndex + 1 >= inputFrames) { + return maxOutputFrames; + } + const double frameTimeSec = std::max(0.0, (*sourcePts)[sourceFrameIndex]); + double outputTimeMs = frameTimeSec * 1000.0; + if (hasTimelineMap && !sourceToOutputMs(*timelineSegments, outputTimeMs, outputTimeMs)) { + return 0; + } + const int64_t expected = + static_cast(std::floor((outputTimeMs / 1000.0) * fps)) + 1; + return static_cast(std::min(std::max(1, expected), maxOutputFrames)); + } + if (inputFrames <= 0 || targetFrames <= 0) { + return maxOutputFrames; + } + + const int64_t expected = + (static_cast(sourceFrameIndex + 1) * targetFrames + inputFrames - 1) / inputFrames; + return static_cast(std::min(expected, maxOutputFrames)); +} + +unsigned char clampByte(int value) { + return static_cast(std::max(0, std::min(255, value))); +} + +struct FrameSelectionState { + int inputFrames = 0; + int targetFrames = 0; + int maxFrames = 0; + int sourceFrames = 0; + int selectedFrames = 0; + int fps = 30; + const std::vector* sourcePts = nullptr; + const std::vector* timelineSegments = nullptr; +}; + +bool shouldCopyDisplayFrame(int displayFrameIndex, void* userData) { + auto* state = static_cast(userData); + state->sourceFrames = displayFrameIndex + 1; + + const int maxSelectedFrames = + state->maxFrames > 0 ? std::min(state->maxFrames, state->targetFrames) : state->targetFrames; + if (state->selectedFrames >= maxSelectedFrames) { + return false; + } + + // maxFrames is a smoke-test stop cap; it must not spread the sampled frames + // across the full source because that hides the true first-window performance. + const int expectedSelectedFrames = expectedOutputFramesForSourceFrame( + displayFrameIndex, + state->inputFrames, + state->targetFrames, + state->maxFrames, + state->fps, + state->sourcePts, + state->timelineSegments); + if (state->selectedFrames >= expectedSelectedFrames) { + return false; + } + + ++state->selectedFrames; + return true; +} + +double elapsedMs(std::chrono::steady_clock::time_point start, std::chrono::steady_clock::time_point end); +struct ProgressCounters { + double decodeWallMs = 0.0; + double encodeMs = 0.0; + double compositeMs = 0.0; + double nvencMs = 0.0; + double packetWriteMs = 0.0; + double webcamDecodeMs = 0.0; + double webcamCopyMs = 0.0; + int roiCompositeFrames = 0; + int monolithicCompositeFrames = 0; + int copyCompositeFrames = 0; +}; + +struct ProgressReportState { + std::chrono::steady_clock::time_point startedAt; + std::chrono::steady_clock::time_point lastReportAt; + int lastReportedFrame = 0; + ProgressCounters lastCounters; +}; + +void reportEncodingProgress( + int encodedFrames, + int totalFrames, + ProgressReportState& state, + const ProgressCounters& counters, + bool force = false); + +struct WebcamFrameCache { + std::vector frames; + double decodeMs = 0.0; + double copyMs = 0.0; + int sourceFrames = 0; + int baseFrameIndex = 0; + int decodedFrames = 0; + int peakFrames = 0; + int width = 0; + int height = 0; + + ~WebcamFrameCache() { + for (unsigned char* frame : frames) { + cudaFree(frame); + } + } + + void pushFrame(unsigned char* frame) { + frames.push_back(frame); + decodedFrames = baseFrameIndex + static_cast(frames.size()); + sourceFrames = decodedFrames; + peakFrames = std::max(peakFrames, static_cast(frames.size())); + } + + void dropBefore(int minFrameIndex) { + const int dropCount = std::min( + std::max(0, minFrameIndex - baseFrameIndex), + std::max(0, static_cast(frames.size()) - 1)); + if (dropCount <= 0) { + return; + } + for (int index = 0; index < dropCount; ++index) { + cudaFree(frames[index]); + } + frames.erase(frames.begin(), frames.begin() + dropCount); + baseFrameIndex += dropCount; + } + + const unsigned char* frameAt(int frameIndex) const { + if (frames.empty()) { + return nullptr; + } + const int clampedFrameIndex = + std::max(baseFrameIndex, std::min(frameIndex, baseFrameIndex + static_cast(frames.size()) - 1)); + return frames[clampedFrameIndex - baseFrameIndex]; + } +}; + +struct CursorSample { + double timeMs = 0.0; + double cx = 0.0; + double cy = 0.0; + int typeIndex = 0; + double bounceScale = 1.0; + bool visible = true; +}; + +struct CursorPosition { + bool visible = false; + double cx = 0.0; + double cy = 0.0; + int typeIndex = 0; + double bounceScale = 1.0; +}; + +struct CursorTrack { + std::vector samples; + + CursorPosition positionAt(double timeMs) const { + if (samples.empty()) { + return {}; + } + if (timeMs <= samples.front().timeMs) { + return { + samples.front().visible, + samples.front().cx, + samples.front().cy, + samples.front().typeIndex, + samples.front().bounceScale, + }; + } + if (timeMs >= samples.back().timeMs) { + return { + samples.back().visible, + samples.back().cx, + samples.back().cy, + samples.back().typeIndex, + samples.back().bounceScale, + }; + } + + int low = 0; + int high = static_cast(samples.size()) - 1; + while (low < high - 1) { + const int mid = (low + high) / 2; + if (samples[mid].timeMs <= timeMs) { + low = mid; + } else { + high = mid; + } + } + + const CursorSample& left = samples[low]; + const CursorSample& right = samples[high]; + const double span = right.timeMs - left.timeMs; + if (span <= 0.0) { + return {left.visible, left.cx, left.cy, left.typeIndex, left.bounceScale}; + } + + const double t = (timeMs - left.timeMs) / span; + return { + left.visible && right.visible, + left.cx + (right.cx - left.cx) * t, + left.cy + (right.cy - left.cy) * t, + t < 0.5 ? left.typeIndex : right.typeIndex, + left.bounceScale + (right.bounceScale - left.bounceScale) * t, + }; + } +}; + +std::unique_ptr loadCursorTrack(const Options& options) { + if (options.cursorSamplesPath.empty()) { + return nullptr; + } + if (options.cursorHeight <= 0) { + fail("--cursor-height is required with --cursor-samples"); + } + + std::ifstream input(options.cursorSamplesPath); + if (!input) { + fail("Failed to open cursor samples: " + options.cursorSamplesPath); + } + + auto track = std::make_unique(); + std::string line; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + std::istringstream row(line); + CursorSample sample; + if (!(row >> sample.timeMs >> sample.cx >> sample.cy)) { + continue; + } + if (!(row >> sample.typeIndex)) { + sample.typeIndex = 0; + } + if (!(row >> sample.bounceScale)) { + sample.bounceScale = 1.0; + } + int visible = 1; + if (row >> visible) { + sample.visible = visible != 0; + } + if (sample.cx < -1.0 || sample.cx > 2.0 || sample.cy < -1.0 || sample.cy > 2.0) { + continue; + } + sample.typeIndex = std::max(0, std::min(kMaxCursorAtlasEntries - 1, sample.typeIndex)); + sample.bounceScale = std::max(0.5, std::min(2.0, sample.bounceScale)); + track->samples.push_back(sample); + } + if (track->samples.empty()) { + fail("No cursor samples were loaded: " + options.cursorSamplesPath); + } + return track; +} + +struct ZoomSample { + double timeMs = 0.0; + double scale = 1.0; + double x = 0.0; + double y = 0.0; +}; + +struct ZoomTrack { + std::vector samples; + + ZoomSample sampleAt(double timeMs) const { + if (samples.empty()) { + return {}; + } + if (timeMs <= samples.front().timeMs) { + return samples.front(); + } + if (timeMs >= samples.back().timeMs) { + return samples.back(); + } + + int low = 0; + int high = static_cast(samples.size()) - 1; + while (low < high - 1) { + const int mid = (low + high) / 2; + if (samples[mid].timeMs <= timeMs) { + low = mid; + } else { + high = mid; + } + } + + const ZoomSample& left = samples[low]; + const ZoomSample& right = samples[high]; + const double span = right.timeMs - left.timeMs; + if (span <= 0.0) { + return left; + } + + const double t = (timeMs - left.timeMs) / span; + return { + timeMs, + left.scale + (right.scale - left.scale) * t, + left.x + (right.x - left.x) * t, + left.y + (right.y - left.y) * t, + }; + } +}; + +std::unique_ptr loadZoomTrack(const Options& options) { + if (options.zoomSamplesPath.empty()) { + return nullptr; + } + + std::ifstream input(options.zoomSamplesPath); + if (!input) { + fail("Failed to open zoom samples: " + options.zoomSamplesPath); + } + + auto track = std::make_unique(); + std::string line; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + std::replace(line.begin(), line.end(), ',', ' '); + std::istringstream row(line); + ZoomSample sample; + if (!(row >> sample.timeMs >> sample.scale >> sample.x >> sample.y)) { + continue; + } + if (!std::isfinite(sample.timeMs) || !std::isfinite(sample.scale) || + !std::isfinite(sample.x) || !std::isfinite(sample.y)) { + continue; + } + sample.timeMs = std::max(0.0, sample.timeMs); + sample.scale = std::max(0.01, sample.scale); + track->samples.push_back(sample); + } + if (track->samples.empty()) { + fail("No zoom samples were loaded: " + options.zoomSamplesPath); + } + std::sort(track->samples.begin(), track->samples.end(), [](const auto& left, const auto& right) { + return left.timeMs < right.timeMs; + }); + return track; +} + +struct CursorAtlasEntry { + int x = 0; + int y = 0; + int width = 0; + int height = 0; + double anchorX = 0.0; + double anchorY = 0.0; + double aspectRatio = 0.0; + bool valid = false; +}; + +struct WebcamCacheState { + WebcamFrameCache* cache = nullptr; +}; + +void cacheMappedWebcamFrame( + CUdeviceptr dpSrcFrame, + unsigned int nSrcPitch, + int width, + int height, + int surfaceHeight, + int64_t, + void* userData) { + auto* state = static_cast(userData); + if (state->cache->width == 0) { + state->cache->width = width; + state->cache->height = height; + } + if (width != state->cache->width || height != state->cache->height) { + std::ostringstream stream; + stream << "Decoded webcam frame size changed from " << state->cache->width << "x" << state->cache->height + << " to " << width << "x" << height; + fail(stream.str()); + } + + const auto copyStart = std::chrono::steady_clock::now(); + const size_t expectedBytes = static_cast(width) * static_cast(height) * 3 / 2; + unsigned char* frame = nullptr; + checkCuda(cudaMalloc(&frame, expectedBytes), "cudaMalloc webcam cached frame"); + + CUDA_MEMCPY2D copy = {}; + copy.srcMemoryType = CU_MEMORYTYPE_DEVICE; + copy.srcDevice = dpSrcFrame; + copy.srcPitch = nSrcPitch; + copy.dstMemoryType = CU_MEMORYTYPE_DEVICE; + copy.dstDevice = reinterpret_cast(frame); + copy.dstPitch = width; + copy.WidthInBytes = width; + copy.Height = height; + checkCu(cuMemcpy2D(©), "cuMemcpy2D webcam luma"); + + copy.srcDevice = dpSrcFrame + nSrcPitch * surfaceHeight; + copy.dstDevice = reinterpret_cast(frame + width * height); + copy.Height = height / 2; + checkCu(cuMemcpy2D(©), "cuMemcpy2D webcam chroma"); + + state->cache->pushFrame(frame); + const auto copyEnd = std::chrono::steady_clock::now(); + state->cache->copyMs += elapsedMs(copyStart, copyEnd); +} + +class WebcamStreamDecoder { +public: + WebcamStreamDecoder(CUcontext context, const Options& options) + : options_(options), + chunk_(static_cast(options.chunkMb) * 1024 * 1024) { + if (options_.webcamInputFrames <= 0 || options_.webcamTargetFrames <= 0) { + fail("--webcam-input-frames and --webcam-target-frames are required with --webcam-annexb"); + } + if (options_.webcamSourceWidth <= 0 || options_.webcamSourceHeight <= 0) { + fail("--webcam-source-width and --webcam-source-height are required with --webcam-annexb"); + } + + const int cropSide = std::min(options_.webcamSourceWidth, options_.webcamSourceHeight) & ~1; + const int cropLeft = ((options_.webcamSourceWidth - cropSide) / 2) & ~1; + const int cropTop = ((options_.webcamSourceHeight - cropSide) / 2) & ~1; + crop_ = Rect{cropLeft, cropTop, cropLeft + cropSide, cropTop + cropSide}; + + cacheState_.cache = &cache_; + decoder_ = + std::make_unique(context, 0, 0, true, cudaVideoCodec_H264, nullptr, true, true, &crop_, nullptr); + decoder_->SetMappedFrameHandler(cacheMappedWebcamFrame, &cacheState_); + + input_.open(options_.webcamAnnexbPath, std::ios::binary); + if (!input_) { + fail("Failed to open webcam input: " + options_.webcamAnnexbPath); + } + } + + WebcamFrameCache* cache() { + return &cache_; + } + + void ensureFrame(int frameIndex) { + if (frameIndex < 0) { + return; + } + while (!flushed_ && cache_.decodedFrames <= frameIndex) { + input_.read(reinterpret_cast(chunk_.data()), static_cast(chunk_.size())); + const int bytesRead = static_cast(input_.gcount()); + const auto decodeStart = std::chrono::steady_clock::now(); + if (bytesRead > 0) { + decoder_->Decode(chunk_.data(), bytesRead, &frames_, &returnedFrames_); + } else { + decoder_->Decode(nullptr, 0, &frames_, &returnedFrames_); + flushed_ = true; + } + const auto decodeEnd = std::chrono::steady_clock::now(); + cache_.decodeMs += elapsedMs(decodeStart, decodeEnd); + } + if (cache_.frames.empty()) { + fail("No webcam frames were decoded"); + } + } + + void dropBefore(int frameIndex) { + cache_.dropBefore(frameIndex); + } + +private: + const Options& options_; + WebcamFrameCache cache_; + WebcamCacheState cacheState_{}; + Rect crop_{}; + std::unique_ptr decoder_; + std::ifstream input_; + std::vector chunk_; + uint8_t** frames_ = nullptr; + int returnedFrames_ = 0; + bool flushed_ = false; +}; + +std::unique_ptr createWebcamStreamDecoder(CUcontext context, const Options& options) { + if (options.webcamAnnexbPath.empty()) { + return nullptr; + } + return std::make_unique(context, options); +} + +__global__ void copyNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight) { + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= dstWidth || y >= dstHeight) { + return; + } + + const int sx = min(srcWidth - 1, (x * srcWidth) / dstWidth); + const int sy = min(srcHeight - 1, (y * srcHeight) / dstHeight); + dst[y * dstPitch + x] = src[sy * srcPitch + sx]; + + if ((x % 2) == 0 && (y % 2) == 0) { + const int suvX = min(srcWidth - 2, ((x * srcWidth) / dstWidth) & ~1); + const int suvY = min((srcHeight / 2) - 1, (y * srcHeight / dstHeight) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + suvY * srcPitch + suvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } +} + +__global__ void fillNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + unsigned char yValue, + unsigned char uValue, + unsigned char vValue) { + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= dstWidth || y >= dstHeight) { + return; + } + + dst[y * dstPitch + x] = yValue; + if ((x % 2) == 0 && (y % 2) == 0) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = uValue; + dstUv[1] = vValue; + } +} + +__device__ bool isInsideRoundedRect( + int x, + int y, + int left, + int top, + int width, + int height, + int radius) { + if (x < left || y < top || x >= left + width || y >= top + height) { + return false; + } + if (radius <= 0) { + return true; + } + + const int right = left + width - 1; + const int bottom = top + height - 1; + const int innerLeft = left + radius; + const int innerRight = right - radius; + const int innerTop = top + radius; + const int innerBottom = bottom - radius; + if ((x >= innerLeft && x <= innerRight) || (y >= innerTop && y <= innerBottom)) { + return true; + } + + const int cx = x < innerLeft ? innerLeft : innerRight; + const int cy = y < innerTop ? innerTop : innerBottom; + const int dx = x - cx; + const int dy = y - cy; + return dx * dx + dy * dy <= radius * radius; +} + +__global__ void overlayContentRectNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight, + int sourceCropX, + int sourceCropY, + int sourceCropWidth, + int sourceCropHeight) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (localX >= contentWidth || localY >= contentHeight) { + return; + } + + const int x = contentX + localX; + const int y = contentY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + const int cropWidth = max(1, min(sourceCropWidth > 0 ? sourceCropWidth : srcWidth, srcWidth - sourceCropX)); + const int cropHeight = max(1, min(sourceCropHeight > 0 ? sourceCropHeight : srcHeight, srcHeight - sourceCropY)); + const int cropX = max(0, min(sourceCropX, srcWidth - 1)); + const int cropY = max(0, min(sourceCropY, srcHeight - 1)); + const int srcX = min(srcWidth - 1, cropX + (localX * cropWidth) / contentWidth); + const int srcY = min(srcHeight - 1, cropY + (localY * cropHeight) / contentHeight); + dst[y * dstPitch + x] = src[srcY * srcPitch + srcX]; + + if ((x % 2) == 0 && (y % 2) == 0) { + const int localUvX = max(0, min(contentWidth - 1, localX + 1)); + const int localUvY = max(0, min(contentHeight - 1, localY + 1)); + const int srcUvX = min(srcWidth - 2, (cropX + ((localUvX * cropWidth) / contentWidth)) & ~1); + const int srcUvY = min((srcHeight / 2) - 1, (cropY + ((localUvY * cropHeight) / contentHeight)) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + srcUvY * srcPitch + srcUvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } +} + +__global__ void overlayContentTransformNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int regionX, + int regionY, + int regionWidth, + int regionHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight, + int radius, + float zoomScale, + float invZoomScale, + float srcScaleX, + float srcScaleY, + int sourceCropX, + int sourceCropY, + float zoomX, + float zoomY) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (localX >= regionWidth || localY >= regionHeight) { + return; + } + + const int x = regionX + localX; + const int y = regionY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + const float layoutXf = (static_cast(x) - zoomX) * invZoomScale; + const float layoutYf = (static_cast(y) - zoomY) * invZoomScale; + const int layoutX = __float2int_rd(layoutXf); + const int layoutY = __float2int_rd(layoutYf); + if (!isInsideRoundedRect(layoutX, layoutY, contentX, contentY, contentWidth, contentHeight, radius)) { + return; + } + + const float localContentX = + fminf(static_cast(contentWidth - 1), fmaxf(0.0f, layoutXf - contentX)); + const float localContentY = + fminf(static_cast(contentHeight - 1), fmaxf(0.0f, layoutYf - contentY)); + const int cropX = max(0, min(sourceCropX, srcWidth - 1)); + const int cropY = max(0, min(sourceCropY, srcHeight - 1)); + const int sx = min(srcWidth - 1, cropX + __float2int_rd(localContentX * srcScaleX)); + const int sy = min(srcHeight - 1, cropY + __float2int_rd(localContentY * srcScaleY)); + dst[y * dstPitch + x] = src[sy * srcPitch + sx]; + + if ((x % 2) == 0 && (y % 2) == 0 && x + 1 < dstWidth && y + 1 < dstHeight) { + const float uvLayoutXf = (static_cast(x + 1) - zoomX) * invZoomScale; + const float uvLayoutYf = (static_cast(y + 1) - zoomY) * invZoomScale; + const int uvLayoutX = __float2int_rd(uvLayoutXf); + const int uvLayoutY = __float2int_rd(uvLayoutYf); + if (isInsideRoundedRect( + uvLayoutX, + uvLayoutY, + contentX, + contentY, + contentWidth, + contentHeight, + radius)) { + const float uvLocalContentX = + fminf(static_cast(contentWidth - 1), fmaxf(0.0f, uvLayoutXf - contentX)); + const float uvLocalContentY = + fminf(static_cast(contentHeight - 1), fmaxf(0.0f, uvLayoutYf - contentY)); + const int suvX = + min(srcWidth - 2, (cropX + __float2int_rd(uvLocalContentX * srcScaleX)) & ~1); + const int suvY = + min((srcHeight / 2) - 1, (cropY + __float2int_rd(uvLocalContentY * srcScaleY)) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + suvY * srcPitch + suvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } + } +} + +__global__ void restoreRoundedContentCornersNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight, + int radius, + unsigned char backgroundY, + unsigned char backgroundU, + unsigned char backgroundV, + const unsigned char* background) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (localX >= radius || localY >= radius) { + return; + } + + const int corner = blockIdx.z; + const bool right = corner == 1 || corner == 3; + const bool bottom = corner >= 2; + const int x = right ? contentX + contentWidth - radius + localX : contentX + localX; + const int y = bottom ? contentY + contentHeight - radius + localY : contentY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + if (!isInsideRoundedRect(x, y, contentX, contentY, contentWidth, contentHeight, radius)) { + dst[y * dstPitch + x] = background ? background[y * dstWidth + x] : backgroundY; + } + + if ((x % 2) == 0 && (y % 2) == 0 && + !isInsideRoundedRect(x + 1, y + 1, contentX, contentY, contentWidth, contentHeight, radius)) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + if (background) { + const unsigned char* bgUv = background + dstWidth * dstHeight + (y / 2) * dstWidth + x; + dstUv[0] = bgUv[0]; + dstUv[1] = bgUv[1]; + } else { + dstUv[0] = backgroundU; + dstUv[1] = backgroundV; + } + } +} + +__device__ bool pointInCursorPolygon(float x, float y, bool inner) { + constexpr int kCount = 7; + const float outerX[kCount] = {2.0f, 61.0f, 45.0f, 57.0f, 38.0f, 27.0f, 13.0f}; + const float outerY[kCount] = {2.0f, 61.0f, 63.0f, 91.0f, 95.0f, 67.0f, 79.0f}; + const float innerX[kCount] = {10.0f, 52.0f, 37.0f, 49.0f, 40.0f, 28.0f, 18.0f}; + const float innerY[kCount] = {11.0f, 53.0f, 53.0f, 78.0f, 83.0f, 57.0f, 66.0f}; + const float* px = inner ? innerX : outerX; + const float* py = inner ? innerY : outerY; + + bool inside = false; + for (int index = 0, previous = kCount - 1; index < kCount; previous = index++) { + const bool crosses = ((py[index] > y) != (py[previous] > y)) && + (x < (px[previous] - px[index]) * (y - py[index]) / (py[previous] - py[index]) + px[index]); + if (crosses) { + inside = !inside; + } + } + return inside; +} + +__device__ int cursorMaskAt( + int x, + int y, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight) { + if (cursorWidth <= 0 || cursorHeight <= 0 || x < cursorX || y < cursorY || + x >= cursorX + cursorWidth || y >= cursorY + cursorHeight) { + return 0; + } + + const float localX = static_cast(x - cursorX) * 64.0f / static_cast(cursorWidth); + const float localY = static_cast(y - cursorY) * 96.0f / static_cast(cursorHeight); + if (pointInCursorPolygon(localX, localY, true)) { + return 2; + } + if (pointInCursorPolygon(localX, localY, false)) { + return 1; + } + return 0; +} + +__device__ unsigned char clampByteDevice(int value) { + return static_cast(min(255, max(0, value))); +} + +__device__ unsigned char blendByte(unsigned char base, unsigned char overlay, int alpha) { + return static_cast( + (static_cast(base) * (255 - alpha) + static_cast(overlay) * alpha + 127) / 255); +} + +__device__ bool sampleCursorAtlasNv12( + const unsigned char* atlas, + int atlasWidth, + int atlasHeight, + int entryX, + int entryY, + int entryWidth, + int entryHeight, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + int x, + int y, + unsigned char* outY, + unsigned char* outU, + unsigned char* outV, + int* outAlpha) { + if (!atlas || cursorWidth <= 0 || cursorHeight <= 0 || entryWidth <= 0 || entryHeight <= 0 || + x < cursorX || y < cursorY || x >= cursorX + cursorWidth || y >= cursorY + cursorHeight) { + return false; + } + + const int localX = max(0, min(cursorWidth - 1, x - cursorX)); + const int localY = max(0, min(cursorHeight - 1, y - cursorY)); + const int sampleX = entryX + min(entryWidth - 1, (localX * entryWidth) / cursorWidth); + const int sampleY = entryY + min(entryHeight - 1, (localY * entryHeight) / cursorHeight); + if (sampleX < 0 || sampleY < 0 || sampleX >= atlasWidth || sampleY >= atlasHeight) { + return false; + } + + const int offset = (sampleY * atlasWidth + sampleX) * 4; + const int alpha = atlas[offset + 3]; + if (alpha <= 0) { + return false; + } + + const int r = atlas[offset]; + const int g = atlas[offset + 1]; + const int b = atlas[offset + 2]; + *outY = clampByteDevice(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16); + *outU = clampByteDevice(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128); + *outV = clampByteDevice(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128); + *outAlpha = alpha; + return true; +} + +__device__ int sampleCursorAtlasAlpha( + const unsigned char* atlas, + int atlasWidth, + int atlasHeight, + int entryX, + int entryY, + int entryWidth, + int entryHeight, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + int x, + int y) { + if (!atlas || cursorWidth <= 0 || cursorHeight <= 0 || entryWidth <= 0 || entryHeight <= 0 || + x < cursorX || y < cursorY || x >= cursorX + cursorWidth || y >= cursorY + cursorHeight) { + return 0; + } + + const int localX = max(0, min(cursorWidth - 1, x - cursorX)); + const int localY = max(0, min(cursorHeight - 1, y - cursorY)); + const int sampleX = entryX + min(entryWidth - 1, (localX * entryWidth) / cursorWidth); + const int sampleY = entryY + min(entryHeight - 1, (localY * entryHeight) / cursorHeight); + if (sampleX < 0 || sampleY < 0 || sampleX >= atlasWidth || sampleY >= atlasHeight) { + return 0; + } + + return atlas[(sampleY * atlasWidth + sampleX) * 4 + 3]; +} + +__device__ int sampleCursorAtlasShadowAlpha( + const unsigned char* atlas, + int atlasWidth, + int atlasHeight, + int entryX, + int entryY, + int entryWidth, + int entryHeight, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + int x, + int y) { + int weightedAlpha = 0; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX, + cursorY + 2, + cursorWidth, + cursorHeight, + x, + y) * 20; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX - 2, + cursorY + 2, + cursorWidth, + cursorHeight, + x, + y) * 6; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX + 2, + cursorY + 2, + cursorWidth, + cursorHeight, + x, + y) * 6; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y) * 4; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX, + cursorY + 4, + cursorWidth, + cursorHeight, + x, + y) * 4; + return min(255, weightedAlpha / 100); +} + +__global__ void compositeStaticNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight, + int sourceCropX, + int sourceCropY, + int sourceCropWidth, + int sourceCropHeight, + int radius, + unsigned char backgroundY, + unsigned char backgroundU, + unsigned char backgroundV, + const unsigned char* background, + int shadowOffsetY, + int shadowIntensityPct, + const unsigned char* webcam, + int webcamX, + int webcamY, + int webcamSize, + int webcamFrameWidth, + int webcamFrameHeight, + int webcamRadius, + bool webcamMirror, + bool cursorVisible, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + const unsigned char* cursorAtlasRgba, + int cursorAtlasWidth, + int cursorAtlasHeight, + int cursorAtlasEntryX, + int cursorAtlasEntryY, + int cursorAtlasEntryWidth, + int cursorAtlasEntryHeight, + bool zoomEnabled, + float zoomScale, + float zoomX, + float zoomY) { + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= dstWidth || y >= dstHeight) { + return; + } + + const bool zoomActive = zoomEnabled && zoomScale > 0.01f; + const float safeZoomScale = fmaxf(zoomScale, 0.01f); + const float layoutXf = zoomActive ? (static_cast(x) - zoomX) / safeZoomScale : static_cast(x); + const float layoutYf = zoomActive ? (static_cast(y) - zoomY) / safeZoomScale : static_cast(y); + const int layoutX = static_cast(floorf(layoutXf)); + const int layoutY = static_cast(floorf(layoutYf)); + + const int cropX = max(0, min(sourceCropX, srcWidth - 1)); + const int cropY = max(0, min(sourceCropY, srcHeight - 1)); + const int cropWidth = max(1, min(sourceCropWidth > 0 ? sourceCropWidth : srcWidth, srcWidth - cropX)); + const int cropHeight = max(1, min(sourceCropHeight > 0 ? sourceCropHeight : srcHeight, srcHeight - cropY)); + const bool inside = isInsideRoundedRect(layoutX, layoutY, contentX, contentY, contentWidth, contentHeight, radius); + unsigned char outY = background ? background[y * dstWidth + x] : backgroundY; + if (inside) { + const float localX = fminf(static_cast(contentWidth - 1), fmaxf(0.0f, layoutXf - contentX)); + const float localY = fminf(static_cast(contentHeight - 1), fmaxf(0.0f, layoutYf - contentY)); + const int sx = min(srcWidth - 1, cropX + static_cast((localX * cropWidth) / contentWidth)); + const int sy = min(srcHeight - 1, cropY + static_cast((localY * cropHeight) / contentHeight)); + outY = src[sy * srcPitch + sx]; + } else { + const bool shadowInside = + shadowIntensityPct > 0 && + isInsideRoundedRect( + layoutX, + layoutY, + contentX, + contentY + shadowOffsetY, + contentWidth, + contentHeight, + radius + 8); + if (shadowInside) { + const int darkenPct = min(75, max(0, shadowIntensityPct / 2)); + outY = static_cast((static_cast(outY) * (100 - darkenPct)) / 100); + } + } + if (webcam && isInsideRoundedRect(x, y, webcamX, webcamY, webcamSize, webcamSize, webcamRadius)) { + const int localX = max(0, min(webcamSize - 1, x - webcamX)); + const int localY = max(0, min(webcamSize - 1, y - webcamY)); + const int sampleX = min(webcamFrameWidth - 1, (localX * webcamFrameWidth) / webcamSize); + const int sampleY = min(webcamFrameHeight - 1, (localY * webcamFrameHeight) / webcamSize); + const int mirroredX = webcamMirror ? webcamFrameWidth - 1 - sampleX : sampleX; + outY = webcam[sampleY * webcamFrameWidth + mirroredX]; + } + unsigned char cursorYValue = 0; + unsigned char cursorUValue = 128; + unsigned char cursorVValue = 128; + int cursorAlpha = 0; + const int cursorShadowAlpha = cursorVisible && cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y) + : 0; + if (cursorShadowAlpha > 0) { + outY = blendByte(outY, 16, cursorShadowAlpha); + } + const bool cursorAtlasHit = + cursorVisible && + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y, + &cursorYValue, + &cursorUValue, + &cursorVValue, + &cursorAlpha); + if (cursorAtlasHit) { + outY = blendByte(outY, cursorYValue, cursorAlpha); + } else { + const int cursorMask = cursorVisible && !cursorAtlasRgba + ? cursorMaskAt(x, y, cursorX, cursorY, cursorWidth, cursorHeight) + : 0; + if (cursorMask == 1) { + outY = 235; + } else if (cursorMask == 2) { + outY = 16; + } + } + dst[y * dstPitch + x] = outY; + + if ((x % 2) == 0 && (y % 2) == 0) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + const float uvLayoutXf = + zoomActive ? (static_cast(x + 1) - zoomX) / safeZoomScale : static_cast(x + 1); + const float uvLayoutYf = + zoomActive ? (static_cast(y + 1) - zoomY) / safeZoomScale : static_cast(y + 1); + const int uvLayoutX = static_cast(floorf(uvLayoutXf)); + const int uvLayoutY = static_cast(floorf(uvLayoutYf)); + const bool uvInside = isInsideRoundedRect( + uvLayoutX, + uvLayoutY, + contentX, + contentY, + contentWidth, + contentHeight, + radius); + if (uvInside) { + const float localX = fminf(static_cast(contentWidth - 1), fmaxf(0.0f, uvLayoutXf - contentX)); + const float localY = fminf(static_cast(contentHeight - 1), fmaxf(0.0f, uvLayoutYf - contentY)); + const int suvX = + min(srcWidth - 2, (cropX + static_cast((localX * cropWidth) / contentWidth)) & ~1); + const int suvY = + min((srcHeight / 2) - 1, (cropY + static_cast(localY * cropHeight / contentHeight)) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + suvY * srcPitch + suvX; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } else { + if (background) { + const unsigned char* bgUv = background + dstWidth * dstHeight + (y / 2) * dstWidth + x; + dstUv[0] = bgUv[0]; + dstUv[1] = bgUv[1]; + } else { + dstUv[0] = backgroundU; + dstUv[1] = backgroundV; + } + } + if (webcam && + isInsideRoundedRect( + x + 1, + y + 1, + webcamX, + webcamY, + webcamSize, + webcamSize, + webcamRadius)) { + const int localX = max(0, min(webcamSize - 1, x + 1 - webcamX)); + const int localY = max(0, min(webcamSize - 1, y + 1 - webcamY)); + const int sampleX = min(webcamFrameWidth - 1, (localX * webcamFrameWidth) / webcamSize); + const int sampleY = min(webcamFrameHeight - 1, (localY * webcamFrameHeight) / webcamSize); + const int mirroredX = webcamMirror ? webcamFrameWidth - 1 - sampleX : sampleX; + const int webcamUvX = min(webcamFrameWidth - 2, mirroredX & ~1); + const int webcamUvY = min((webcamFrameHeight / 2) - 1, sampleY / 2); + const unsigned char* webcamUv = + webcam + webcamFrameWidth * webcamFrameHeight + webcamUvY * webcamFrameWidth + webcamUvX; + dstUv[0] = webcamUv[0]; + dstUv[1] = webcamUv[1]; + } + unsigned char cursorUvY = 0; + unsigned char cursorUvU = 128; + unsigned char cursorUvV = 128; + int cursorUvAlpha = 0; + const int cursorUvShadowAlpha = cursorVisible && cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1) + : 0; + if (cursorUvShadowAlpha > 0) { + dstUv[0] = blendByte(dstUv[0], 128, cursorUvShadowAlpha); + dstUv[1] = blendByte(dstUv[1], 128, cursorUvShadowAlpha); + } + const bool cursorAtlasUvHit = + cursorVisible && + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1, + &cursorUvY, + &cursorUvU, + &cursorUvV, + &cursorUvAlpha); + if (cursorAtlasUvHit) { + dstUv[0] = blendByte(dstUv[0], cursorUvU, cursorUvAlpha); + dstUv[1] = blendByte(dstUv[1], cursorUvV, cursorUvAlpha); + } else { + const int cursorUvMask = + cursorVisible && !cursorAtlasRgba + ? cursorMaskAt(x + 1, y + 1, cursorX, cursorY, cursorWidth, cursorHeight) + : 0; + if (cursorUvMask > 0) { + dstUv[0] = 128; + dstUv[1] = 128; + } + } + } +} + +__global__ void overlayWebcamNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + const unsigned char* webcam, + int regionX, + int regionY, + int regionWidth, + int regionHeight, + int webcamX, + int webcamY, + int webcamSize, + int webcamFrameWidth, + int webcamFrameHeight, + int webcamRadius, + bool webcamMirror) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (!webcam || localX >= regionWidth || localY >= regionHeight) { + return; + } + + const int x = regionX + localX; + const int y = regionY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + if (isInsideRoundedRect(x, y, webcamX, webcamY, webcamSize, webcamSize, webcamRadius)) { + const int webcamLocalX = max(0, min(webcamSize - 1, x - webcamX)); + const int webcamLocalY = max(0, min(webcamSize - 1, y - webcamY)); + const int sampleX = min(webcamFrameWidth - 1, (webcamLocalX * webcamFrameWidth) / webcamSize); + const int sampleY = min(webcamFrameHeight - 1, (webcamLocalY * webcamFrameHeight) / webcamSize); + const int mirroredX = webcamMirror ? webcamFrameWidth - 1 - sampleX : sampleX; + dst[y * dstPitch + x] = webcam[sampleY * webcamFrameWidth + mirroredX]; + } + + if ((x % 2) == 0 && (y % 2) == 0 && x + 1 < dstWidth && y + 1 < dstHeight && + isInsideRoundedRect(x + 1, y + 1, webcamX, webcamY, webcamSize, webcamSize, webcamRadius)) { + const int uvLocalX = max(0, min(webcamSize - 1, x + 1 - webcamX)); + const int uvLocalY = max(0, min(webcamSize - 1, y + 1 - webcamY)); + const int uvSampleX = min(webcamFrameWidth - 1, (uvLocalX * webcamFrameWidth) / webcamSize); + const int uvSampleY = min(webcamFrameHeight - 1, (uvLocalY * webcamFrameHeight) / webcamSize); + const int uvMirroredX = webcamMirror ? webcamFrameWidth - 1 - uvSampleX : uvSampleX; + const int webcamUvX = min(webcamFrameWidth - 2, uvMirroredX & ~1); + const int webcamUvY = min((webcamFrameHeight / 2) - 1, uvSampleY / 2); + const unsigned char* webcamUv = + webcam + webcamFrameWidth * webcamFrameHeight + webcamUvY * webcamFrameWidth + webcamUvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = webcamUv[0]; + dstUv[1] = webcamUv[1]; + } +} + +__global__ void overlayCursorNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int regionX, + int regionY, + int regionWidth, + int regionHeight, + bool cursorVisible, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + const unsigned char* cursorAtlasRgba, + int cursorAtlasWidth, + int cursorAtlasHeight, + int cursorAtlasEntryX, + int cursorAtlasEntryY, + int cursorAtlasEntryWidth, + int cursorAtlasEntryHeight) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (!cursorVisible || localX >= regionWidth || localY >= regionHeight) { + return; + } + + const int x = regionX + localX; + const int y = regionY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + unsigned char outY = dst[y * dstPitch + x]; + unsigned char cursorYValue = 0; + unsigned char cursorUValue = 128; + unsigned char cursorVValue = 128; + int cursorAlpha = 0; + const int cursorShadowAlpha = cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y) + : 0; + if (cursorShadowAlpha > 0) { + outY = blendByte(outY, 16, cursorShadowAlpha); + } + const bool cursorAtlasHit = + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y, + &cursorYValue, + &cursorUValue, + &cursorVValue, + &cursorAlpha); + if (cursorAtlasHit) { + outY = blendByte(outY, cursorYValue, cursorAlpha); + } else { + const int cursorMask = !cursorAtlasRgba + ? cursorMaskAt(x, y, cursorX, cursorY, cursorWidth, cursorHeight) + : 0; + if (cursorMask == 1) { + outY = 235; + } else if (cursorMask == 2) { + outY = 16; + } + } + dst[y * dstPitch + x] = outY; + + if ((x % 2) == 0 && (y % 2) == 0 && x + 1 < dstWidth && y + 1 < dstHeight) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + unsigned char cursorUvY = 0; + unsigned char cursorUvU = 128; + unsigned char cursorUvV = 128; + int cursorUvAlpha = 0; + const int cursorUvShadowAlpha = cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1) + : 0; + if (cursorUvShadowAlpha > 0) { + dstUv[0] = blendByte(dstUv[0], 128, cursorUvShadowAlpha); + dstUv[1] = blendByte(dstUv[1], 128, cursorUvShadowAlpha); + } + const bool cursorAtlasUvHit = + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1, + &cursorUvY, + &cursorUvU, + &cursorUvV, + &cursorUvAlpha); + if (cursorAtlasUvHit) { + dstUv[0] = blendByte(dstUv[0], cursorUvU, cursorUvAlpha); + dstUv[1] = blendByte(dstUv[1], cursorUvV, cursorUvAlpha); + } else { + const int cursorUvMask = + !cursorAtlasRgba ? cursorMaskAt(x + 1, y + 1, cursorX, cursorY, cursorWidth, cursorHeight) : 0; + if (cursorUvMask > 0) { + dstUv[0] = 128; + dstUv[1] = 128; + } + } + } +} + +__global__ void prewarmKernel(unsigned int* state, unsigned int seed) { + const unsigned int index = blockIdx.x * blockDim.x + threadIdx.x; + unsigned int value = seed ^ (index * 747796405u + 2891336453u); + for (int iteration = 0; iteration < 256; ++iteration) { + value = value * 1664525u + 1013904223u; + value ^= value >> 13; + } + state[index] = value; +} + +void prewarmCuda(int durationMs) { + if (durationMs <= 0) { + return; + } + + constexpr int blockSize = 256; + constexpr int blockCount = 256; + unsigned int* state = nullptr; + checkCuda(cudaMalloc(&state, blockSize * blockCount * sizeof(unsigned int)), "cudaMalloc prewarm"); + + const auto start = std::chrono::steady_clock::now(); + int iteration = 0; + while (elapsedMs(start, std::chrono::steady_clock::now()) < durationMs) { + prewarmKernel<<>>(state, static_cast(iteration++)); + checkCuda(cudaGetLastError(), "prewarmKernel"); + checkCuda(cudaDeviceSynchronize(), "cudaDeviceSynchronize prewarm"); + } + + checkCuda(cudaFree(state), "cudaFree prewarm"); +} + +GUID getNvencPresetGuid(const std::string& encodingMode) { + return encodingMode == "fast" ? NV_ENC_PRESET_HP_GUID : NV_ENC_PRESET_HQ_GUID; +} + +uint32_t getNvencMaxBitrate(uint32_t bitrate, const std::string& encodingMode) { + const uint64_t multiplier = encodingMode == "fast" ? 3 : 2; + const uint64_t divisor = encodingMode == "fast" ? 2 : 1; + return static_cast( + std::min(0xffffffffu, (static_cast(bitrate) * multiplier) / divisor)); +} + +uint32_t getNvencBufferSize(uint32_t bitrate, const std::string& encodingMode) { + const uint64_t multiplier = encodingMode == "fast" ? 1 : 2; + return static_cast( + std::min(0xffffffffu, static_cast(bitrate) * multiplier)); +} + +class NvencSink { +public: + NvencSink( + CUcontext context, + int width, + int height, + int fps, + uint32_t bitrate, + const std::string& outputPath, + bool streamSync, + Options layoutOptions, + const WebcamFrameCache* webcamCache, + const CursorTrack* cursorTrack, + const ZoomTrack* zoomTrack) + : encoder_(context, width, height, NV_ENC_BUFFER_FORMAT_NV12), + width_(width), + height_(height), + fps_(fps), + streamSync_(streamSync), + layoutOptions_(layoutOptions), + webcamCache_(webcamCache), + cursorTrack_(cursorTrack), + zoomTrack_(zoomTrack) { + loadBackgroundFrame(); + loadWebcamFrame(); + loadCursorAtlas(); + if (streamSync_) { + checkCuda(cudaStreamCreateWithFlags(©Stream_, cudaStreamNonBlocking), "cudaStreamCreateWithFlags"); + } + + NV_ENC_INITIALIZE_PARAMS initializeParams = {NV_ENC_INITIALIZE_PARAMS_VER}; + NV_ENC_CONFIG encodeConfig = {NV_ENC_CONFIG_VER}; + initializeParams.encodeConfig = &encodeConfig; + encoder_.CreateDefaultEncoderParams( + &initializeParams, + NV_ENC_CODEC_H264_GUID, + getNvencPresetGuid(layoutOptions_.encodingMode)); + + initializeParams.frameRateNum = static_cast(fps); + initializeParams.frameRateDen = 1; + initializeParams.enableEncodeAsync = 1; + encodeConfig.profileGUID = NV_ENC_H264_PROFILE_HIGH_GUID; + encodeConfig.gopLength = static_cast(fps * 2); + encodeConfig.frameIntervalP = 1; + encodeConfig.rcParams.rateControlMode = NV_ENC_PARAMS_RC_VBR; + encodeConfig.rcParams.averageBitRate = bitrate; + encodeConfig.rcParams.maxBitRate = getNvencMaxBitrate(bitrate, layoutOptions_.encodingMode); + encodeConfig.rcParams.vbvBufferSize = getNvencBufferSize(bitrate, layoutOptions_.encodingMode); + encodeConfig.rcParams.vbvInitialDelay = bitrate; + if (layoutOptions_.encodingMode != "fast") { + encodeConfig.rcParams.enableAQ = 1; + encodeConfig.rcParams.aqStrength = layoutOptions_.encodingMode == "quality" ? 10 : 8; + } + encodeConfig.encodeCodecConfig.h264Config.idrPeriod = encodeConfig.gopLength; + encoder_.CreateEncoder(&initializeParams); + + output_.open(outputPath, std::ios::binary); + if (!output_) { + fail("Failed to open output: " + outputPath); + } + } + + void encodeFrame( + const unsigned char* srcFrame, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + int outputFrameIndex) { + const NvEncInputFrame* inputFrame = encoder_.GetNextInputFrame(); + const dim3 block(16, 16); + const dim3 grid((width_ + block.x - 1) / block.x, (height_ + block.y - 1) / block.y); + const double outputFrameTimeMs = + static_cast(outputFrameIndex) * 1000.0 / static_cast(fps_); + const double frameTimeMs = + outputToSourceMs(layoutOptions_.timelineSegments, outputFrameTimeMs); + const unsigned char* webcamFrame = selectWebcamFrame(frameTimeMs); + const ZoomSample zoomSample = zoomTrack_ ? zoomTrack_->sampleAt(frameTimeMs) : ZoomSample{}; + const bool zoomEnabled = zoomTrack_ && zoomSample.scale > 0.01; + const CursorPosition cursorPosition = cursorTrack_ + ? cursorTrack_->positionAt(frameTimeMs) + : CursorPosition{}; + const CursorAtlasEntry* cursorEntry = cursorAtlasEntryFor(cursorPosition.typeIndex); + const bool useCursorAtlas = cursorEntry && cursorAtlasDevice_; + const int cursorHeight = layoutOptions_.cursorHeight > 0 + ? std::max(1, static_cast(std::round(layoutOptions_.cursorHeight * cursorPosition.bounceScale))) + : 0; + const double cursorAspectRatio = useCursorAtlas ? cursorEntry->aspectRatio : (618.0 / 958.0); + const int cursorWidth = + cursorHeight > 0 ? std::max(1, static_cast(std::round(cursorHeight * cursorAspectRatio))) : 0; + const int cursorHotspotX = useCursorAtlas + ? static_cast(std::round(cursorWidth * cursorEntry->anchorX)) + : cursorWidth * 14 / 100; + const int cursorHotspotY = useCursorAtlas + ? static_cast(std::round(cursorHeight * cursorEntry->anchorY)) + : cursorHeight * 6 / 100; + const double cursorHotspotContentX = + layoutOptions_.contentX + cursorPosition.cx * layoutOptions_.contentWidth; + const double cursorHotspotContentY = + layoutOptions_.contentY + cursorPosition.cy * layoutOptions_.contentHeight; + const double cursorHotspotOutputX = + zoomEnabled ? cursorHotspotContentX * zoomSample.scale + zoomSample.x : cursorHotspotContentX; + const double cursorHotspotOutputY = + zoomEnabled ? cursorHotspotContentY * zoomSample.scale + zoomSample.y : cursorHotspotContentY; + const int cursorX = cursorPosition.visible + ? static_cast(std::round(cursorHotspotOutputX)) - cursorHotspotX + : 0; + const int cursorY = cursorPosition.visible + ? static_cast(std::round(cursorHotspotOutputY)) - cursorHotspotY + : 0; + const bool hasSourceCrop = + layoutOptions_.sourceCropWidth >= 2 && + layoutOptions_.sourceCropHeight >= 2; + const int sourceCropX = hasSourceCrop + ? std::max(0, std::min(layoutOptions_.sourceCropX, srcWidth - 2)) & ~1 + : 0; + const int sourceCropY = hasSourceCrop + ? std::max(0, std::min(layoutOptions_.sourceCropY, srcHeight - 2)) & ~1 + : 0; + const int sourceCropWidth = hasSourceCrop + ? std::max(2, std::min(layoutOptions_.sourceCropWidth, srcWidth - sourceCropX)) & ~1 + : srcWidth; + const int sourceCropHeight = hasSourceCrop + ? std::max(2, std::min(layoutOptions_.sourceCropHeight, srcHeight - sourceCropY)) & ~1 + : srcHeight; + const bool zoomChangesLayout = + zoomTrack_ && + (std::abs(zoomSample.scale - 1.0) > 0.001 || + std::abs(zoomSample.x) > 0.5 || + std::abs(zoomSample.y) > 0.5); + const bool useFastRoiComposite = + canUseFastRoiComposite(zoomChangesLayout); + const bool useLayeredStaticRoiComposite = + !useFastRoiComposite && canUseLayeredStaticRoiComposite(zoomChangesLayout); + const auto compositeStart = std::chrono::steady_clock::now(); + if (useFastRoiComposite) { + copyNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_); + checkCuda(cudaGetLastError(), "copyNv12Kernel fast ROI base"); + + if (webcamFrame && layoutOptions_.webcamSize > 0) { + const int webcamRegionX = std::max(0, layoutOptions_.webcamX - 1); + const int webcamRegionY = std::max(0, layoutOptions_.webcamY - 1); + const int webcamRegionRight = std::min( + width_, + layoutOptions_.webcamX + layoutOptions_.webcamSize); + const int webcamRegionBottom = std::min( + height_, + layoutOptions_.webcamY + layoutOptions_.webcamSize); + const int webcamRegionWidth = webcamRegionRight - webcamRegionX; + const int webcamRegionHeight = webcamRegionBottom - webcamRegionY; + if (webcamRegionWidth > 0 && webcamRegionHeight > 0) { + const dim3 webcamGrid( + (webcamRegionWidth + block.x - 1) / block.x, + (webcamRegionHeight + block.y - 1) / block.y); + overlayWebcamNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + webcamFrame, + webcamRegionX, + webcamRegionY, + webcamRegionWidth, + webcamRegionHeight, + layoutOptions_.webcamX, + layoutOptions_.webcamY, + layoutOptions_.webcamSize, + webcamFrameWidth(), + webcamFrameHeight(), + layoutOptions_.webcamRadius, + layoutOptions_.webcamMirror); + checkCuda(cudaGetLastError(), "overlayWebcamNv12Kernel"); + } + } + + if (cursorPosition.visible && cursorWidth > 0 && cursorHeight > 0) { + const int cursorPadding = useCursorAtlas ? 4 : 2; + const int regionX = std::max(0, cursorX - cursorPadding); + const int regionY = std::max(0, cursorY - cursorPadding); + const int regionRight = std::min(width_, cursorX + cursorWidth + cursorPadding); + const int regionBottom = std::min(height_, cursorY + cursorHeight + cursorPadding); + const int regionWidth = regionRight - regionX; + const int regionHeight = regionBottom - regionY; + if (regionWidth > 0 && regionHeight > 0) { + const dim3 cursorGrid( + (regionWidth + block.x - 1) / block.x, + (regionHeight + block.y - 1) / block.y); + overlayCursorNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + regionX, + regionY, + regionWidth, + regionHeight, + cursorPosition.visible, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + useCursorAtlas ? cursorAtlasDevice_ : nullptr, + cursorAtlasWidth_, + cursorAtlasHeight_, + useCursorAtlas ? cursorEntry->x : 0, + useCursorAtlas ? cursorEntry->y : 0, + useCursorAtlas ? cursorEntry->width : 0, + useCursorAtlas ? cursorEntry->height : 0); + checkCuda(cudaGetLastError(), "overlayCursorNv12Kernel"); + } + } + ++roiCompositeFrames_; + } else if (useLayeredStaticRoiComposite) { + if (backgroundDevice_) { + checkCuda( + cudaMemcpy2DAsync( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + backgroundDevice_, + static_cast(width_), + static_cast(width_), + static_cast(height_), + cudaMemcpyDeviceToDevice, + copyStream_), + "cudaMemcpy2DAsync layered ROI background Y"); + checkCuda( + cudaMemcpy2DAsync( + static_cast(inputFrame->inputPtr) + + static_cast(inputFrame->chromaOffsets[0]), + static_cast(inputFrame->pitch), + backgroundDevice_ + width_ * height_, + static_cast(width_), + static_cast(width_), + static_cast(height_ / 2), + cudaMemcpyDeviceToDevice, + copyStream_), + "cudaMemcpy2DAsync layered ROI background UV"); + } else { + fillNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + clampByte(layoutOptions_.backgroundY), + clampByte(layoutOptions_.backgroundU), + clampByte(layoutOptions_.backgroundV)); + checkCuda(cudaGetLastError(), "fillNv12Kernel layered ROI background"); + } + + const float safeZoomScale = std::max(0.01f, static_cast(zoomSample.scale)); + const float invZoomScale = 1.0f / safeZoomScale; + const float srcScaleX = + static_cast(sourceCropWidth) / static_cast(std::max(1, layoutOptions_.contentWidth)); + const float srcScaleY = + static_cast(sourceCropHeight) / static_cast(std::max(1, layoutOptions_.contentHeight)); + const int transformedContentX = zoomChangesLayout + ? static_cast(std::floor(layoutOptions_.contentX * safeZoomScale + zoomSample.x)) + : layoutOptions_.contentX; + const int transformedContentY = zoomChangesLayout + ? static_cast(std::floor(layoutOptions_.contentY * safeZoomScale + zoomSample.y)) + : layoutOptions_.contentY; + const int transformedContentRight = zoomChangesLayout + ? static_cast( + std::ceil( + (layoutOptions_.contentX + layoutOptions_.contentWidth) * + safeZoomScale + + zoomSample.x)) + : layoutOptions_.contentX + layoutOptions_.contentWidth; + const int transformedContentBottom = zoomChangesLayout + ? static_cast( + std::ceil( + (layoutOptions_.contentY + layoutOptions_.contentHeight) * + safeZoomScale + + zoomSample.y)) + : layoutOptions_.contentY + layoutOptions_.contentHeight; + const int contentRegionLeft = std::max(0, transformedContentX); + const int contentRegionTop = std::max(0, transformedContentY); + const int contentRegionRight = std::min(width_, transformedContentRight); + const int contentRegionBottom = std::min(height_, transformedContentBottom); + const int contentRegionWidth = contentRegionRight - contentRegionLeft; + const int contentRegionHeight = contentRegionBottom - contentRegionTop; + if (contentRegionWidth > 0 && contentRegionHeight > 0) { + const dim3 contentGrid( + (contentRegionWidth + block.x - 1) / block.x, + (contentRegionHeight + block.y - 1) / block.y); + if (zoomChangesLayout) { + overlayContentTransformNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + contentRegionLeft, + contentRegionTop, + contentRegionWidth, + contentRegionHeight, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight, + layoutOptions_.radius, + safeZoomScale, + invZoomScale, + srcScaleX, + srcScaleY, + sourceCropX, + sourceCropY, + static_cast(zoomSample.x), + static_cast(zoomSample.y)); + checkCuda(cudaGetLastError(), "overlayContentTransformNv12Kernel"); + } else { + overlayContentRectNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight, + sourceCropX, + sourceCropY, + sourceCropWidth, + sourceCropHeight); + checkCuda(cudaGetLastError(), "overlayContentRectNv12Kernel"); + + const int cornerRadius = std::min( + layoutOptions_.radius, + std::min(layoutOptions_.contentWidth, layoutOptions_.contentHeight) / 2); + if (cornerRadius > 0) { + const dim3 cornerGrid( + (cornerRadius + block.x - 1) / block.x, + (cornerRadius + block.y - 1) / block.y, + 4); + restoreRoundedContentCornersNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight, + cornerRadius, + clampByte(layoutOptions_.backgroundY), + clampByte(layoutOptions_.backgroundU), + clampByte(layoutOptions_.backgroundV), + backgroundDevice_); + checkCuda(cudaGetLastError(), "restoreRoundedContentCornersNv12Kernel"); + } + } + } + + if (webcamFrame && layoutOptions_.webcamSize > 0) { + const int webcamRegionX = std::max(0, layoutOptions_.webcamX - 1); + const int webcamRegionY = std::max(0, layoutOptions_.webcamY - 1); + const int webcamRegionRight = std::min( + width_, + layoutOptions_.webcamX + layoutOptions_.webcamSize); + const int webcamRegionBottom = std::min( + height_, + layoutOptions_.webcamY + layoutOptions_.webcamSize); + const int webcamRegionWidth = webcamRegionRight - webcamRegionX; + const int webcamRegionHeight = webcamRegionBottom - webcamRegionY; + if (webcamRegionWidth > 0 && webcamRegionHeight > 0) { + const dim3 webcamGrid( + (webcamRegionWidth + block.x - 1) / block.x, + (webcamRegionHeight + block.y - 1) / block.y); + overlayWebcamNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + webcamFrame, + webcamRegionX, + webcamRegionY, + webcamRegionWidth, + webcamRegionHeight, + layoutOptions_.webcamX, + layoutOptions_.webcamY, + layoutOptions_.webcamSize, + webcamFrameWidth(), + webcamFrameHeight(), + layoutOptions_.webcamRadius, + layoutOptions_.webcamMirror); + checkCuda(cudaGetLastError(), "overlayWebcamNv12Kernel layered ROI"); + } + } + + if (cursorPosition.visible && cursorWidth > 0 && cursorHeight > 0) { + const int cursorPadding = useCursorAtlas ? 4 : 2; + const int regionX = std::max(0, cursorX - cursorPadding); + const int regionY = std::max(0, cursorY - cursorPadding); + const int regionRight = std::min(width_, cursorX + cursorWidth + cursorPadding); + const int regionBottom = std::min(height_, cursorY + cursorHeight + cursorPadding); + const int regionWidth = regionRight - regionX; + const int regionHeight = regionBottom - regionY; + if (regionWidth > 0 && regionHeight > 0) { + const dim3 cursorGrid( + (regionWidth + block.x - 1) / block.x, + (regionHeight + block.y - 1) / block.y); + overlayCursorNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + regionX, + regionY, + regionWidth, + regionHeight, + cursorPosition.visible, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + useCursorAtlas ? cursorAtlasDevice_ : nullptr, + cursorAtlasWidth_, + cursorAtlasHeight_, + useCursorAtlas ? cursorEntry->x : 0, + useCursorAtlas ? cursorEntry->y : 0, + useCursorAtlas ? cursorEntry->width : 0, + useCursorAtlas ? cursorEntry->height : 0); + checkCuda(cudaGetLastError(), "overlayCursorNv12Kernel layered ROI"); + } + } + ++roiCompositeFrames_; + } else if (hasStaticLayout(layoutOptions_)) { + compositeStaticNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight, + sourceCropX, + sourceCropY, + sourceCropWidth, + sourceCropHeight, + layoutOptions_.radius, + clampByte(layoutOptions_.backgroundY), + clampByte(layoutOptions_.backgroundU), + clampByte(layoutOptions_.backgroundV), + backgroundDevice_, + layoutOptions_.shadowOffsetY, + layoutOptions_.shadowIntensityPct, + webcamFrame, + layoutOptions_.webcamX, + layoutOptions_.webcamY, + layoutOptions_.webcamSize, + webcamFrameWidth(), + webcamFrameHeight(), + layoutOptions_.webcamRadius, + layoutOptions_.webcamMirror, + cursorPosition.visible, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + useCursorAtlas ? cursorAtlasDevice_ : nullptr, + cursorAtlasWidth_, + cursorAtlasHeight_, + useCursorAtlas ? cursorEntry->x : 0, + useCursorAtlas ? cursorEntry->y : 0, + useCursorAtlas ? cursorEntry->width : 0, + useCursorAtlas ? cursorEntry->height : 0, + zoomEnabled, + static_cast(zoomSample.scale), + static_cast(zoomSample.x), + static_cast(zoomSample.y)); + checkCuda(cudaGetLastError(), "compositeStaticNv12Kernel"); + ++monolithicCompositeFrames_; + } else { + copyNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_); + checkCuda(cudaGetLastError(), "copyNv12Kernel"); + ++copyCompositeFrames_; + } + if (streamSync_) { + checkCuda(cudaStreamSynchronize(copyStream_), "cudaStreamSynchronize copy"); + } else { + checkCuda(cudaDeviceSynchronize(), "cudaDeviceSynchronize"); + } + const auto compositeEnd = std::chrono::steady_clock::now(); + compositeMs_ += elapsedMs(compositeStart, compositeEnd); + + std::vector> packets; + const auto nvencStart = std::chrono::steady_clock::now(); + encoder_.EncodeFrame(packets); + const auto nvencEnd = std::chrono::steady_clock::now(); + nvencMs_ += elapsedMs(nvencStart, nvencEnd); + const auto writeStart = std::chrono::steady_clock::now(); + writePackets(packets); + const auto writeEnd = std::chrono::steady_clock::now(); + packetWriteMs_ += elapsedMs(writeStart, writeEnd); + ++frames_; + } + + void finish() { + std::vector> packets; + encoder_.EndEncode(packets); + writePackets(packets); + encoder_.DestroyEncoder(); + output_.close(); + if (copyStream_) { + checkCuda(cudaStreamDestroy(copyStream_), "cudaStreamDestroy"); + copyStream_ = nullptr; + } + if (backgroundDevice_) { + checkCuda(cudaFree(backgroundDevice_), "cudaFree backgroundDevice"); + backgroundDevice_ = nullptr; + } + if (webcamDevice_) { + checkCuda(cudaFree(webcamDevice_), "cudaFree webcamDevice"); + webcamDevice_ = nullptr; + } + if (cursorAtlasDevice_) { + checkCuda(cudaFree(cursorAtlasDevice_), "cudaFree cursorAtlasDevice"); + cursorAtlasDevice_ = nullptr; + } + } + + uint64_t outputBytes() const { + return outputBytes_; + } + + double compositeMs() const { + return compositeMs_; + } + + double nvencMs() const { + return nvencMs_; + } + + double packetWriteMs() const { + return packetWriteMs_; + } + + int roiCompositeFrames() const { + return roiCompositeFrames_; + } + + int monolithicCompositeFrames() const { + return monolithicCompositeFrames_; + } + + int copyCompositeFrames() const { + return copyCompositeFrames_; + } + +private: + bool canUseFastRoiComposite(bool zoomChangesLayout) const { + return hasStaticLayout(layoutOptions_) && + layoutOptions_.contentX == 0 && + layoutOptions_.contentY == 0 && + layoutOptions_.contentWidth == width_ && + layoutOptions_.contentHeight == height_ && + layoutOptions_.radius == 0 && + layoutOptions_.shadowIntensityPct == 0 && + backgroundDevice_ == nullptr && + layoutOptions_.sourceCropWidth <= 0 && + layoutOptions_.sourceCropHeight <= 0 && + !zoomChangesLayout; + } + + bool canUseLayeredStaticRoiComposite(bool zoomChangesLayout) const { + return hasStaticLayout(layoutOptions_) && + layoutOptions_.contentWidth > 0 && + layoutOptions_.contentHeight > 0 && + layoutOptions_.contentX < width_ && + layoutOptions_.contentY < height_ && + layoutOptions_.contentX + layoutOptions_.contentWidth > 0 && + layoutOptions_.contentY + layoutOptions_.contentHeight > 0 && + layoutOptions_.shadowIntensityPct == 0; + } + + const unsigned char* selectWebcamFrame(double sourceTimeMs) const { + if (webcamCache_ && !webcamCache_->frames.empty()) { + return webcamCache_->frameAt(webcamFrameIndexForSourceTimeMs(sourceTimeMs, layoutOptions_)); + } + return webcamDevice_; + } + + int webcamFrameWidth() const { + return webcamCache_ ? webcamCache_->width : layoutOptions_.webcamSize; + } + + int webcamFrameHeight() const { + return webcamCache_ ? webcamCache_->height : layoutOptions_.webcamSize; + } + + void loadBackgroundFrame() { + if (layoutOptions_.backgroundNv12Path.empty()) { + return; + } + + const size_t expectedBytes = static_cast(width_) * static_cast(height_) * 3 / 2; + std::vector bytes(expectedBytes); + std::ifstream input(layoutOptions_.backgroundNv12Path, std::ios::binary); + if (!input) { + fail("Failed to open background NV12: " + layoutOptions_.backgroundNv12Path); + } + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(input.gcount()) != expectedBytes) { + fail("Background NV12 has an unexpected size: " + layoutOptions_.backgroundNv12Path); + } + + checkCuda(cudaMalloc(&backgroundDevice_, expectedBytes), "cudaMalloc backgroundDevice"); + checkCuda( + cudaMemcpy(backgroundDevice_, bytes.data(), expectedBytes, cudaMemcpyHostToDevice), + "cudaMemcpy backgroundDevice"); + } + + void loadWebcamFrame() { + if (layoutOptions_.webcamNv12Path.empty()) { + return; + } + + const size_t expectedBytes = + static_cast(layoutOptions_.webcamSize) * static_cast(layoutOptions_.webcamSize) * 3 / 2; + std::vector bytes(expectedBytes); + std::ifstream input(layoutOptions_.webcamNv12Path, std::ios::binary); + if (!input) { + fail("Failed to open webcam NV12: " + layoutOptions_.webcamNv12Path); + } + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(input.gcount()) != expectedBytes) { + fail("Webcam NV12 has an unexpected size: " + layoutOptions_.webcamNv12Path); + } + + checkCuda(cudaMalloc(&webcamDevice_, expectedBytes), "cudaMalloc webcamDevice"); + checkCuda( + cudaMemcpy(webcamDevice_, bytes.data(), expectedBytes, cudaMemcpyHostToDevice), + "cudaMemcpy webcamDevice"); + } + + const CursorAtlasEntry* cursorAtlasEntryFor(int typeIndex) const { + if (typeIndex < 0 || typeIndex >= kMaxCursorAtlasEntries) { + return nullptr; + } + const CursorAtlasEntry& entry = cursorAtlasEntries_[typeIndex]; + return entry.valid ? &entry : nullptr; + } + + void loadCursorAtlas() { + if (layoutOptions_.cursorAtlasRgbaPath.empty()) { + return; + } + if (layoutOptions_.cursorAtlasMetadataPath.empty() || + layoutOptions_.cursorAtlasWidth <= 0 || + layoutOptions_.cursorAtlasHeight <= 0) { + fail("Cursor atlas requires metadata, width, and height"); + } + + std::ifstream metadata(layoutOptions_.cursorAtlasMetadataPath); + if (!metadata) { + fail("Failed to open cursor atlas metadata: " + layoutOptions_.cursorAtlasMetadataPath); + } + + int loadedEntries = 0; + int index = 0; + CursorAtlasEntry entry; + while (metadata >> index >> entry.x >> entry.y >> entry.width >> entry.height >> + entry.anchorX >> entry.anchorY >> entry.aspectRatio) { + if (index < 0 || index >= kMaxCursorAtlasEntries || entry.width <= 0 || entry.height <= 0) { + continue; + } + entry.valid = true; + cursorAtlasEntries_[index] = entry; + ++loadedEntries; + } + if (loadedEntries == 0) { + fail("No cursor atlas entries were loaded: " + layoutOptions_.cursorAtlasMetadataPath); + } + + cursorAtlasWidth_ = layoutOptions_.cursorAtlasWidth; + cursorAtlasHeight_ = layoutOptions_.cursorAtlasHeight; + const size_t expectedBytes = + static_cast(cursorAtlasWidth_) * static_cast(cursorAtlasHeight_) * 4; + std::vector bytes(expectedBytes); + std::ifstream input(layoutOptions_.cursorAtlasRgbaPath, std::ios::binary); + if (!input) { + fail("Failed to open cursor atlas RGBA: " + layoutOptions_.cursorAtlasRgbaPath); + } + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(input.gcount()) != expectedBytes) { + fail("Cursor atlas RGBA has an unexpected size: " + layoutOptions_.cursorAtlasRgbaPath); + } + + checkCuda(cudaMalloc(&cursorAtlasDevice_, expectedBytes), "cudaMalloc cursorAtlasDevice"); + checkCuda( + cudaMemcpy(cursorAtlasDevice_, bytes.data(), expectedBytes, cudaMemcpyHostToDevice), + "cudaMemcpy cursorAtlasDevice"); + } + + void writePackets(const std::vector>& packets) { + for (const auto& packet : packets) { + if (packet.empty()) { + continue; + } + output_.write(reinterpret_cast(packet.data()), static_cast(packet.size())); + outputBytes_ += packet.size(); + } + } + + NvEncoderCuda encoder_; + std::ofstream output_; + int width_ = 0; + int height_ = 0; + int fps_ = 30; + int frames_ = 0; + uint64_t outputBytes_ = 0; + double compositeMs_ = 0.0; + double nvencMs_ = 0.0; + double packetWriteMs_ = 0.0; + int roiCompositeFrames_ = 0; + int monolithicCompositeFrames_ = 0; + int copyCompositeFrames_ = 0; + bool streamSync_ = false; + Options layoutOptions_; + unsigned char* backgroundDevice_ = nullptr; + unsigned char* webcamDevice_ = nullptr; + unsigned char* cursorAtlasDevice_ = nullptr; + CursorAtlasEntry cursorAtlasEntries_[kMaxCursorAtlasEntries]; + int cursorAtlasWidth_ = 0; + int cursorAtlasHeight_ = 0; + const WebcamFrameCache* webcamCache_ = nullptr; + const CursorTrack* cursorTrack_ = nullptr; + const ZoomTrack* zoomTrack_ = nullptr; + cudaStream_t copyStream_ = nullptr; +}; + +struct CallbackEncodeState { + CUcontext context = nullptr; + const Options* options = nullptr; + uint32_t bitrate = 0; + std::unique_ptr* sink = nullptr; + double* decodeMs = nullptr; + double* encodeMs = nullptr; + int* encodedFrames = nullptr; + int displayFrameIndex = 0; + const WebcamFrameCache* webcamCache = nullptr; + const CursorTrack* cursorTrack = nullptr; + const ZoomTrack* zoomTrack = nullptr; + ProgressReportState* progress = nullptr; + bool oneFramePerMappedDisplayFrame = false; + int mappedFrames = 0; + const std::vector* sourcePts = nullptr; + const std::vector* timelineSegments = nullptr; +}; + +ProgressCounters collectProgressCounters( + const NvencSink* sink, + const WebcamFrameCache* webcamCache, + double decodeWallMs, + double encodeMs) { + ProgressCounters counters; + counters.decodeWallMs = decodeWallMs; + counters.encodeMs = encodeMs; + if (sink) { + counters.compositeMs = sink->compositeMs(); + counters.nvencMs = sink->nvencMs(); + counters.packetWriteMs = sink->packetWriteMs(); + counters.roiCompositeFrames = sink->roiCompositeFrames(); + counters.monolithicCompositeFrames = sink->monolithicCompositeFrames(); + counters.copyCompositeFrames = sink->copyCompositeFrames(); + } + if (webcamCache) { + counters.webcamDecodeMs = webcamCache->decodeMs; + counters.webcamCopyMs = webcamCache->copyMs; + } + return counters; +} + +int maxCallbackOutputFrames(const Options& options) { + int maxOutputFrames = options.targetFrames > 0 + ? options.targetFrames + : std::numeric_limits::max(); + if (options.maxFrames > 0) { + maxOutputFrames = std::min(maxOutputFrames, options.maxFrames); + } + return maxOutputFrames; +} + +bool shouldContinueEncoding(int encodedFrames, const Options& options) { + return encodedFrames < maxCallbackOutputFrames(options); +} + +int expectedCallbackOutputFramesForSourceFrame( + int sourceFrameIndex, + const Options& options, + const std::vector* sourcePts, + const std::vector* timelineSegments) { + return expectedOutputFramesForSourceFrame( + sourceFrameIndex, + options.inputFrames, + options.targetFrames, + options.maxFrames, + options.fps, + sourcePts, + timelineSegments); +} + +void encodeMappedDisplayFrame( + CUdeviceptr dpSrcFrame, + unsigned int nSrcPitch, + int width, + int height, + int surfaceHeight, + int64_t, + void* userData) { + auto* state = static_cast(userData); + ++state->mappedFrames; + const int sourceFrameIndex = state->displayFrameIndex++; + const int maxOutputFrames = maxCallbackOutputFrames(*state->options); + if (*state->encodedFrames >= maxOutputFrames) { + return; + } + const int expectedOutputFrames = state->oneFramePerMappedDisplayFrame + ? *state->encodedFrames + 1 + : expectedCallbackOutputFramesForSourceFrame( + sourceFrameIndex, + *state->options, + state->sourcePts, + state->timelineSegments); + if (*state->encodedFrames >= expectedOutputFrames) { + return; + } + + if (!*state->sink) { + *state->sink = std::make_unique( + state->context, + outputWidthForSource(*state->options, width), + outputHeightForSource(*state->options, height), + state->options->fps, + state->bitrate, + state->options->outputPath, + state->options->streamSync, + *state->options, + state->webcamCache, + state->cursorTrack, + state->zoomTrack); + } + + while (*state->encodedFrames < expectedOutputFrames && *state->encodedFrames < maxOutputFrames) { + const auto encodeStart = std::chrono::steady_clock::now(); + (*state->sink)->encodeFrame( + reinterpret_cast(dpSrcFrame), + static_cast(nSrcPitch), + width, + height, + surfaceHeight, + *state->encodedFrames); + const auto encodeEnd = std::chrono::steady_clock::now(); + *state->encodeMs += elapsedMs(encodeStart, encodeEnd); + ++*state->encodedFrames; + if (state->progress) { + const NvencSink* activeSink = state->sink && *state->sink ? state->sink->get() : nullptr; + reportEncodingProgress( + *state->encodedFrames, + maxOutputFrames, + *state->progress, + collectProgressCounters( + activeSink, + state->webcamCache, + state->decodeMs ? *state->decodeMs : 0.0, + state->encodeMs ? *state->encodeMs : 0.0)); + } + } +} + +double elapsedMs(std::chrono::steady_clock::time_point start, std::chrono::steady_clock::time_point end) { + return std::chrono::duration(end - start).count(); +} + +void reportEncodingProgress( + int encodedFrames, + int totalFrames, + ProgressReportState& state, + const ProgressCounters& counters, + bool force) { + if (totalFrames <= 0) { + return; + } + + const auto now = std::chrono::steady_clock::now(); + if (!force && encodedFrames < totalFrames && encodedFrames % 30 != 0 && elapsedMs(state.lastReportAt, now) < 500.0) { + return; + } + + const double percentage = std::min(100.0, std::max(0.0, static_cast(encodedFrames) * 100.0 / totalFrames)); + const double elapsedSeconds = std::max(elapsedMs(state.startedAt, now) / 1000.0, 0.001); + const double averageFps = encodedFrames > 0 ? static_cast(encodedFrames) / elapsedSeconds : 0.0; + const double intervalMs = std::max(elapsedMs(state.lastReportAt, now), 0.0); + const int intervalFrames = std::max(0, encodedFrames - state.lastReportedFrame); + const double instantFps = + intervalMs > 0.0 && intervalFrames > 0 ? static_cast(intervalFrames) * 1000.0 / intervalMs : 0.0; + const double intervalEncodeMs = std::max(0.0, counters.encodeMs - state.lastCounters.encodeMs); + const double intervalCompositeMs = std::max(0.0, counters.compositeMs - state.lastCounters.compositeMs); + const double intervalNvencMs = std::max(0.0, counters.nvencMs - state.lastCounters.nvencMs); + const double intervalPacketWriteMs = std::max(0.0, counters.packetWriteMs - state.lastCounters.packetWriteMs); + const double intervalWebcamDecodeMs = std::max(0.0, counters.webcamDecodeMs - state.lastCounters.webcamDecodeMs); + const double intervalWebcamCopyMs = std::max(0.0, counters.webcamCopyMs - state.lastCounters.webcamCopyMs); + const double intervalDecodeWallMs = std::max(0.0, counters.decodeWallMs - state.lastCounters.decodeWallMs); + const double intervalPipelineWaitMs = std::max(0.0, intervalMs - intervalEncodeMs); + const int intervalRoiCompositeFrames = + std::max(0, counters.roiCompositeFrames - state.lastCounters.roiCompositeFrames); + const int intervalMonolithicCompositeFrames = + std::max(0, counters.monolithicCompositeFrames - state.lastCounters.monolithicCompositeFrames); + const int intervalCopyCompositeFrames = + std::max(0, counters.copyCompositeFrames - state.lastCounters.copyCompositeFrames); + std::cerr << std::fixed << std::setprecision(2) + << "PROGRESS {\"currentFrame\":" << encodedFrames + << ",\"totalFrames\":" << totalFrames + << ",\"percentage\":" << percentage + << ",\"averageFps\":" << averageFps + << ",\"instantFps\":" << instantFps + << ",\"intervalMs\":" << intervalMs + << ",\"intervalFrames\":" << intervalFrames + << ",\"intervalDecodeWallMs\":" << intervalDecodeWallMs + << ",\"intervalEncodeMs\":" << intervalEncodeMs + << ",\"intervalPipelineWaitMs\":" << intervalPipelineWaitMs + << ",\"intervalCompositeMs\":" << intervalCompositeMs + << ",\"intervalNvencMs\":" << intervalNvencMs + << ",\"intervalPacketWriteMs\":" << intervalPacketWriteMs + << ",\"intervalWebcamDecodeMs\":" << intervalWebcamDecodeMs + << ",\"intervalWebcamCopyMs\":" << intervalWebcamCopyMs + << ",\"intervalRoiCompositeFrames\":" << intervalRoiCompositeFrames + << ",\"intervalMonolithicCompositeFrames\":" << intervalMonolithicCompositeFrames + << ",\"intervalCopyCompositeFrames\":" << intervalCopyCompositeFrames + << "}" << std::endl; + state.lastReportAt = now; + state.lastReportedFrame = encodedFrames; + state.lastCounters = counters; +} + +} // namespace + +int main(int argc, char** argv) { + try { + Options options = parseOptions(argc, argv); + options.timelineSegments = loadTimelineMap(options.timelineMapPath); + if (!options.timelineSegments.empty() && !options.callbackEncode) { + fail("Timeline-map CUDA export requires --callback-encode"); + } + const uint32_t bitrate = static_cast(options.bitrateMbps) * 1000U * 1000U; + + checkCuda(cudaSetDevice(0), "cudaSetDevice"); + checkCu(cuInit(0), "cuInit"); + CUdevice device = 0; + checkCu(cuDeviceGet(&device, 0), "cuDeviceGet"); + CUcontext context = nullptr; + checkCu(cuCtxCreate(&context, 0, device), "cuCtxCreate"); + checkCu(cuCtxSetCurrent(context), "cuCtxSetCurrent"); + prewarmCuda(options.prewarmMs); + + std::ifstream input(options.inputPath, std::ios::binary); + if (!input) { + fail("Failed to open input: " + options.inputPath); + } + + std::unique_ptr webcamStream = createWebcamStreamDecoder(context, options); + const WebcamFrameCache* webcamCachePtr = webcamStream ? webcamStream->cache() : nullptr; + std::unique_ptr cursorTrack = loadCursorTrack(options); + const CursorTrack* cursorTrackPtr = cursorTrack.get(); + std::unique_ptr zoomTrack = loadZoomTrack(options); + const ZoomTrack* zoomTrackPtr = zoomTrack.get(); + const std::vector sourcePts = loadFramePts(options.sourcePtsPath); + const bool useSourcePts = + options.inputFrames > 0 && + sourcePts.size() >= static_cast(options.inputFrames); + if (!options.timelineSegments.empty() && !useSourcePts) { + fail("Timeline-map CUDA export requires source frame PTS"); + } + auto decoder = std::make_unique(context, 0, 0, true, cudaVideoCodec_H264, nullptr, true, true); + std::unique_ptr sink; + const bool useDecoderFramePolicy = + options.inputFrames > 0 && + options.targetFrames > 0 && + options.inputFrames >= options.targetFrames && + !useSourcePts && + !options.postSelect; + FrameSelectionState selectionState{ + options.inputFrames, + options.targetFrames, + options.maxFrames, + 0, + 0, + options.fps, + useSourcePts ? &sourcePts : nullptr, + options.timelineSegments.empty() ? nullptr : &options.timelineSegments, + }; + if (useDecoderFramePolicy) { + decoder->SetDisplayFramePolicy(shouldCopyDisplayFrame, &selectionState); + } + + std::vector chunk(static_cast(options.chunkMb) * 1024 * 1024); + uint8_t** frames = nullptr; + int returnedFrames = 0; + int sourceFrames = 0; + int encodedFrames = 0; + const auto totalStart = std::chrono::steady_clock::now(); + double decodeMs = 0.0; + double encodeMs = 0.0; + ProgressReportState progressState; + progressState.startedAt = std::chrono::steady_clock::now(); + progressState.lastReportAt = progressState.startedAt; + const int progressTotalFrames = maxCallbackOutputFrames(options); + reportEncodingProgress(0, progressTotalFrames, progressState, ProgressCounters{}, true); + CallbackEncodeState callbackState{ + context, + &options, + bitrate, + &sink, + &decodeMs, + &encodeMs, + &encodedFrames, + 0, + webcamCachePtr, + cursorTrackPtr, + zoomTrackPtr, + &progressState, + useDecoderFramePolicy, + 0, + useSourcePts ? &sourcePts : nullptr, + options.timelineSegments.empty() ? nullptr : &options.timelineSegments, + }; + if (options.callbackEncode) { + decoder->SetMappedFrameHandler(encodeMappedDisplayFrame, &callbackState); + } + + auto prepareWebcamFrames = [&]() { + if (!webcamStream) { + return; + } + int outputFrameIndex = encodedFrames + kWebcamPrefetchOutputFrames; + if (options.maxFrames > 0) { + outputFrameIndex = std::min(outputFrameIndex, options.maxFrames - 1); + } + if (options.targetFrames > 0) { + outputFrameIndex = std::min(outputFrameIndex, options.targetFrames - 1); + } + webcamStream->ensureFrame(webcamFrameIndexForOutputFrame(outputFrameIndex, options)); + + const int keepFromOutputFrame = std::max(0, encodedFrames - 8); + webcamStream->dropBefore(webcamFrameIndexForOutputFrame(keepFromOutputFrame, options)); + }; + + while (input && shouldContinueEncoding(encodedFrames, options)) { + prepareWebcamFrames(); + input.read(reinterpret_cast(chunk.data()), static_cast(chunk.size())); + const int bytesRead = static_cast(input.gcount()); + if (bytesRead <= 0) { + break; + } + + const auto decodeStart = std::chrono::steady_clock::now(); + decoder->Decode(chunk.data(), bytesRead, &frames, &returnedFrames); + const auto decodeEnd = std::chrono::steady_clock::now(); + decodeMs += elapsedMs(decodeStart, decodeEnd); + + for (int index = 0; index < returnedFrames; ++index) { + if (!shouldContinueEncoding(encodedFrames, options)) { + break; + } + const int sourceFrameIndex = sourceFrames++; + if (!useDecoderFramePolicy && !shouldEncodeFrame(sourceFrameIndex, encodedFrames, options)) { + continue; + } + if (!sink) { + sink = std::make_unique( + context, + outputWidthForSource(options, decoder->GetWidth()), + outputHeightForSource(options, decoder->GetHeight()), + options.fps, + bitrate, + options.outputPath, + options.streamSync, + options, + webcamCachePtr, + cursorTrackPtr, + zoomTrackPtr); + } + const auto encodeStart = std::chrono::steady_clock::now(); + sink->encodeFrame( + frames[index], + decoder->GetDeviceFramePitch(), + decoder->GetWidth(), + decoder->GetHeight(), + decoder->GetHeight(), + encodedFrames); + const auto encodeEnd = std::chrono::steady_clock::now(); + encodeMs += elapsedMs(encodeStart, encodeEnd); + ++encodedFrames; + reportEncodingProgress( + encodedFrames, + progressTotalFrames, + progressState, + collectProgressCounters(sink.get(), webcamCachePtr, decodeMs, encodeMs)); + } + } + + if (shouldContinueEncoding(encodedFrames, options)) { + const auto decodeStart = std::chrono::steady_clock::now(); + decoder->Decode(nullptr, 0, &frames, &returnedFrames); + const auto decodeEnd = std::chrono::steady_clock::now(); + decodeMs += elapsedMs(decodeStart, decodeEnd); + for (int index = 0; index < returnedFrames; ++index) { + if (!shouldContinueEncoding(encodedFrames, options)) { + break; + } + const int sourceFrameIndex = sourceFrames++; + if (!useDecoderFramePolicy && !shouldEncodeFrame(sourceFrameIndex, encodedFrames, options)) { + continue; + } + if (!sink) { + sink = std::make_unique( + context, + outputWidthForSource(options, decoder->GetWidth()), + outputHeightForSource(options, decoder->GetHeight()), + options.fps, + bitrate, + options.outputPath, + options.streamSync, + options, + webcamCachePtr, + cursorTrackPtr, + zoomTrackPtr); + } + const auto encodeStart = std::chrono::steady_clock::now(); + sink->encodeFrame( + frames[index], + decoder->GetDeviceFramePitch(), + decoder->GetWidth(), + decoder->GetHeight(), + decoder->GetHeight(), + encodedFrames); + const auto encodeEnd = std::chrono::steady_clock::now(); + encodeMs += elapsedMs(encodeStart, encodeEnd); + ++encodedFrames; + reportEncodingProgress( + encodedFrames, + progressTotalFrames, + progressState, + collectProgressCounters(sink.get(), webcamCachePtr, decodeMs, encodeMs)); + } + } + + if (!sink) { + fail("No decoded frames were produced"); + } + const auto flushStart = std::chrono::steady_clock::now(); + sink->finish(); + const auto flushEnd = std::chrono::steady_clock::now(); + const auto totalEnd = std::chrono::steady_clock::now(); + reportEncodingProgress( + encodedFrames, + progressTotalFrames, + progressState, + collectProgressCounters(sink.get(), webcamCachePtr, decodeMs, encodeMs), + true); + + const double totalMs = elapsedMs(totalStart, totalEnd); + const double mediaMs = static_cast(encodedFrames) * 1000.0 / options.fps; + const double measuredFps = static_cast(encodedFrames) / (totalMs / 1000.0); + const double realtime = mediaMs / totalMs; + const int reportedSourceFrames = + useDecoderFramePolicy ? selectionState.sourceFrames : + (options.callbackEncode ? decoder->GetDisplayFrameCount() : sourceFrames); + const int mappedDisplayFrames = options.callbackEncode ? callbackState.mappedFrames : encodedFrames; + const int selectedDisplayFrames = + useDecoderFramePolicy ? selectionState.selectedFrames : mappedDisplayFrames; + const int skippedDisplayFrames = + useDecoderFramePolicy ? std::max(0, selectionState.sourceFrames - selectionState.selectedFrames) : 0; + const double decodeOnlyApproxMs = options.callbackEncode ? std::max(0.0, decodeMs - encodeMs) : decodeMs; + + std::cout << std::fixed << std::setprecision(2) + << "{" + << "\"success\":true," + << "\"mode\":\"nvdec-cuda-nvenc-annexb\"," + << "\"selectionStage\":\"" + << (options.callbackEncode + ? (useDecoderFramePolicy ? "decoder-policy-mapped-callback" : "mapped-callback") + : (useDecoderFramePolicy ? "decoder" : "post")) + << "\"," + << "\"sourceTimestampMode\":\"" << (useSourcePts ? "pts" : "ordinal") << "\"," + << "\"timelineMap\":" << (!options.timelineSegments.empty() ? "true" : "false") << "," + << "\"timelineSegments\":" << options.timelineSegments.size() << "," + << "\"syncMode\":\"" << (options.streamSync ? "stream" : "device") << "\"," + << "\"prewarmMs\":" << options.prewarmMs << "," + << "\"chunkMb\":" << options.chunkMb << "," + << "\"width\":" << outputWidthForSource(options, decoder->GetWidth()) << "," + << "\"height\":" << outputHeightForSource(options, decoder->GetHeight()) << "," + << "\"fps\":" << options.fps << "," + << "\"encodingMode\":\"" << options.encodingMode << "\"," + << "\"staticLayout\":" << (hasStaticLayout(options) ? "true" : "false") << "," + << "\"contentX\":" << options.contentX << "," + << "\"contentY\":" << options.contentY << "," + << "\"contentWidth\":" << options.contentWidth << "," + << "\"contentHeight\":" << options.contentHeight << "," + << "\"radius\":" << options.radius << "," + << "\"backgroundImage\":" << (!options.backgroundNv12Path.empty() ? "true" : "false") << "," + << "\"shadowOffsetY\":" << options.shadowOffsetY << "," + << "\"shadowIntensityPct\":" << options.shadowIntensityPct << "," + << "\"webcamOverlay\":" << (hasWebcamOverlay(options) ? "true" : "false") << "," + << "\"webcamX\":" << options.webcamX << "," + << "\"webcamY\":" << options.webcamY << "," + << "\"webcamSize\":" << options.webcamSize << "," + << "\"webcamRadius\":" << options.webcamRadius << "," + << "\"webcamStream\":" << (!options.webcamAnnexbPath.empty() ? "true" : "false") << "," + << "\"webcamMirror\":" << (options.webcamMirror ? "true" : "false") << "," + << "\"webcamTimeOffsetMs\":" << options.webcamTimeOffsetMs << "," + << "\"webcamSourceDurationMs\":" << options.webcamSourceDurationMs << "," + << "\"webcamCachedFrames\":" << (webcamCachePtr ? webcamCachePtr->frames.size() : 0) << "," + << "\"webcamPeakCachedFrames\":" << (webcamCachePtr ? webcamCachePtr->peakFrames : 0) << "," + << "\"webcamCacheBaseFrame\":" << (webcamCachePtr ? webcamCachePtr->baseFrameIndex : 0) << "," + << "\"webcamDecodedFrames\":" << (webcamCachePtr ? webcamCachePtr->decodedFrames : 0) << "," + << "\"webcamDecodeMs\":" << (webcamCachePtr ? webcamCachePtr->decodeMs : 0.0) << "," + << "\"webcamCopyMs\":" << (webcamCachePtr ? webcamCachePtr->copyMs : 0.0) << "," + << "\"cursorOverlay\":" << (cursorTrackPtr ? "true" : "false") << "," + << "\"cursorSamples\":" << (cursorTrackPtr ? cursorTrackPtr->samples.size() : 0) << "," + << "\"cursorHeight\":" << options.cursorHeight << "," + << "\"cursorAtlas\":" << (!options.cursorAtlasRgbaPath.empty() ? "true" : "false") << "," + << "\"zoomOverlay\":" << (zoomTrackPtr ? "true" : "false") << "," + << "\"zoomSamples\":" << (zoomTrackPtr ? zoomTrackPtr->samples.size() : 0) << "," + << "\"sourceFrames\":" << reportedSourceFrames << "," + << "\"mappedDisplayFrames\":" << mappedDisplayFrames << "," + << "\"selectedDisplayFrames\":" << selectedDisplayFrames << "," + << "\"skippedDisplayFrames\":" << skippedDisplayFrames << "," + << "\"frames\":" << encodedFrames << "," + << "\"totalMs\":" << totalMs << "," + << "\"decodeMs\":" << decodeOnlyApproxMs << "," + << "\"decodeWallMs\":" << decodeMs << "," + << "\"encodeMs\":" << encodeMs << "," + << "\"compositeMs\":" << sink->compositeMs() << "," + << "\"roiCompositeFrames\":" << sink->roiCompositeFrames() << "," + << "\"monolithicCompositeFrames\":" << sink->monolithicCompositeFrames() << "," + << "\"copyCompositeFrames\":" << sink->copyCompositeFrames() << "," + << "\"nvencMs\":" << sink->nvencMs() << "," + << "\"packetWriteMs\":" << sink->packetWriteMs() << "," + << "\"flushMs\":" << elapsedMs(flushStart, flushEnd) << "," + << "\"measuredFps\":" << measuredFps << "," + << "\"realtimeMultiplier\":" << realtime << "," + << "\"outputBytes\":" << sink->outputBytes() << "," + << "\"outputPath\":\"" << options.outputPath << "\"" + << "}" << std::endl; + + sink.reset(); + decoder.reset(); + webcamStream.reset(); + checkCu(cuCtxDestroy(context), "cuCtxDestroy"); + return 0; + } catch (const std::exception& error) { + std::cerr << "{\"success\":false,\"error\":\"" << error.what() << "\"}" << std::endl; + return 1; + } +} diff --git a/electron/native/wgc-capture/CMakeLists.txt b/electron/native/wgc-capture/CMakeLists.txt new file mode 100644 index 000000000..15b0859a5 --- /dev/null +++ b/electron/native/wgc-capture/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.20) +project(wgc-capture LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(wgc-capture + src/main.cpp + src/wgc_session.cpp + src/mf_encoder.cpp + src/monitor_utils.cpp + src/wasapi_loopback.cpp +) + +target_compile_options(wgc-capture PRIVATE /EHsc /W3 /utf-8) + +target_link_libraries(wgc-capture PRIVATE + windowsapp + d3d11 + dxgi + mfplat + mfreadwrite + mf + mfuuid + ole32 + shcore + dwmapi +) diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp new file mode 100644 index 000000000..68a408efe --- /dev/null +++ b/electron/native/wgc-capture/src/main.cpp @@ -0,0 +1,538 @@ +#include "wgc_session.h" +#include "mf_encoder.h" +#include "monitor_utils.h" +#include "wasapi_loopback.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_stopRequested{false}; +static std::atomic g_pauseRequested{false}; +static std::atomic g_lastFrameTimestampHns{0}; +static std::atomic g_pauseStartTimestampHns{0}; +static std::atomic g_accumulatedPausedHns{0}; +static std::mutex g_stopMutex; +static std::condition_variable g_stopCv; + +static void reportMicrophoneCaptureUnavailable() { + std::cerr << "WARNING: Failed to initialize WASAPI mic capture" << std::endl; + // This stable stdout marker is ordered before "Recording started", allowing + // Electron to select its per-recording browser fallback without a pipe race. + std::cout << "MICROPHONE_CAPTURE_UNAVAILABLE" << std::endl; + std::cout.flush(); +} + +struct CaptureConfig { + int64_t displayId = 0; + int64_t windowHandle = 0; + std::string outputPath; + std::string audioOutputPath; + std::string micOutputPath; + std::string micDeviceId; + std::string micDeviceName; + int fps = 60; + int width = 0; + int height = 0; + int displayX = 0; + int displayY = 0; + int displayW = 0; + int displayH = 0; + bool hasDisplayBounds = false; + bool captureSystemAudio = false; + bool captureMic = false; +}; + +static bool parseSimpleJson(const std::string& json, CaptureConfig& config) { + auto findInt = [&](const std::string& key) -> int { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return -1; + pos = json.find(':', pos); + if (pos == std::string::npos) return -1; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; + try { + return std::stoi(json.substr(pos)); + } catch (...) { + return -1; + } + }; + + auto findInt64 = [&](const std::string& key) -> int64_t { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return -1; + pos = json.find(':', pos); + if (pos == std::string::npos) return -1; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; + try { + return std::stoll(json.substr(pos)); + } catch (...) { + return -1; + } + }; + + auto findString = [&](const std::string& key) -> std::string { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return ""; + pos = json.find(':', pos); + if (pos == std::string::npos) return ""; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; + if (pos >= json.size() || json[pos] != '"') return ""; + pos++; + std::string result; + while (pos < json.size() && json[pos] != '"') { + if (json[pos] == '\\' && pos + 1 < json.size()) { + pos++; + if (json[pos] == 'n') result += '\n'; + else if (json[pos] == 't') result += '\t'; + else if (json[pos] == '\\') result += '\\'; + else if (json[pos] == '"') result += '"'; + else if (json[pos] == '/') result += '/'; + else result += json[pos]; + } else { + result += json[pos]; + } + pos++; + } + return result; + }; + + config.outputPath = findString("outputPath"); + if (config.outputPath.empty()) return false; + + int64_t displayId = findInt64("displayId"); + if (displayId >= 0) config.displayId = displayId; + + int64_t windowHandle = findInt64("windowHandle"); + if (windowHandle > 0) config.windowHandle = windowHandle; + + int fps = findInt("fps"); + if (fps > 0) config.fps = fps; + + int width = findInt("width"); + if (width > 0) config.width = width; + + int height = findInt("height"); + if (height > 0) config.height = height; + + config.audioOutputPath = findString("audioOutputPath"); + config.micOutputPath = findString("micOutputPath"); + config.micDeviceId = findString("micDeviceId"); + config.micDeviceName = findString("micDeviceName"); + + auto findBool = [&](const std::string& key) -> bool { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return false; + auto colonPos = json.find(':', pos); + if (colonPos == std::string::npos) return false; + auto valStart = json.find_first_not_of(" \t", colonPos + 1); + return valStart != std::string::npos && json.substr(valStart, 4) == "true"; + }; + + config.captureSystemAudio = findBool("captureSystemAudio"); + config.captureMic = findBool("captureMic"); + + int dx = findInt("displayX"); + int dy = findInt("displayY"); + int dw = findInt("displayW"); + int dh = findInt("displayH"); + if (dw > 0 && dh > 0) { + config.displayX = dx; + config.displayY = dy; + config.displayW = dw; + config.displayH = dh; + config.hasDisplayBounds = true; + } + + return true; +} + +static std::wstring utf8ToWide(const std::string& str) { + if (str.empty()) return L""; + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0); + std::wstring wstr(len, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), &wstr[0], len); + return wstr; +} + +static int64_t queryPerformanceCounterHns() { + LARGE_INTEGER counter; + LARGE_INTEGER frequency; + if (!QueryPerformanceCounter(&counter) || !QueryPerformanceFrequency(&frequency) || frequency.QuadPart <= 0) { + return g_lastFrameTimestampHns.load(); + } + + return static_cast( + (static_cast(counter.QuadPart) * 10000000.0L) / + static_cast(frequency.QuadPart)); +} + +static int64_t adjustedVideoTimestampHns(int64_t timestampHns) { + int64_t accumulatedPausedHns = g_accumulatedPausedHns.load(); + if (g_pauseRequested.load()) { + const int64_t pauseStart = g_pauseStartTimestampHns.load(); + if (pauseStart > 0 && timestampHns > pauseStart) { + accumulatedPausedHns += (timestampHns - pauseStart); + } + } + + int64_t adjustedTimestampHns = timestampHns - accumulatedPausedHns; + if (adjustedTimestampHns < 0) { + adjustedTimestampHns = 0; + } + return adjustedTimestampHns; +} + +static void openActivePauseAt(int64_t timestampHns) { + g_pauseStartTimestampHns.store(timestampHns); + g_pauseRequested.store(true); +} + +static void closeActivePauseAt(int64_t timestampHns) { + const int64_t pauseStart = g_pauseStartTimestampHns.exchange(0); + if (pauseStart > 0 && timestampHns > pauseStart) { + g_accumulatedPausedHns.fetch_add(timestampHns - pauseStart); + } + g_pauseRequested.store(false); +} + +static void writeCompanionAudioTimingMetadata( + const std::string& audioPath, + int64_t firstVideoTimestampHns, + const WasapiCapture& capture +) { + if (audioPath.empty() || firstVideoTimestampHns < 0) { + return; + } + + const int64_t firstPacketQpcHns = capture.firstPacketQpcHns(); + if (firstPacketQpcHns < 0) { + return; + } + + int64_t startDelayMs = (firstPacketQpcHns - firstVideoTimestampHns + 5000) / 10000; + if (startDelayMs < 0) { + startDelayMs = 0; + } + + std::ofstream metadataFile(audioPath + ".json", std::ios::trunc); + if (!metadataFile) { + std::cerr << "WARNING: Failed to write audio timing metadata for " << audioPath << std::endl; + return; + } + + metadataFile << "{\"startDelayMs\":" << startDelayMs; + metadataFile << ",\"capturedDurationMs\":" << capture.capturedDurationMs(); + metadataFile << ",\"dataBytes\":" << capture.totalDataBytes(); + metadataFile << ",\"sampleRate\":" << capture.sampleRate(); + metadataFile << ",\"channels\":" << capture.channelCount(); + metadataFile << ",\"gapFillCount\":" << capture.gapFillCount(); + metadataFile << ",\"insertedSilenceFrames\":" << capture.insertedSilenceFrames(); + metadataFile << ",\"compactedDiscontinuityCount\":" << capture.compactedDiscontinuityCount(); + metadataFile << ",\"compactedDiscontinuityFrames\":" << capture.compactedDiscontinuityFrames(); + metadataFile << ",\"compactedSilentDiscontinuityCount\":" << capture.compactedSilentDiscontinuityCount(); + metadataFile << ",\"compactedSilentDiscontinuityFrames\":" << capture.compactedSilentDiscontinuityFrames(); + const uint32_t discontinuityCount = capture.dataDiscontinuityCount(); + if (discontinuityCount > 0) { + metadataFile << ",\"dataDiscontinuityCount\":" << discontinuityCount; + } + const uint32_t timestampErrorCount = capture.timestampErrorCount(); + if (timestampErrorCount > 0) { + metadataFile << ",\"timestampErrorCount\":" << timestampErrorCount; + } + metadataFile << "}"; +} + +static void stdinListenerThread() { + std::string line; + while (std::getline(std::cin, line)) { + // Trim whitespace + while (!line.empty() && (line.back() == '\r' || line.back() == '\n' || line.back() == ' ')) { + line.pop_back(); + } + + if (line == "pause") { + openActivePauseAt(queryPerformanceCounterHns()); + continue; + } + + if (line == "resume") { + closeActivePauseAt(queryPerformanceCounterHns()); + continue; + } + + if (line == "stop") { + g_stopRequested = true; + g_stopCv.notify_all(); + return; + } + } + + // stdin closed (parent process died) + g_stopRequested = true; + g_stopCv.notify_all(); +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "ERROR: Missing JSON config argument" << std::endl; + return 1; + } + + // Keep Win32 monitor/window rectangles in physical pixels. Without + // per-monitor awareness, mixed-DPI desktops can report virtualized bounds + // that do not line up with the WGC monitor texture. + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + winrt::init_apartment(winrt::apartment_type::multi_threaded); + + CaptureConfig config; + if (!parseSimpleJson(argv[1], config)) { + std::cerr << "ERROR: Failed to parse config JSON" << std::endl; + return 1; + } + + WgcSession session; + + if (config.windowHandle > 0) { + HWND hwnd = reinterpret_cast(static_cast(config.windowHandle)); + if (!IsWindow(hwnd)) { + std::cerr << "ERROR: Invalid window handle " << config.windowHandle << std::endl; + return 1; + } + if (!session.initialize(hwnd, config.fps)) { + std::cerr << "ERROR: Failed to initialize WGC window capture session" << std::endl; + return 1; + } + } else { + HMONITOR monitor = findMonitorByDisplayId(config.displayId); + if (!monitor && config.hasDisplayBounds) { + std::cerr << "Monitor ID match failed, attempting coordinate-based match: " + << config.displayX << "," << config.displayY << " " << config.displayW << "x" << config.displayH << std::endl; + monitor = findMonitorByBounds(config.displayX, config.displayY, config.displayW, config.displayH); + } + + if (!monitor) { + std::cerr << "ERROR: Could not find monitor for displayId " << config.displayId << std::endl; + return 1; + } + if (!session.initialize(monitor, config.fps)) { + std::cerr << "ERROR: Failed to initialize WGC capture session" << std::endl; + return 1; + } + } + + int captureWidth = config.width > 0 ? config.width : session.captureWidth(); + int captureHeight = config.height > 0 ? config.height : session.captureHeight(); + + // Ensure even dimensions for H.264 + captureWidth = (captureWidth / 2) * 2; + captureHeight = (captureHeight / 2) * 2; + + // Initialize encoder + MFEncoder encoder; + std::wstring outputPathW = utf8ToWide(config.outputPath); + if (!encoder.initialize(outputPathW, captureWidth, captureHeight, config.fps, + session.device(), session.context())) { + std::cerr << "ERROR: Failed to initialize Media Foundation encoder" << std::endl; + return 1; + } + + // Set up frame callback + std::atomic frameCount{0}; + std::atomic firstVideoTimestampHns{-1}; + std::atomic recordingStartedAnnounced{false}; + std::atomic captureSetupComplete{false}; + session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) { + g_lastFrameTimestampHns = timestampHns; + int64_t expectedFirstVideoTimestampHns = -1; + firstVideoTimestampHns.compare_exchange_strong(expectedFirstVideoTimestampHns, timestampHns); + if (g_stopRequested) return; + + if (g_pauseRequested) return; + + const int64_t adjustedTimestampHns = adjustedVideoTimestampHns(timestampHns); + + if (encoder.writeFrame(texture, adjustedTimestampHns)) { + const int64_t writtenFrames = frameCount.fetch_add(1) + 1; + if (captureSetupComplete && writtenFrames >= 1 && !recordingStartedAnnounced.exchange(true)) { + std::cout << "Recording started" << std::endl; + std::cout.flush(); + } + } + }); + + // Start stdin listener + std::thread stdinThread(stdinListenerThread); + stdinThread.detach(); + + // Initialize WASAPI captures (but don't start yet) + WasapiCapture loopback; + WasapiCapture micCapture; + bool audioActive = false; + bool audioInitialized = false; + bool micActive = false; + bool micInitialized = false; + + if (config.captureSystemAudio && !config.audioOutputPath.empty()) { + audioInitialized = loopback.initializeLoopback(config.audioOutputPath); + if (!audioInitialized) { + std::cerr << "WARNING: Failed to initialize WASAPI loopback" << std::endl; + } + } + + if (config.captureMic && !config.micOutputPath.empty()) { + micInitialized = micCapture.initializeMic( + config.micOutputPath, + config.micDeviceId, + config.micDeviceName); + if (!micInitialized) { + reportMicrophoneCaptureUnavailable(); + } + } + + // Start video capture, then audio immediately after for sync + if (!session.startCapture()) { + std::cerr << "ERROR: Failed to start WGC capture" << std::endl; + return 1; + } + + if (audioInitialized) { + audioActive = loopback.start(); + } + if (micInitialized) { + micActive = micCapture.start(); + if (!micActive) { + reportMicrophoneCaptureUnavailable(); + } + } + captureSetupComplete = true; + + // Wait for stop signal while pausing/resuming audio tracks in lockstep. + while (!g_stopRequested && !session.hasFatalError()) { + if (g_pauseRequested) { + if (audioActive) loopback.pause(); + if (micActive) micCapture.pause(); + } else { + if (audioActive) loopback.resume(); + if (micActive) micCapture.resume(); + } + + std::unique_lock lock(g_stopMutex); + g_stopCv.wait_for(lock, std::chrono::milliseconds(20), [] { return g_stopRequested.load(); }); + } + + // Stop capture and finalize + const int64_t adjustedStopTimestampHns = adjustedVideoTimestampHns(queryPerformanceCounterHns()); + session.stopCapture(); + if (audioActive) loopback.stop(); + if (micActive) micCapture.stop(); + + if (session.hasFatalError()) { + std::cerr << "ERROR: WGC capture session failed during recording" << std::endl; + encoder.finalize(); + DeleteFileW(outputPathW.c_str()); + if (!config.audioOutputPath.empty()) { + const std::wstring audioPathW = utf8ToWide(config.audioOutputPath); + const std::wstring audioMetadataPathW = utf8ToWide(config.audioOutputPath + ".json"); + DeleteFileW(audioPathW.c_str()); + DeleteFileW(audioMetadataPathW.c_str()); + } + if (!config.micOutputPath.empty()) { + const std::wstring micPathW = utf8ToWide(config.micOutputPath); + const std::wstring micMetadataPathW = utf8ToWide(config.micOutputPath + ".json"); + DeleteFileW(micPathW.c_str()); + DeleteFileW(micMetadataPathW.c_str()); + } + return 1; + } + + if (audioActive) { + writeCompanionAudioTimingMetadata( + config.audioOutputPath, + firstVideoTimestampHns.load(), + loopback); + } + if (micActive) { + writeCompanionAudioTimingMetadata( + config.micOutputPath, + firstVideoTimestampHns.load(), + micCapture); + } + + if (frameCount.load() <= 0) { + std::cerr << "ERROR: No video frames were captured before stop" << std::endl; + DeleteFileW(outputPathW.c_str()); + return 1; + } + + if (!encoder.extendLastFrameTo(adjustedStopTimestampHns)) { + std::cerr << "WARNING: Failed to extend the last video frame to the stop timestamp" << std::endl; + } + + if (!encoder.finalize()) { + std::cerr << "ERROR: Failed to finalize Media Foundation encoder" << std::endl; + return 1; + } + + std::cout << "Recording stopped. Output path: " << config.outputPath << std::endl; + if (audioActive) { + if ( + loopback.dataDiscontinuityCount() > 0 || + loopback.timestampErrorCount() > 0 || + loopback.gapFillCount() > 0 || + loopback.compactedDiscontinuityCount() > 0 || + loopback.compactedSilentDiscontinuityCount() > 0 + ) { + std::cerr << "WARNING: System audio timing metadata includes discontinuities=" + << loopback.dataDiscontinuityCount() + << " timestampErrors=" << loopback.timestampErrorCount() + << " gapFills=" << loopback.gapFillCount() + << " insertedSilenceFrames=" << loopback.insertedSilenceFrames() + << " compactedDiscontinuities=" << loopback.compactedDiscontinuityCount() + << " compactedDiscontinuityFrames=" << loopback.compactedDiscontinuityFrames() + << " compactedSilentDiscontinuities=" << loopback.compactedSilentDiscontinuityCount() + << " compactedSilentDiscontinuityFrames=" << loopback.compactedSilentDiscontinuityFrames() + << std::endl; + } + std::cout << "Audio path: " << config.audioOutputPath << std::endl; + } + if (micActive) { + if ( + micCapture.dataDiscontinuityCount() > 0 || + micCapture.timestampErrorCount() > 0 || + micCapture.gapFillCount() > 0 || + micCapture.compactedDiscontinuityCount() > 0 || + micCapture.compactedSilentDiscontinuityCount() > 0 + ) { + std::cerr << "WARNING: Microphone timing metadata includes discontinuities=" + << micCapture.dataDiscontinuityCount() + << " timestampErrors=" << micCapture.timestampErrorCount() + << " gapFills=" << micCapture.gapFillCount() + << " insertedSilenceFrames=" << micCapture.insertedSilenceFrames() + << " compactedDiscontinuities=" << micCapture.compactedDiscontinuityCount() + << " compactedDiscontinuityFrames=" << micCapture.compactedDiscontinuityFrames() + << " compactedSilentDiscontinuities=" << micCapture.compactedSilentDiscontinuityCount() + << " compactedSilentDiscontinuityFrames=" << micCapture.compactedSilentDiscontinuityFrames() + << std::endl; + } + std::cout << "Mic path: " << config.micOutputPath << std::endl; + } + std::cout.flush(); + + // Allow pipe buffers to drain before forceful exit + Sleep(100); + + // Fast exit to avoid WinRT/COM teardown crashes during apartment cleanup + ExitProcess(0); +} diff --git a/electron/native/wgc-capture/src/mf_encoder.cpp b/electron/native/wgc-capture/src/mf_encoder.cpp new file mode 100644 index 000000000..17b11effa --- /dev/null +++ b/electron/native/wgc-capture/src/mf_encoder.cpp @@ -0,0 +1,372 @@ +#include "mf_encoder.h" +#include +#include +#include +#include +#include +#include +#include +#include "../../common/bt709_video.h" + +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfreadwrite.lib") +#pragma comment(lib, "mf.lib") +#pragma comment(lib, "mfuuid.lib") + +static UINT32 calculateScreenRecordingBitrate(int width, int height, int fps) { + constexpr uint64_t kFourKPixels = 3840ULL * 2160ULL; + constexpr uint64_t kQhdPixels = 2560ULL * 1440ULL; + constexpr UINT32 kBitrate4K = 45000000; + constexpr UINT32 kBitrateQhd = 28000000; + constexpr UINT32 kBitrateBase = 18000000; + constexpr double kHighFrameRateBoost = 1.35; + + const uint64_t pixels = + static_cast((std::max)(width, 1)) * + static_cast((std::max)(height, 1)); + const UINT32 baseBitrate = + pixels >= kFourKPixels ? kBitrate4K : + pixels >= kQhdPixels ? kBitrateQhd : + kBitrateBase; + const double boost = fps >= 60 ? kHighFrameRateBoost : 1.0; + return static_cast(static_cast(baseBitrate) * boost + 0.5); +} + +MFEncoder::MFEncoder() {} + +MFEncoder::~MFEncoder() { + finalize(); +} + +bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height, int fps, + ID3D11Device* device, ID3D11DeviceContext* context) { + std::lock_guard lock(mutex_); + + if (initialized_) return false; + + if (fps <= 0) { + std::cerr << "ERROR: Encoder fps must be positive, got " << fps << std::endl; + return false; + } + + if (width % 2 != 0 || height % 2 != 0) { + std::cerr << "ERROR: Encoder dimensions must be even, got " << width << "x" << height << std::endl; + return false; + } + + width_ = width; + height_ = height; + fps_ = fps; + device_ = device; + context_ = context; + + HRESULT hr = MFStartup(MF_VERSION); + if (FAILED(hr)) { + std::cerr << "ERROR: MFStartup failed: 0x" << std::hex << hr << std::endl; + return false; + } + + // Output media type (H.264) + ComPtr outputType; + hr = MFCreateMediaType(&outputType); + if (FAILED(hr)) return false; + + outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264); + const UINT32 videoBitrate = calculateScreenRecordingBitrate(width_, height_, fps_); + outputType->SetUINT32(MF_MT_AVG_BITRATE, videoBitrate); + MFSetAttributeSize(outputType.Get(), MF_MT_FRAME_SIZE, width_, height_); + MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, fps_, 1); + MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + hr = setBt709LimitedVideoAttributes(outputType.Get()); + if (FAILED(hr)) return false; + std::cerr << "Encoder bitrate: " << videoBitrate << " bps for " + << width_ << "x" << height_ << "@" << fps_ << "fps" << std::endl; + + // Input media type (NV12) + ComPtr inputType; + hr = MFCreateMediaType(&inputType); + if (FAILED(hr)) return false; + + inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12); + MFSetAttributeSize(inputType.Get(), MF_MT_FRAME_SIZE, width_, height_); + MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, fps_, 1); + MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + hr = setBt709LimitedVideoAttributes(inputType.Get()); + if (FAILED(hr)) return false; + + // Create SinkWriter with MPEG4 container + ComPtr writerAttrs; + hr = MFCreateAttributes(&writerAttrs, 1); + if (FAILED(hr)) return false; + + writerAttrs->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); + + hr = MFCreateSinkWriterFromURL(outputPath.c_str(), nullptr, writerAttrs.Get(), &sinkWriter_); + if (FAILED(hr)) { + std::cerr << "ERROR: MFCreateSinkWriterFromURL failed: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = sinkWriter_->AddStream(outputType.Get(), &streamIndex_); + if (FAILED(hr)) { + std::cerr << "ERROR: AddStream failed: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = sinkWriter_->SetInputMediaType(streamIndex_, inputType.Get(), nullptr); + if (FAILED(hr)) { + std::cerr << "ERROR: SetInputMediaType failed: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = sinkWriter_->BeginWriting(); + if (FAILED(hr)) { + std::cerr << "ERROR: BeginWriting failed: 0x" << std::hex << hr << std::endl; + return false; + } + + // Pre-allocate staging texture + D3D11_TEXTURE2D_DESC stagingDesc = {}; + stagingDesc.Width = width_; + stagingDesc.Height = height_; + stagingDesc.MipLevels = 1; + stagingDesc.ArraySize = 1; + stagingDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + stagingDesc.SampleDesc.Count = 1; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + hr = device_->CreateTexture2D(&stagingDesc, nullptr, &stagingTexture_); + if (FAILED(hr)) { + std::cerr << "ERROR: Failed to create staging texture: 0x" << std::hex << hr << std::endl; + return false; + } + + // WGC window captures can change frame size while recording. Keep the muxer + // output dimensions stable by compositing resized frames into this fixed + // BGRA surface before CPU readback. + D3D11_TEXTURE2D_DESC compositeDesc = {}; + compositeDesc.Width = width_; + compositeDesc.Height = height_; + compositeDesc.MipLevels = 1; + compositeDesc.ArraySize = 1; + compositeDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + compositeDesc.SampleDesc.Count = 1; + compositeDesc.Usage = D3D11_USAGE_DEFAULT; + compositeDesc.BindFlags = D3D11_BIND_RENDER_TARGET; + + hr = device_->CreateTexture2D(&compositeDesc, nullptr, &resizeCompositeTexture_); + if (FAILED(hr)) { + std::cerr << "ERROR: Failed to create resize composite texture: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = device_->CreateRenderTargetView(resizeCompositeTexture_.Get(), nullptr, &resizeCompositeView_); + if (FAILED(hr)) { + std::cerr << "ERROR: Failed to create resize composite view: 0x" << std::hex << hr << std::endl; + return false; + } + + // Pre-allocate NV12 buffer + const int ySize = width_ * height_; + const int uvSize = (width_ / 2) * (height_ / 2) * 2; + nv12Buffer_.resize(ySize + uvSize); + lastFrameBuffer_.clear(); + firstSampleTimeHns_ = -1; + lastSampleTimeHns_ = -1; + + initialized_ = true; + return true; +} + +bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) { + std::lock_guard lock(mutex_); + + if (!initialized_ || !sinkWriter_) return false; + + D3D11_TEXTURE2D_DESC sourceDesc = {}; + texture->GetDesc(&sourceDesc); + + if (sourceDesc.Width == static_cast(width_) && + sourceDesc.Height == static_cast(height_)) { + context_->CopyResource(stagingTexture_.Get(), texture); + } else { + if (!resizeCompositeTexture_ || !resizeCompositeView_) return false; + + const FLOAT clearColor[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + context_->ClearRenderTargetView(resizeCompositeView_.Get(), clearColor); + + D3D11_BOX sourceBox = {}; + sourceBox.left = 0; + sourceBox.top = 0; + sourceBox.front = 0; + sourceBox.right = (std::min)(sourceDesc.Width, static_cast(width_)); + sourceBox.bottom = (std::min)(sourceDesc.Height, static_cast(height_)); + sourceBox.back = 1; + + if (sourceBox.right == 0 || sourceBox.bottom == 0) return false; + + context_->CopySubresourceRegion( + resizeCompositeTexture_.Get(), + 0, + 0, + 0, + 0, + texture, + 0, + &sourceBox); + context_->CopyResource(stagingTexture_.Get(), resizeCompositeTexture_.Get()); + } + + D3D11_MAPPED_SUBRESOURCE mapped; + HRESULT hr = context_->Map(stagingTexture_.Get(), 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED(hr)) return false; + + // Convert full-range desktop BGRA to explicitly tagged BT.709 video-range NV12. + const uint8_t* bgra = static_cast(mapped.pData); + const int bgraPitch = static_cast(mapped.RowPitch); + convertBgraToBt709LimitedNv12(bgra, bgraPitch, width_, height_, nv12Buffer_); + + context_->Unmap(stagingTexture_.Get(), 0); + + // WGC may stop delivering frames while the scene is static; keep the MP4 + // timeline continuous by repeating the previous frame before writing a new one. + int64_t normalizedTimestampHns = 0; + normalizeWriteTimestampHnsLocked(timestampHns, normalizedTimestampHns); + + if (!lastFrameBuffer_.empty() && !extendLastFrameToLocked(normalizedTimestampHns)) { + return false; + } + + bool wroteSample = writeNv12SampleLocked(nv12Buffer_, normalizedTimestampHns); + if (wroteSample) { + lastFrameBuffer_ = nv12Buffer_; + lastSampleTimeHns_ = normalizedTimestampHns; + } + return wroteSample; +} + +bool MFEncoder::extendLastFrameTo(int64_t timestampHns) { + std::lock_guard lock(mutex_); + + int64_t normalizedTimestampHns = 0; + if (!normalizeTimelineTimestampHnsLocked(timestampHns, normalizedTimestampHns)) { + return false; + } + + return extendLastFrameToLocked(normalizedTimestampHns); +} + +void MFEncoder::normalizeWriteTimestampHnsLocked(int64_t timestampHns, int64_t& normalizedTimestampHns) { + if (firstSampleTimeHns_ < 0) { + firstSampleTimeHns_ = timestampHns < 0 ? 0 : timestampHns; + } + + normalizedTimestampHns = timestampHns - firstSampleTimeHns_; + if (normalizedTimestampHns < 0) { + normalizedTimestampHns = 0; + } +} + +bool MFEncoder::normalizeTimelineTimestampHnsLocked( + int64_t timestampHns, + int64_t& normalizedTimestampHns +) const { + if (firstSampleTimeHns_ < 0) return false; + + normalizedTimestampHns = timestampHns - firstSampleTimeHns_; + if (normalizedTimestampHns < 0) { + normalizedTimestampHns = 0; + } + return true; +} + +bool MFEncoder::extendLastFrameToLocked(int64_t timestampHns) { + if (!initialized_ || !sinkWriter_) return false; + if (lastFrameBuffer_.empty()) return false; + if (lastSampleTimeHns_ < 0) return false; + + if (fps_ <= 0) return false; + const int64_t frameDurationHns = 10000000LL / fps_; + if (frameDurationHns <= 0) return false; + if (timestampHns <= lastSampleTimeHns_ + frameDurationHns) { + return true; + } + + int64_t nextSampleTimeHns = lastSampleTimeHns_ + frameDurationHns; + while (nextSampleTimeHns + frameDurationHns <= timestampHns) { + if (!writeNv12SampleLocked(lastFrameBuffer_, nextSampleTimeHns)) { + return false; + } + lastSampleTimeHns_ = nextSampleTimeHns; + nextSampleTimeHns += frameDurationHns; + } + + return true; +} + +bool MFEncoder::writeNv12SampleLocked(const std::vector& frameBuffer, int64_t timestampHns) { + if (frameBuffer.empty()) return false; + if (fps_ <= 0) return false; + + const int64_t frameDurationHns = 10000000LL / fps_; + if (frameDurationHns <= 0) return false; + + // Create MF sample + DWORD bufferSize = static_cast(frameBuffer.size()); + ComPtr buffer; + HRESULT hr = MFCreateMemoryBuffer(bufferSize, &buffer); + if (FAILED(hr)) return false; + + BYTE* bufferData = nullptr; + hr = buffer->Lock(&bufferData, nullptr, nullptr); + if (FAILED(hr)) return false; + + std::memcpy(bufferData, frameBuffer.data(), bufferSize); + buffer->Unlock(); + buffer->SetCurrentLength(bufferSize); + + ComPtr sample; + hr = MFCreateSample(&sample); + if (FAILED(hr)) return false; + + sample->AddBuffer(buffer.Get()); + sample->SetSampleTime(timestampHns); + sample->SetSampleDuration(frameDurationHns); + + hr = sinkWriter_->WriteSample(streamIndex_, sample.Get()); + if (FAILED(hr)) { + std::cerr << "ERROR: WriteSample failed: 0x" << std::hex << hr << std::endl; + } + return SUCCEEDED(hr); +} + +bool MFEncoder::finalize() { + std::lock_guard lock(mutex_); + + if (!initialized_) return false; + if (!sinkWriter_) return false; + + HRESULT hr = sinkWriter_->Finalize(); + if (FAILED(hr)) { + std::cerr << "ERROR: SinkWriter Finalize failed: 0x" << std::hex << hr << std::endl; + } + + initialized_ = false; + sinkWriter_.Reset(); + stagingTexture_.Reset(); + resizeCompositeView_.Reset(); + resizeCompositeTexture_.Reset(); + nv12Buffer_.clear(); + lastFrameBuffer_.clear(); + nv12Buffer_.shrink_to_fit(); + lastFrameBuffer_.shrink_to_fit(); + firstSampleTimeHns_ = -1; + lastSampleTimeHns_ = -1; + MFShutdown(); + return SUCCEEDED(hr); +} diff --git a/electron/native/wgc-capture/src/mf_encoder.h b/electron/native/wgc-capture/src/mf_encoder.h new file mode 100644 index 000000000..3217389af --- /dev/null +++ b/electron/native/wgc-capture/src/mf_encoder.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +class MFEncoder { +public: + MFEncoder(); + ~MFEncoder(); + + bool initialize(const std::wstring& outputPath, int width, int height, int fps, + ID3D11Device* device, ID3D11DeviceContext* context); + bool writeFrame(ID3D11Texture2D* texture, int64_t timestampHns); + bool extendLastFrameTo(int64_t timestampHns); + bool finalize(); + +private: + void normalizeWriteTimestampHnsLocked(int64_t timestampHns, int64_t& normalizedTimestampHns); + bool normalizeTimelineTimestampHnsLocked(int64_t timestampHns, int64_t& normalizedTimestampHns) const; + bool extendLastFrameToLocked(int64_t timestampHns); + bool writeNv12SampleLocked(const std::vector& frameBuffer, int64_t timestampHns); + + ComPtr sinkWriter_; + ID3D11Device* device_ = nullptr; + ID3D11DeviceContext* context_ = nullptr; + ComPtr stagingTexture_; + ComPtr resizeCompositeTexture_; + ComPtr resizeCompositeView_; + std::vector nv12Buffer_; + std::vector lastFrameBuffer_; + DWORD streamIndex_ = 0; + int width_ = 0; + int height_ = 0; + int fps_ = 60; + int64_t firstSampleTimeHns_ = -1; + int64_t lastSampleTimeHns_ = -1; + bool initialized_ = false; + std::mutex mutex_; +}; diff --git a/electron/native/wgc-capture/src/monitor_utils.cpp b/electron/native/wgc-capture/src/monitor_utils.cpp new file mode 100644 index 000000000..a175bd7f0 --- /dev/null +++ b/electron/native/wgc-capture/src/monitor_utils.cpp @@ -0,0 +1,85 @@ +#include "monitor_utils.h" +#include +#include + +static BOOL CALLBACK enumMonitorCallback(HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) { + auto* monitors = reinterpret_cast*>(lParam); + + MONITORINFOEXW mi = {}; + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(hMonitor, &mi)) { + MonitorInfo info; + info.handle = hMonitor; + info.x = mi.rcMonitor.left; + info.y = mi.rcMonitor.top; + info.width = mi.rcMonitor.right - mi.rcMonitor.left; + info.height = mi.rcMonitor.bottom - mi.rcMonitor.top; + info.deviceName = mi.szDevice; + monitors->push_back(info); + } + + return TRUE; +} + +std::vector enumerateMonitors() { + std::vector monitors; + EnumDisplayMonitors(nullptr, nullptr, enumMonitorCallback, reinterpret_cast(&monitors)); + return monitors; +} + +// Electron uses the HMONITOR handle value cast to a number as the display ID. +HMONITOR findMonitorByDisplayId(int64_t displayId) { + auto monitors = enumerateMonitors(); + + for (const auto& m : monitors) { + if (static_cast(reinterpret_cast(m.handle)) == displayId) { + return m.handle; + } + } + + return nullptr; +} + +HMONITOR findMonitorByBounds(int x, int y, int width, int height) { + auto monitors = enumerateMonitors(); + + for (const auto& m : monitors) { + if (m.x == x && m.y == y && m.width == width && m.height == height) { + std::cerr << "Found monitor by exact bounds: " << x << "," << y << " " << width << "x" << height << std::endl; + return m.handle; + } + } + + for (const auto& m : monitors) { + if (m.x == x && m.y == y) { + std::cerr << "Found monitor by top-left point match: " << x << "," << y << std::endl; + return m.handle; + } + } + + RECT rect = { x, y, x + width, y + height }; + HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONULL); + if (monitor) { + std::cerr << "Found monitor via Windows OS MonitorFromRect fallback" << std::endl; + return monitor; + } + + return nullptr; +} + +MonitorInfo getMonitorInfo(HMONITOR monitor) { + MonitorInfo info; + info.handle = monitor; + + MONITORINFOEXW mi = {}; + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(monitor, &mi)) { + info.x = mi.rcMonitor.left; + info.y = mi.rcMonitor.top; + info.width = mi.rcMonitor.right - mi.rcMonitor.left; + info.height = mi.rcMonitor.bottom - mi.rcMonitor.top; + info.deviceName = mi.szDevice; + } + + return info; +} diff --git a/electron/native/wgc-capture/src/monitor_utils.h b/electron/native/wgc-capture/src/monitor_utils.h new file mode 100644 index 000000000..90f3ac396 --- /dev/null +++ b/electron/native/wgc-capture/src/monitor_utils.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include + +struct MonitorInfo { + HMONITOR handle; + int x; + int y; + int width; + int height; + std::wstring deviceName; +}; + +std::vector enumerateMonitors(); +HMONITOR findMonitorByDisplayId(int64_t displayId); +HMONITOR findMonitorByBounds(int x, int y, int width, int height); +MonitorInfo getMonitorInfo(HMONITOR monitor); diff --git a/electron/native/wgc-capture/src/wasapi_loopback.cpp b/electron/native/wgc-capture/src/wasapi_loopback.cpp new file mode 100644 index 000000000..240264d8c --- /dev/null +++ b/electron/native/wgc-capture/src/wasapi_loopback.cpp @@ -0,0 +1,623 @@ +#include "wasapi_loopback.h" +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "ole32.lib") + +namespace { +constexpr int64_t kHundredNanosecondsPerSecond = 10000000; +constexpr uint64_t kSilenceWriteChunkFrames = 4096; +constexpr uint32_t kMinimumGapFillThresholdMs = 50; +// Wireless headsets can flag short WASAPI discontinuities that are less audible +// when compacted and later duration-corrected than when rendered as hard silence. +constexpr uint32_t kDiscontinuityGapFillThresholdMs = 140; +constexpr uint32_t kSilentDiscontinuityCompactThresholdMs = 40; +constexpr uint32_t kBoundaryFadeInMs = 5; + +int64_t queryPerformanceCounterHns() { + LARGE_INTEGER counter; + LARGE_INTEGER frequency; + if (!QueryPerformanceCounter(&counter) || !QueryPerformanceFrequency(&frequency) || frequency.QuadPart <= 0) { + return 0; + } + + return static_cast( + (static_cast(counter.QuadPart) * 10000000.0L) / + static_cast(frequency.QuadPart)); +} + +bool isFloatFormat(const WAVEFORMATEX* format) { + if (!format) return false; + if (format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) return true; + if (format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) return false; + return reinterpret_cast(format)->SubFormat == + KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; +} + +bool isPcmFormat(const WAVEFORMATEX* format) { + if (!format) return false; + if (format->wFormatTag == WAVE_FORMAT_PCM) return true; + if (format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) return false; + return reinterpret_cast(format)->SubFormat == + KSDATAFORMAT_SUBTYPE_PCM; +} + +int16_t pcm24ToInt16(const BYTE* sample) { + int32_t value = static_cast(sample[0]) | + (static_cast(sample[1]) << 8) | + (static_cast(sample[2]) << 16); + if ((value & 0x800000) != 0) { + value |= ~0xFFFFFF; + } + return static_cast(value >> 8); +} + +std::wstring normalizeDeviceName(const std::wstring& value) { + std::wstring result; + result.reserve(value.size()); + bool lastWasSpace = true; + for (const wchar_t character : value) { + if (std::iswalnum(character)) { + result.push_back(static_cast(std::towlower(character))); + lastWasSpace = false; + } else if (!lastWasSpace) { + result.push_back(L' '); + lastWasSpace = true; + } + } + if (!result.empty() && result.back() == L' ') result.pop_back(); + return result; +} + +bool deviceNamesMatch( + const std::wstring& candidateName, + const std::wstring& requestedName) { + const std::wstring candidate = normalizeDeviceName(candidateName); + const std::wstring requested = normalizeDeviceName(requestedName); + return !candidate.empty() && candidate == requested; +} + +std::wstring getDeviceFriendlyName(IMMDevice* device) { + if (!device) return L""; + IPropertyStore* store = nullptr; + if (FAILED(device->OpenPropertyStore(STGM_READ, &store)) || !store) return L""; + PROPVARIANT value; + PropVariantInit(&value); + const HRESULT hr = store->GetValue(PKEY_Device_FriendlyName, &value); + std::wstring name; + if (SUCCEEDED(hr) && value.vt == VT_LPWSTR && value.pwszVal) name = value.pwszVal; + PropVariantClear(&value); + store->Release(); + return name; +} +} + +static const CLSID CLSID_MMDeviceEnumerator_ = __uuidof(MMDeviceEnumerator); +static const IID IID_IMMDeviceEnumerator_ = __uuidof(IMMDeviceEnumerator); +static const IID IID_IAudioClient_ = __uuidof(IAudioClient); +static const IID IID_IAudioCaptureClient_ = __uuidof(IAudioCaptureClient); + +WasapiCapture::WasapiCapture() {} + +WasapiCapture::~WasapiCapture() { + stop(); + if (mixFormat_) CoTaskMemFree(mixFormat_); + if (captureClient_) captureClient_->Release(); + if (audioClient_) audioClient_->Release(); + if (device_) device_->Release(); + if (enumerator_) enumerator_->Release(); +} + +static std::wstring utf8ToWide(const std::string& str) { + if (str.empty()) return L""; + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0); + std::wstring wstr(len, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), &wstr[0], len); + return wstr; +} + +IMMDevice* WasapiCapture::findCaptureDeviceByName(const std::wstring& targetName) { + IMMDeviceCollection* collection = nullptr; + HRESULT hr = enumerator_->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &collection); + if (FAILED(hr)) return nullptr; + + UINT count = 0; + collection->GetCount(&count); + + IMMDevice* matchingDevice = nullptr; + for (UINT i = 0; i < count; i++) { + IMMDevice* dev = nullptr; + if (FAILED(collection->Item(i, &dev)) || !dev) continue; + + const std::wstring candidateName = getDeviceFriendlyName(dev); + if (deviceNamesMatch(candidateName, targetName)) { + if (matchingDevice) { + matchingDevice->Release(); + dev->Release(); + collection->Release(); + return nullptr; + } + matchingDevice = dev; + continue; + } + dev->Release(); + } + + collection->Release(); + return matchingDevice; +} + +bool WasapiCapture::initializeLoopback(const std::string& outputPath) { + outputPath_ = outputPath; + streamFlags_ = AUDCLNT_STREAMFLAGS_LOOPBACK; + + HRESULT hr = CoCreateInstance( + CLSID_MMDeviceEnumerator_, nullptr, CLSCTX_ALL, + IID_IMMDeviceEnumerator_, reinterpret_cast(&enumerator_)); + if (FAILED(hr)) return false; + + hr = enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device_); + if (FAILED(hr)) return false; + + return initializeCommon(); +} + +bool WasapiCapture::initializeMic( + const std::string& outputPath, + const std::string& deviceId, + const std::string& deviceName) { + outputPath_ = outputPath; + streamFlags_ = 0; + + HRESULT hr = CoCreateInstance( + CLSID_MMDeviceEnumerator_, nullptr, CLSCTX_ALL, + IID_IMMDeviceEnumerator_, reinterpret_cast(&enumerator_)); + if (FAILED(hr)) return false; + + if (!deviceId.empty() && deviceId != "default") { + const std::wstring requestedId = utf8ToWide(deviceId); + hr = enumerator_->GetDevice(requestedId.c_str(), &device_); + if (FAILED(hr)) device_ = nullptr; + } + if (!device_ && !deviceName.empty() && deviceId != "default") { + device_ = findCaptureDeviceByName(utf8ToWide(deviceName)); + } + if (!device_) { + const bool wantedSpecificDevice = + deviceId != "default" && (!deviceId.empty() || !deviceName.empty()); + if (wantedSpecificDevice) { + std::cerr << "WARNING: Requested microphone unavailable" << std::endl; + return false; + } + hr = enumerator_->GetDefaultAudioEndpoint(eCapture, eConsole, &device_); + if (FAILED(hr)) return false; + } + + return initializeCommon(); +} + +bool WasapiCapture::initializeCommon() { + HRESULT hr = device_->Activate(IID_IAudioClient_, CLSCTX_ALL, nullptr, + reinterpret_cast(&audioClient_)); + if (FAILED(hr)) return false; + + hr = audioClient_->GetMixFormat(&mixFormat_); + if (FAILED(hr)) return false; + + REFERENCE_TIME bufferDuration = 200000; // 20ms + hr = audioClient_->Initialize( + AUDCLNT_SHAREMODE_SHARED, + streamFlags_, + bufferDuration, 0, mixFormat_, nullptr); + if (FAILED(hr)) return false; + + hr = audioClient_->GetBufferSize(&bufferFrameCount_); + if (FAILED(hr)) return false; + + hr = audioClient_->GetService(IID_IAudioCaptureClient_, + reinterpret_cast(&captureClient_)); + if (FAILED(hr)) return false; + + return true; +} + +bool WasapiCapture::start() { + if (capturing_) return true; + + outputFile_ = CreateFileA( + outputPath_.c_str(), GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (outputFile_ == INVALID_HANDLE_VALUE) { + std::cerr << "ERROR: Cannot create audio output file" << std::endl; + return false; + } + + totalDataBytes_ = 0; + framesWritten_ = 0; + firstPacketQpcHns_ = -1; + pauseStartQpcHns_ = 0; + accumulatedPausedQpcHns_ = 0; + dataDiscontinuityCount_ = 0; + timestampErrorCount_ = 0; + gapFillCount_ = 0; + insertedSilenceFrames_ = 0; + compactedDiscontinuityCount_ = 0; + compactedDiscontinuityFrames_ = 0; + compactedSilentDiscontinuityCount_ = 0; + compactedSilentDiscontinuityFrames_ = 0; + fadeInFramesRemaining_ = 0; + writeWavHeader(outputFile_, 0); + + HRESULT hr = audioClient_->Start(); + if (FAILED(hr)) { + CloseHandle(outputFile_); + outputFile_ = INVALID_HANDLE_VALUE; + return false; + } + + capturing_ = true; + paused_ = false; + thread_ = std::thread(&WasapiCapture::captureThread, this); + return true; +} + +bool WasapiCapture::pause() { + if (!capturing_ || paused_) return true; + pauseStartQpcHns_ = queryPerformanceCounterHns(); + paused_ = true; + return audioClient_ ? SUCCEEDED(audioClient_->Stop()) : false; +} + +bool WasapiCapture::resume() { + if (!capturing_ || !paused_) return true; + const int64_t resumedQpcHns = queryPerformanceCounterHns(); + const int64_t pauseStartQpcHns = pauseStartQpcHns_.load(); + HRESULT hr = audioClient_ ? audioClient_->Start() : E_FAIL; + if (FAILED(hr)) return false; + if (pauseStartQpcHns > 0 && resumedQpcHns > pauseStartQpcHns) { + accumulatedPausedQpcHns_.fetch_add(resumedQpcHns - pauseStartQpcHns); + } + pauseStartQpcHns_ = 0; + paused_ = false; + fadeInFramesRemaining_ = boundaryFadeInFrameCount(); + return true; +} + +void WasapiCapture::stop() { + if (!capturing_) return; + capturing_ = false; + if (thread_.joinable()) thread_.join(); + if (audioClient_) audioClient_->Stop(); + + if (outputFile_ != INVALID_HANDLE_VALUE) { + SetFilePointer(outputFile_, 0, nullptr, FILE_BEGIN); + const uint64_t dataBytes = totalDataBytes_.load(); + const DWORD wavDataBytes = + static_cast(std::min(dataBytes, 0xFFFFFFFFu)); + writeWavHeader(outputFile_, wavDataBytes); + CloseHandle(outputFile_); + outputFile_ = INVALID_HANDLE_VALUE; + } + + paused_ = false; + pauseStartQpcHns_ = 0; + accumulatedPausedQpcHns_ = 0; + fadeInFramesRemaining_ = 0; +} + +static int16_t floatToInt16(float v) { + v = v < -1.0f ? -1.0f : (v > 1.0f ? 1.0f : v); + return static_cast(v * 32767.0f); +} + +bool WasapiCapture::writeWavHeader(HANDLE file, DWORD dataSize) { + WORD channels = static_cast(mixFormat_->nChannels); + DWORD sampleRate = mixFormat_->nSamplesPerSec; + WORD bitsPerSample = 16; + WORD blockAlign = channels * (bitsPerSample / 8); + DWORD byteRate = sampleRate * blockAlign; + + DWORD written; + WriteFile(file, "RIFF", 4, &written, nullptr); + DWORD chunkSize = 36 + dataSize; + WriteFile(file, &chunkSize, 4, &written, nullptr); + WriteFile(file, "WAVE", 4, &written, nullptr); + WriteFile(file, "fmt ", 4, &written, nullptr); + DWORD fmtSize = 16; + WriteFile(file, &fmtSize, 4, &written, nullptr); + WORD audioFormat = 1; + WriteFile(file, &audioFormat, 2, &written, nullptr); + WriteFile(file, &channels, 2, &written, nullptr); + WriteFile(file, &sampleRate, 4, &written, nullptr); + WriteFile(file, &byteRate, 4, &written, nullptr); + WriteFile(file, &blockAlign, 2, &written, nullptr); + WriteFile(file, &bitsPerSample, 2, &written, nullptr); + WriteFile(file, "data", 4, &written, nullptr); + WriteFile(file, &dataSize, 4, &written, nullptr); + return true; +} + +void WasapiCapture::writePcmFrames(const int16_t* samples, UINT32 frameCount, WORD channels) { + if (!samples || frameCount == 0 || channels == 0 || outputFile_ == INVALID_HANDLE_VALUE) { + return; + } + + const DWORD bytesToWrite = frameCount * channels * sizeof(int16_t); + const uint32_t fadeFrames = fadeInFramesRemaining_.load(); + if (fadeFrames > 0) { + const uint32_t totalFadeFrames = boundaryFadeInFrameCount(); + const uint32_t remainingFadeFrames = (std::min)(fadeFrames, totalFadeFrames); + const uint32_t framesToFade = + (std::min)(static_cast(frameCount), remainingFadeFrames); + const uint32_t completedFadeFrames = totalFadeFrames - remainingFadeFrames; + std::vector faded(samples, samples + frameCount * channels); + for (uint32_t frame = 0; frame < framesToFade; frame++) { + const double scale = + static_cast(completedFadeFrames + frame + 1) / + static_cast(totalFadeFrames); + for (WORD channel = 0; channel < channels; channel++) { + const size_t index = static_cast(frame) * channels + channel; + const long scaled = std::lround(static_cast(faded[index]) * scale); + faded[index] = static_cast( + std::clamp(scaled, -32768, 32767)); + } + } + fadeInFramesRemaining_ = remainingFadeFrames - framesToFade; + + DWORD written = 0; + WriteFile(outputFile_, faded.data(), bytesToWrite, &written, nullptr); + totalDataBytes_.fetch_add(written); + framesWritten_.fetch_add(written / (channels * sizeof(int16_t))); + return; + } + + DWORD written = 0; + WriteFile(outputFile_, samples, bytesToWrite, &written, nullptr); + totalDataBytes_.fetch_add(written); + framesWritten_.fetch_add(written / (channels * sizeof(int16_t))); +} + +void WasapiCapture::writeSilenceFrames(uint64_t frameCount, WORD channels) { + if (frameCount == 0 || channels == 0 || outputFile_ == INVALID_HANDLE_VALUE) { + return; + } + + gapFillCount_.fetch_add(1); + insertedSilenceFrames_.fetch_add(frameCount); + std::vector silence(static_cast(kSilenceWriteChunkFrames * channels), 0); + while (frameCount > 0) { + const uint64_t chunkFrames = (std::min)(frameCount, kSilenceWriteChunkFrames); + writePcmFrames(silence.data(), static_cast(chunkFrames), channels); + frameCount -= chunkFrames; + } + fadeInFramesRemaining_ = boundaryFadeInFrameCount(); +} + +uint64_t WasapiCapture::capturedDurationMs() const { + if (!mixFormat_ || mixFormat_->nSamplesPerSec == 0) { + return 0; + } + + return (framesWritten_.load() * 1000) / mixFormat_->nSamplesPerSec; +} + +uint32_t WasapiCapture::sampleRate() const { + return mixFormat_ ? mixFormat_->nSamplesPerSec : 0; +} + +uint16_t WasapiCapture::channelCount() const { + return mixFormat_ ? mixFormat_->nChannels : 0; +} + +uint32_t WasapiCapture::boundaryFadeInFrameCount() const { + const uint32_t rate = sampleRate(); + if (rate == 0) { + return 1; + } + const uint32_t fadeFrames = (rate * kBoundaryFadeInMs) / 1000; + return fadeFrames > 0 ? fadeFrames : 1; +} + +void WasapiCapture::captureThread() { + // COM must be initialized on every thread that uses COM objects. + // The main thread calls winrt::init_apartment(MTA) but that only covers + // the main thread. Windows 11 enforces per-thread COM init more strictly + // than Windows 10, causing silent audio capture failures without this. + CoInitializeEx(nullptr, COINIT_MULTITHREADED); + + WORD channels = static_cast(mixFormat_->nChannels); + const bool isFloat = isFloatFormat(mixFormat_); + const bool isPcm = isPcmFormat(mixFormat_); + const WORD bitsPerSample = mixFormat_->wBitsPerSample; + const WORD sourceBlockAlign = mixFormat_->nBlockAlign; + const WORD sourceBytesPerSample = + channels > 0 ? static_cast(sourceBlockAlign / channels) : 0; + + std::vector pcmBuffer; + + DWORD sleepMs = static_cast((static_cast(bufferFrameCount_) / mixFormat_->nSamplesPerSec) * 500.0); + if (sleepMs < 5) sleepMs = 5; + + while (capturing_) { + if (paused_) { + Sleep(10); + continue; + } + + Sleep(sleepMs); + + UINT32 packetLength = 0; + HRESULT hr = captureClient_->GetNextPacketSize(&packetLength); + if (FAILED(hr)) { + std::cerr << "WASAPI: GetNextPacketSize failed hr=0x" << std::hex << hr << std::dec << std::endl; + break; + } + + while (packetLength > 0) { + BYTE* data = nullptr; + UINT32 numFrames = 0; + DWORD flags = 0; + UINT64 devicePosition = 0; + UINT64 qpcPosition = 0; + + hr = captureClient_->GetBuffer( + &data, + &numFrames, + &flags, + &devicePosition, + &qpcPosition); + if (FAILED(hr)) { + std::cerr << "WASAPI: GetBuffer failed hr=0x" << std::hex << hr << std::dec << std::endl; + break; + } + + const bool hasDataDiscontinuity = + (flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0; + if (hasDataDiscontinuity) { + dataDiscontinuityCount_.fetch_add(1); + } + if ((flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) != 0) { + timestampErrorCount_.fetch_add(1); + } + const bool hasReliableTimestamp = + numFrames > 0 && + qpcPosition > 0 && + (flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) == 0; + if (hasReliableTimestamp) { + int64_t expected = -1; + firstPacketQpcHns_.compare_exchange_strong( + expected, + static_cast(qpcPosition)); + + const int64_t firstPacketQpcHns = firstPacketQpcHns_.load(); + if ( + firstPacketQpcHns >= 0 && + static_cast(qpcPosition) > firstPacketQpcHns + ) { + const int64_t elapsedHns = + static_cast(qpcPosition) - firstPacketQpcHns; + const int64_t adjustedElapsedHns = + elapsedHns > accumulatedPausedQpcHns_.load() + ? elapsedHns - accumulatedPausedQpcHns_.load() + : 0; + const uint64_t expectedStartFrame = + (static_cast(adjustedElapsedHns) * mixFormat_->nSamplesPerSec + + kHundredNanosecondsPerSecond / 2) / + kHundredNanosecondsPerSecond; + const uint64_t writtenFrames = framesWritten_.load(); + const uint32_t gapThresholdMs = hasDataDiscontinuity + ? kDiscontinuityGapFillThresholdMs + : kMinimumGapFillThresholdMs; + const uint32_t gapThresholdFrameCount = + (mixFormat_->nSamplesPerSec * gapThresholdMs) / 1000; + const uint64_t gapThresholdFrames = + gapThresholdFrameCount > 0 ? gapThresholdFrameCount : 1; + + if (expectedStartFrame > writtenFrames) { + const uint64_t missingFrames = expectedStartFrame - writtenFrames; + if (missingFrames > gapThresholdFrames) { + writeSilenceFrames(missingFrames, channels); + } else if (hasDataDiscontinuity) { + compactedDiscontinuityCount_.fetch_add(1); + compactedDiscontinuityFrames_.fetch_add(missingFrames); + } + } + } + } + + UINT32 totalSamples = numFrames * channels; + const bool isSilentPacket = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0; + const uint32_t silentCompactThresholdFrames = + (mixFormat_->nSamplesPerSec * kSilentDiscontinuityCompactThresholdMs) / 1000; + if ( + hasDataDiscontinuity && + isSilentPacket && + framesWritten_.load() > 0 && + numFrames > 0 && + numFrames <= (silentCompactThresholdFrames > 0 ? silentCompactThresholdFrames : 1) + ) { + compactedSilentDiscontinuityCount_.fetch_add(1); + compactedSilentDiscontinuityFrames_.fetch_add(numFrames); + captureClient_->ReleaseBuffer(numFrames); + hr = captureClient_->GetNextPacketSize(&packetLength); + if (FAILED(hr)) { + std::cerr << "WASAPI: GetNextPacketSize failed hr=0x" << std::hex << hr << std::dec << std::endl; + break; + } + continue; + } + + if (isSilentPacket) { + pcmBuffer.assign(totalSamples, 0); + } else if (isFloat && bitsPerSample == 32 && sourceBytesPerSample >= 4) { + pcmBuffer.resize(totalSamples); + const float* src = reinterpret_cast(data); + for (UINT32 i = 0; i < totalSamples; i++) { + pcmBuffer[i] = floatToInt16(src[i]); + } + } else if (isPcm && bitsPerSample == 16 && sourceBytesPerSample >= 2) { + pcmBuffer.resize(totalSamples); + if (sourceBytesPerSample == sizeof(int16_t)) { + std::memcpy(pcmBuffer.data(), data, totalSamples * sizeof(int16_t)); + } else { + for (UINT32 frame = 0; frame < numFrames; frame++) { + const BYTE* frameData = data + frame * sourceBlockAlign; + for (WORD channel = 0; channel < channels; channel++) { + const BYTE* sample = frameData + channel * sourceBytesPerSample; + pcmBuffer[frame * channels + channel] = + *reinterpret_cast(sample); + } + } + } + } else if (isPcm && bitsPerSample == 24 && sourceBytesPerSample >= 3) { + pcmBuffer.resize(totalSamples); + for (UINT32 frame = 0; frame < numFrames; frame++) { + const BYTE* frameData = data + frame * sourceBlockAlign; + for (WORD channel = 0; channel < channels; channel++) { + const BYTE* sample = frameData + channel * sourceBytesPerSample; + pcmBuffer[frame * channels + channel] = pcm24ToInt16(sample); + } + } + } else if (isPcm && bitsPerSample == 32 && sourceBytesPerSample >= 4) { + pcmBuffer.resize(totalSamples); + for (UINT32 frame = 0; frame < numFrames; frame++) { + const BYTE* frameData = data + frame * sourceBlockAlign; + for (WORD channel = 0; channel < channels; channel++) { + const BYTE* sample = frameData + channel * sourceBytesPerSample; + const int32_t value = *reinterpret_cast(sample); + pcmBuffer[frame * channels + channel] = static_cast(value >> 16); + } + } + } else if (isPcm && bitsPerSample == 8 && sourceBytesPerSample >= 1) { + pcmBuffer.resize(totalSamples); + for (UINT32 frame = 0; frame < numFrames; frame++) { + const BYTE* frameData = data + frame * sourceBlockAlign; + for (WORD channel = 0; channel < channels; channel++) { + const BYTE value = *(frameData + channel * sourceBytesPerSample); + pcmBuffer[frame * channels + channel] = + static_cast((static_cast(value) - 128) << 8); + } + } + } else { + pcmBuffer.assign(totalSamples, 0); + } + + captureClient_->ReleaseBuffer(numFrames); + + writePcmFrames(pcmBuffer.data(), numFrames, channels); + + hr = captureClient_->GetNextPacketSize(&packetLength); + if (FAILED(hr)) { + std::cerr << "WASAPI: GetNextPacketSize failed hr=0x" << std::hex << hr << std::dec << std::endl; + break; + } + } + } + + CoUninitialize(); +} diff --git a/electron/native/wgc-capture/src/wasapi_loopback.h b/electron/native/wgc-capture/src/wasapi_loopback.h new file mode 100644 index 000000000..ab3cd7a6c --- /dev/null +++ b/electron/native/wgc-capture/src/wasapi_loopback.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class WasapiCapture { +public: + WasapiCapture(); + ~WasapiCapture(); + + bool initializeLoopback(const std::string& outputPath); + bool initializeMic( + const std::string& outputPath, + const std::string& deviceId = "", + const std::string& deviceName = ""); + bool start(); + bool pause(); + bool resume(); + void stop(); + int64_t firstPacketQpcHns() const { return firstPacketQpcHns_.load(); } + uint64_t capturedDurationMs() const; + uint64_t totalDataBytes() const { return totalDataBytes_.load(); } + uint32_t sampleRate() const; + uint16_t channelCount() const; + uint32_t dataDiscontinuityCount() const { return dataDiscontinuityCount_.load(); } + uint32_t timestampErrorCount() const { return timestampErrorCount_.load(); } + uint32_t gapFillCount() const { return gapFillCount_.load(); } + uint64_t insertedSilenceFrames() const { return insertedSilenceFrames_.load(); } + uint32_t compactedDiscontinuityCount() const { return compactedDiscontinuityCount_.load(); } + uint64_t compactedDiscontinuityFrames() const { return compactedDiscontinuityFrames_.load(); } + uint32_t compactedSilentDiscontinuityCount() const { return compactedSilentDiscontinuityCount_.load(); } + uint64_t compactedSilentDiscontinuityFrames() const { return compactedSilentDiscontinuityFrames_.load(); } + +private: + bool initializeCommon(); + void captureThread(); + bool writeWavHeader(HANDLE file, DWORD dataSize); + void writePcmFrames(const int16_t* samples, UINT32 frameCount, WORD channels); + void writeSilenceFrames(uint64_t frameCount, WORD channels); + uint32_t boundaryFadeInFrameCount() const; + IMMDevice* findCaptureDeviceByName(const std::wstring& name); + + std::string outputPath_; + std::thread thread_; + std::atomic capturing_{false}; + std::atomic paused_{false}; + HANDLE outputFile_ = INVALID_HANDLE_VALUE; + std::atomic totalDataBytes_{0}; + std::atomic framesWritten_{0}; + + IMMDeviceEnumerator* enumerator_ = nullptr; + IMMDevice* device_ = nullptr; + IAudioClient* audioClient_ = nullptr; + IAudioCaptureClient* captureClient_ = nullptr; + WAVEFORMATEX* mixFormat_ = nullptr; + DWORD streamFlags_ = 0; + + UINT32 bufferFrameCount_ = 0; + std::atomic firstPacketQpcHns_{-1}; + std::atomic pauseStartQpcHns_{0}; + std::atomic accumulatedPausedQpcHns_{0}; + std::atomic dataDiscontinuityCount_{0}; + std::atomic timestampErrorCount_{0}; + std::atomic gapFillCount_{0}; + std::atomic insertedSilenceFrames_{0}; + std::atomic compactedDiscontinuityCount_{0}; + std::atomic compactedDiscontinuityFrames_{0}; + std::atomic compactedSilentDiscontinuityCount_{0}; + std::atomic compactedSilentDiscontinuityFrames_{0}; + std::atomic fadeInFramesRemaining_{0}; +}; diff --git a/electron/native/wgc-capture/src/wgc_session.cpp b/electron/native/wgc-capture/src/wgc_session.cpp new file mode 100644 index 000000000..1dde80815 --- /dev/null +++ b/electron/native/wgc-capture/src/wgc_session.cpp @@ -0,0 +1,374 @@ +#include "wgc_session.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +// IDirect3DDxgiInterfaceAccess is a COM interface for getting the DXGI interface +// from a WinRT IDirect3DSurface +MIDL_INTERFACE("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1") +IDirect3DDxgiInterfaceAccess : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetInterface(REFIID iid, void** p) = 0; +}; + +// Convert ID3D11Device → IDirect3DDevice (WinRT interop) +extern "C" { + HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice( + IDXGIDevice* dxgiDevice, + IInspectable** graphicsDevice); +} + +static int normalizeFramePoolExtent(int value) { + int normalized = value < 2 ? 2 : value; + if ((normalized % 2) != 0) ++normalized; + return normalized; +} + +WgcSession::WgcSession() {} + +WgcSession::~WgcSession() { + stopCapture(); +} + +bool WgcSession::createD3DDevice() { + D3D_FEATURE_LEVEL featureLevels[] = { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + }; + + HRESULT hr = D3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_HARDWARE, + nullptr, + D3D11_CREATE_DEVICE_BGRA_SUPPORT, + featureLevels, + ARRAYSIZE(featureLevels), + D3D11_SDK_VERSION, + &d3dDevice_, + nullptr, + &d3dContext_); + + if (FAILED(hr)) { + std::cerr << "ERROR: D3D11CreateDevice failed: 0x" << std::hex << hr << std::endl; + return false; + } + + return true; +} + +winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice WgcSession::createWinRTDevice() { + ComPtr dxgiDevice; + HRESULT hr = d3dDevice_.As(&dxgiDevice); + if (FAILED(hr)) return nullptr; + + winrt::com_ptr inspectable; + hr = CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.Get(), inspectable.put()); + if (FAILED(hr)) return nullptr; + + return inspectable.as(); +} + +winrt::Windows::Graphics::Capture::GraphicsCaptureItem WgcSession::createCaptureItemForMonitor(HMONITOR monitor) { + auto factory = winrt::get_activation_factory< + winrt::Windows::Graphics::Capture::GraphicsCaptureItem>(); + + auto interop = factory.as(); + + winrt::Windows::Graphics::Capture::GraphicsCaptureItem item{nullptr}; + HRESULT hr = interop->CreateForMonitor( + monitor, + winrt::guid_of(), + winrt::put_abi(item)); + + if (FAILED(hr)) { + std::cerr << "ERROR: CreateForMonitor failed: 0x" << std::hex << hr << std::endl; + return nullptr; + } + + return item; +} + +bool WgcSession::initializeWithItem(int fps) { + if (!captureItem_) return false; + + auto size = captureItem_.Size(); + captureWidth_ = size.Width; + captureHeight_ = size.Height; + framePoolWidth_ = size.Width; + framePoolHeight_ = size.Height; + + framePool_ = winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::CreateFreeThreaded( + winrtDevice_, + winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, + 2, + size); + + session_ = framePool_.CreateCaptureSession(captureItem_); + + session_.IsCursorCaptureEnabled(false); + + // IsBorderRequired is only available on Windows 11+ (build 22000). propagating an hresult_error results in Native Windows capture failure + try { + session_.IsBorderRequired(false); + } catch (winrt::hresult_error const&) { + } + + return true; +} + +bool WgcSession::recreateFramePoolIfNeeded( + winrt::Windows::Graphics::SizeInt32 const& contentSize) { + if (!framePool_) return false; + + const int normalizedWidth = normalizeFramePoolExtent(contentSize.Width); + const int normalizedHeight = normalizeFramePoolExtent(contentSize.Height); + if (normalizedWidth == framePoolWidth_ && normalizedHeight == framePoolHeight_) { + return false; + } + + winrt::Windows::Graphics::SizeInt32 normalizedSize{ + normalizedWidth, + normalizedHeight, + }; + + try { + framePool_.Recreate( + winrtDevice_, + winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, + 2, + normalizedSize); + framePoolWidth_ = normalizedWidth; + framePoolHeight_ = normalizedHeight; + std::cerr << "INFO: Recreated WGC frame pool for resized content " + << framePoolWidth_ << "x" << framePoolHeight_ << std::endl; + } catch (winrt::hresult_error const& e) { + fatalError_ = true; + capturing_ = false; + std::cerr << "ERROR: Failed to recreate WGC frame pool after resize: 0x" + << std::hex << e.code() << std::dec << std::endl; + } + + return true; +} + +bool WgcSession::initialize(HMONITOR monitor, int fps) { + fps_ = fps; + frameIntervalHns_ = 10000000LL / fps_; + + if (!createD3DDevice()) return false; + + winrtDevice_ = createWinRTDevice(); + if (!winrtDevice_) { + std::cerr << "ERROR: Failed to create WinRT D3D device" << std::endl; + return false; + } + + captureItem_ = createCaptureItemForMonitor(monitor); + return initializeWithItem(fps); +} + +bool WgcSession::initialize(HWND hwnd, int fps) { + fps_ = fps; + frameIntervalHns_ = 10000000LL / fps_; + + if (!createD3DDevice()) return false; + + winrtDevice_ = createWinRTDevice(); + if (!winrtDevice_) { + std::cerr << "ERROR: Failed to create WinRT D3D device" << std::endl; + return false; + } + + HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (!monitor) return false; + + captureItem_ = createCaptureItemForMonitor(monitor); + return initializeWithItem(fps) && initializeWindowCrop(hwnd); +} + +bool WgcSession::initializeWindowCrop(HWND hwnd) { + windowHandle_ = hwnd; + MONITORINFO monitorInfo{}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &monitorInfo)) return false; + monitorBounds_ = monitorInfo.rcMonitor; + + return updateWindowCropRect(true); +} + +bool WgcSession::updateWindowCropRect(bool initializeSize) { + RECT windowBounds{}; + if (FAILED(DwmGetWindowAttribute(windowHandle_, DWMWA_EXTENDED_FRAME_BOUNDS, &windowBounds, sizeof(windowBounds))) && + !GetWindowRect(windowHandle_, &windowBounds)) return false; + RECT clipped{}; + if (!IntersectRect(&clipped, &windowBounds, &monitorBounds_)) return false; + + const LONG monitorWidth = monitorBounds_.right - monitorBounds_.left; + const LONG monitorHeight = monitorBounds_.bottom - monitorBounds_.top; + if (monitorWidth <= 0 || monitorHeight <= 0 || framePoolWidth_ < 2 || framePoolHeight_ < 2) return false; + + // WGC textures are in capture-surface pixels, while Win32 monitor/window + // rectangles can be DPI-virtualized. Map both edges into texture space + // instead of assuming those coordinate systems are identical. + const auto mapX = [this, monitorWidth](LONG desktopX) { + const double normalized = static_cast(desktopX - monitorBounds_.left) / + static_cast(monitorWidth); + return std::clamp( + static_cast(std::llround(normalized * framePoolWidth_)), + 0L, + static_cast(framePoolWidth_)); + }; + const auto mapY = [this, monitorHeight](LONG desktopY) { + const double normalized = static_cast(desktopY - monitorBounds_.top) / + static_cast(monitorHeight); + return std::clamp( + static_cast(std::llround(normalized * framePoolHeight_)), + 0L, + static_cast(framePoolHeight_)); + }; + + LONG left = mapX(clipped.left); + LONG top = mapY(clipped.top); + LONG right = mapX(clipped.right); + LONG bottom = mapY(clipped.bottom); + const LONG mappedWidth = (right - left) & ~1L; + const LONG mappedHeight = (bottom - top) & ~1L; + if (mappedWidth < 2 || mappedHeight < 2) return false; + + if (initializeSize) { + captureWidth_ = static_cast(mappedWidth); + captureHeight_ = static_cast(mappedHeight); + + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = static_cast(captureWidth_); + desc.Height = static_cast(captureHeight_); + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET; + + ComPtr resizedTexture; + if (FAILED(d3dDevice_->CreateTexture2D(&desc, nullptr, &resizedTexture))) return false; + ComPtr renderTargetView; + if (FAILED(d3dDevice_->CreateRenderTargetView(resizedTexture.Get(), nullptr, &renderTargetView))) return false; + cropTexture_ = resizedTexture; + cropRenderTargetView_ = renderTargetView; + } + + if (!cropTexture_ || !cropRenderTargetView_) return false; + + // The encoder's dimensions are fixed for the lifetime of the MP4. Keep the + // crop texture fixed too: reallocating it after a resize made the encoder + // pad the changed frame with black bars. If the selected window shrinks, + // copy only its current area so the crop never exposes nearby desktop pixels. + const LONG copyWidth = (std::min)(mappedWidth, static_cast(captureWidth_)); + const LONG copyHeight = (std::min)(mappedHeight, static_cast(captureHeight_)); + left = std::clamp(left, 0L, static_cast(framePoolWidth_) - copyWidth); + top = std::clamp(top, 0L, static_cast(framePoolHeight_) - copyHeight); + cropRect_ = {left, top, left + copyWidth, top + copyHeight}; + return true; +} + +void WgcSession::setFrameCallback(FrameCallback callback) { + frameCallback_ = std::move(callback); +} + +bool WgcSession::startCapture() { + if (!session_ || !framePool_) return false; + + capturing_ = true; + fatalError_ = false; + lastFrameTimeHns_ = 0; + + frameArrivedRevoker_ = framePool_.FrameArrived( + winrt::auto_revoke, + [this](auto const& sender, auto const& args) { + onFrameArrived(sender, args); + }); + + session_.StartCapture(); + return true; +} + +void WgcSession::stopCapture() { + capturing_ = false; + + frameArrivedRevoker_.revoke(); + + if (session_) { + session_.Close(); + session_ = nullptr; + } + if (framePool_) { + framePool_.Close(); + framePool_ = nullptr; + } + cropTexture_.Reset(); + cropRenderTargetView_.Reset(); +} + +void WgcSession::onFrameArrived( + winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender, + winrt::Windows::Foundation::IInspectable const&) { + + if (!capturing_ || fatalError_) return; + + auto frame = sender.TryGetNextFrame(); + if (!frame) return; + auto contentSize = frame.ContentSize(); + if (recreateFramePoolIfNeeded(contentSize)) { + frame.Close(); + return; + } + + auto timestamp = frame.SystemRelativeTime(); + int64_t frameTimeHns = std::chrono::duration_cast>>(timestamp).count(); + + // Frame rate limiting: skip frames that arrive too soon + if (lastFrameTimeHns_ > 0 && (frameTimeHns - lastFrameTimeHns_) < (frameIntervalHns_ * 7 / 10)) { + frame.Close(); + return; + } + lastFrameTimeHns_ = frameTimeHns; + + auto surface = frame.Surface(); + + // Get the underlying D3D texture from the WinRT surface via COM interop + winrt::com_ptr access; + try { + access = surface.as(); + } catch (...) { + frame.Close(); + return; + } + ComPtr texture; + HRESULT hr = access->GetInterface(IID_PPV_ARGS(&texture)); + + if (SUCCEEDED(hr) && texture && frameCallback_) { + if (windowHandle_ && cropTexture_ && updateWindowCropRect()) { + constexpr float clearColor[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + d3dContext_->ClearRenderTargetView(cropRenderTargetView_.Get(), clearColor); + D3D11_BOX sourceBox{ + static_cast(cropRect_.left), static_cast(cropRect_.top), 0, + static_cast(cropRect_.right), static_cast(cropRect_.bottom), 1, + }; + d3dContext_->CopySubresourceRegion(cropTexture_.Get(), 0, 0, 0, 0, texture.Get(), 0, &sourceBox); + frameCallback_(cropTexture_.Get(), frameTimeHns); + } else if (!windowHandle_) { + frameCallback_(texture.Get(), frameTimeHns); + } + } + + frame.Close(); +} diff --git a/electron/native/wgc-capture/src/wgc_session.h b/electron/native/wgc-capture/src/wgc_session.h new file mode 100644 index 000000000..a2a154668 --- /dev/null +++ b/electron/native/wgc-capture/src/wgc_session.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +class WgcSession { +public: + using FrameCallback = std::function; + + WgcSession(); + ~WgcSession(); + + bool initialize(HMONITOR monitor, int fps); + bool initialize(HWND hwnd, int fps); + void setFrameCallback(FrameCallback callback); + bool startCapture(); + void stopCapture(); + bool hasFatalError() const { return fatalError_.load(); } + + int captureWidth() const { return captureWidth_; } + int captureHeight() const { return captureHeight_; } + ID3D11Device* device() const { return d3dDevice_.Get(); } + ID3D11DeviceContext* context() const { return d3dContext_.Get(); } + +private: + ComPtr d3dDevice_; + ComPtr d3dContext_; + ComPtr cropTexture_; + ComPtr cropRenderTargetView_; + winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice winrtDevice_{nullptr}; + winrt::Windows::Graphics::Capture::GraphicsCaptureItem captureItem_{nullptr}; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool framePool_{nullptr}; + winrt::Windows::Graphics::Capture::GraphicsCaptureSession session_{nullptr}; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::FrameArrived_revoker frameArrivedRevoker_; + + FrameCallback frameCallback_; + std::atomic capturing_{false}; + std::atomic fatalError_{false}; + int fps_ = 60; + int captureWidth_ = 0; + int captureHeight_ = 0; + int framePoolWidth_ = 0; + int framePoolHeight_ = 0; + int64_t frameIntervalHns_ = 0; + int64_t lastFrameTimeHns_ = 0; + HWND windowHandle_ = nullptr; + RECT monitorBounds_{}; + RECT cropRect_{}; + + bool createD3DDevice(); + winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice createWinRTDevice(); + winrt::Windows::Graphics::Capture::GraphicsCaptureItem createCaptureItemForMonitor(HMONITOR monitor); + bool initializeWithItem(int fps); + bool initializeWindowCrop(HWND hwnd); + bool updateWindowCropRect(bool initializeSize = false); + bool recreateFramePoolIfNeeded( + winrt::Windows::Graphics::SizeInt32 const& contentSize); + void onFrameArrived( + winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender, + winrt::Windows::Foundation::IInspectable const& args); +}; diff --git a/electron/native/windows-capture/CMakeLists.txt b/electron/native/windows-capture/CMakeLists.txt new file mode 100644 index 000000000..773452c48 --- /dev/null +++ b/electron/native/windows-capture/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.20) +project(windows-capture LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(windows-capture + src/main.cpp + src/dxgi_session.cpp + src/mf_encoder.cpp + src/monitor_utils.cpp + src/wasapi_loopback.cpp +) + +target_compile_options(windows-capture PRIVATE /EHsc /W3 /utf-8) + +target_link_libraries(windows-capture PRIVATE + d3d11 + dxgi + dwmapi + mfplat + mfreadwrite + mf + mfuuid + ole32 + shcore +) diff --git a/electron/native/windows-capture/src/dxgi_session.cpp b/electron/native/windows-capture/src/dxgi_session.cpp new file mode 100644 index 000000000..76c914021 --- /dev/null +++ b/electron/native/windows-capture/src/dxgi_session.cpp @@ -0,0 +1,343 @@ +#include "dxgi_session.h" + +#include + +#include +#include +#include + +namespace { + +bool intersectRectChecked(const RECT& lhs, const RECT& rhs, RECT& result) { + return IntersectRect(&result, &lhs, &rhs) != FALSE; +} + +int evenFloor(int value) { + return value > 1 ? (value & ~1) : value; +} + +RECT getExtendedWindowBounds(HWND hwnd) { + RECT bounds{}; + if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &bounds, sizeof(bounds)))) { + return bounds; + } + + GetWindowRect(hwnd, &bounds); + return bounds; +} + +} // namespace + +DxgiSession::DxgiSession() {} + +DxgiSession::~DxgiSession() { + stopCapture(); +} + +bool DxgiSession::findOutputForMonitor(HMONITOR monitor, ComPtr& adapter, ComPtr& output) { + ComPtr factory; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); + if (FAILED(hr)) { + std::cerr << "ERROR: CreateDXGIFactory1 failed: 0x" << std::hex << hr << std::endl; + return false; + } + + for (UINT adapterIndex = 0;; ++adapterIndex) { + ComPtr candidateAdapter; + hr = factory->EnumAdapters1(adapterIndex, &candidateAdapter); + if (hr == DXGI_ERROR_NOT_FOUND) { + break; + } + if (FAILED(hr)) { + continue; + } + + for (UINT outputIndex = 0;; ++outputIndex) { + ComPtr candidateOutput; + hr = candidateAdapter->EnumOutputs(outputIndex, &candidateOutput); + if (hr == DXGI_ERROR_NOT_FOUND) { + break; + } + if (FAILED(hr)) { + continue; + } + + DXGI_OUTPUT_DESC desc{}; + if (FAILED(candidateOutput->GetDesc(&desc))) { + continue; + } + + if (!desc.AttachedToDesktop || desc.Monitor != monitor) { + continue; + } + + ComPtr output1; + hr = candidateOutput.As(&output1); + if (FAILED(hr)) { + continue; + } + + adapter = candidateAdapter; + output = output1; + outputDesc_ = desc; + return true; + } + } + + return false; +} + +bool DxgiSession::createD3DDevice(IDXGIAdapter1* adapter) { + UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + D3D_FEATURE_LEVEL featureLevels[] = { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + }; + + HRESULT hr = D3D11CreateDevice( + adapter, + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + creationFlags, + featureLevels, + ARRAYSIZE(featureLevels), + D3D11_SDK_VERSION, + &d3dDevice_, + nullptr, + &d3dContext_); + + if (FAILED(hr)) { + std::cerr << "ERROR: D3D11CreateDevice failed: 0x" << std::hex << hr << std::endl; + return false; + } + + return true; +} + +bool DxgiSession::createDuplication(IDXGIOutput1* output) { + HRESULT hr = output->DuplicateOutput(d3dDevice_.Get(), &duplication_); + if (FAILED(hr)) { + std::cerr << "ERROR: DuplicateOutput failed: 0x" << std::hex << hr << std::endl; + return false; + } + + return true; +} + +bool DxgiSession::createCaptureTexture() { + if (captureWidth_ <= 0 || captureHeight_ <= 0) { + return false; + } + + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = static_cast(captureWidth_); + desc.Height = static_cast(captureHeight_); + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + + HRESULT hr = d3dDevice_->CreateTexture2D(&desc, nullptr, &captureTexture_); + if (FAILED(hr)) { + std::cerr << "ERROR: CreateTexture2D failed: 0x" << std::hex << hr << std::endl; + return false; + } + + return true; +} + +bool DxgiSession::initializeFullMonitorRect() { + const int width = outputDesc_.DesktopCoordinates.right - outputDesc_.DesktopCoordinates.left; + const int height = outputDesc_.DesktopCoordinates.bottom - outputDesc_.DesktopCoordinates.top; + + captureWidth_ = evenFloor(width); + captureHeight_ = evenFloor(height); + if (captureWidth_ <= 0 || captureHeight_ <= 0) { + return false; + } + + captureRect_.left = 0; + captureRect_.top = 0; + captureRect_.right = captureWidth_; + captureRect_.bottom = captureHeight_; + return true; +} + +bool DxgiSession::updateWindowCaptureRect() { + if (!captureWindow_ || !windowHandle_) { + return false; + } + + RECT windowRect = getExtendedWindowBounds(windowHandle_); + RECT monitorRect = outputDesc_.DesktopCoordinates; + RECT clippedRect{}; + if (!intersectRectChecked(windowRect, monitorRect, clippedRect)) { + return false; + } + + int left = clippedRect.left - monitorRect.left; + int top = clippedRect.top - monitorRect.top; + const int monitorWidth = monitorRect.right - monitorRect.left; + const int monitorHeight = monitorRect.bottom - monitorRect.top; + + left = std::clamp(left, 0, std::max(0, monitorWidth - captureWidth_)); + top = std::clamp(top, 0, std::max(0, monitorHeight - captureHeight_)); + + captureRect_.left = left; + captureRect_.top = top; + captureRect_.right = left + captureWidth_; + captureRect_.bottom = top + captureHeight_; + return true; +} + +bool DxgiSession::initializeWindowRect(HWND hwnd) { + windowHandle_ = hwnd; + captureWindow_ = true; + + RECT windowRect = getExtendedWindowBounds(hwnd); + RECT monitorRect = outputDesc_.DesktopCoordinates; + RECT clippedRect{}; + if (!intersectRectChecked(windowRect, monitorRect, clippedRect)) { + return false; + } + + captureWidth_ = evenFloor(clippedRect.right - clippedRect.left); + captureHeight_ = evenFloor(clippedRect.bottom - clippedRect.top); + if (captureWidth_ <= 0 || captureHeight_ <= 0) { + return false; + } + + return updateWindowCaptureRect(); +} + +bool DxgiSession::initializeForMonitor(HMONITOR monitor, int fps) { + fps_ = fps; + frameIntervalHns_ = 10000000LL / fps_; + + ComPtr adapter; + ComPtr output; + if (!findOutputForMonitor(monitor, adapter, output)) { + std::cerr << "ERROR: Failed to find DXGI output for monitor" << std::endl; + return false; + } + + if (!createD3DDevice(adapter.Get())) { + return false; + } + + if (!createDuplication(output.Get())) { + return false; + } + + return true; +} + +bool DxgiSession::initialize(HMONITOR monitor, int fps) { + captureWindow_ = false; + windowHandle_ = nullptr; + + if (!initializeForMonitor(monitor, fps)) { + return false; + } + + return initializeFullMonitorRect() && createCaptureTexture(); +} + +bool DxgiSession::initialize(HWND hwnd, int fps) { + HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (!monitor) { + std::cerr << "ERROR: MonitorFromWindow failed" << std::endl; + return false; + } + + if (!initializeForMonitor(monitor, fps)) { + return false; + } + + return initializeWindowRect(hwnd) && createCaptureTexture(); +} + +void DxgiSession::setFrameCallback(FrameCallback callback) { + frameCallback_ = std::move(callback); +} + +bool DxgiSession::startCapture() { + if (!duplication_ || !captureTexture_) { + return false; + } + + capturing_ = true; + lastFrameTimeHns_ = 0; + captureThread_ = std::thread(&DxgiSession::captureLoop, this); + return true; +} + +void DxgiSession::stopCapture() { + capturing_ = false; + + if (captureThread_.joinable()) { + captureThread_.join(); + } + + duplication_.Reset(); + captureTexture_.Reset(); +} + +int64_t DxgiSession::nowHns() const { + return std::chrono::duration_cast>>( + std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +void DxgiSession::captureLoop() { + while (capturing_) { + DXGI_OUTDUPL_FRAME_INFO frameInfo{}; + ComPtr desktopResource; + HRESULT hr = duplication_->AcquireNextFrame(100, &frameInfo, &desktopResource); + + if (hr == DXGI_ERROR_WAIT_TIMEOUT) { + continue; + } + + if (FAILED(hr)) { + if (hr != DXGI_ERROR_ACCESS_LOST) { + std::cerr << "ERROR: AcquireNextFrame failed: 0x" << std::hex << hr << std::endl; + } + break; + } + + ComPtr sourceTexture; + hr = desktopResource.As(&sourceTexture); + if (SUCCEEDED(hr) && sourceTexture) { + if (!captureWindow_ || updateWindowCaptureRect()) { + const int64_t timestampHns = nowHns(); + if (lastFrameTimeHns_ == 0 || (timestampHns - lastFrameTimeHns_) >= (frameIntervalHns_ * 7 / 10)) { + D3D11_BOX sourceBox{}; + sourceBox.left = static_cast(captureRect_.left); + sourceBox.top = static_cast(captureRect_.top); + sourceBox.front = 0; + sourceBox.right = static_cast(captureRect_.right); + sourceBox.bottom = static_cast(captureRect_.bottom); + sourceBox.back = 1; + + d3dContext_->CopySubresourceRegion( + captureTexture_.Get(), + 0, + 0, + 0, + 0, + sourceTexture.Get(), + 0, + &sourceBox); + + if (frameCallback_) { + lastFrameTimeHns_ = timestampHns; + frameCallback_(captureTexture_.Get(), timestampHns); + } + } + } + } + + duplication_->ReleaseFrame(); + } +} \ No newline at end of file diff --git a/electron/native/windows-capture/src/dxgi_session.h b/electron/native/windows-capture/src/dxgi_session.h new file mode 100644 index 000000000..37cd04b71 --- /dev/null +++ b/electron/native/windows-capture/src/dxgi_session.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +class DxgiSession { +public: + using FrameCallback = std::function; + + DxgiSession(); + ~DxgiSession(); + + bool initialize(HMONITOR monitor, int fps); + bool initialize(HWND hwnd, int fps); + void setFrameCallback(FrameCallback callback); + bool startCapture(); + void stopCapture(); + + int captureWidth() const { return captureWidth_; } + int captureHeight() const { return captureHeight_; } + ID3D11Device* device() const { return d3dDevice_.Get(); } + ID3D11DeviceContext* context() const { return d3dContext_.Get(); } + +private: + ComPtr d3dDevice_; + ComPtr d3dContext_; + ComPtr duplication_; + ComPtr captureTexture_; + + FrameCallback frameCallback_; + std::atomic capturing_{false}; + std::thread captureThread_; + + DXGI_OUTPUT_DESC outputDesc_{}; + RECT captureRect_{}; + HWND windowHandle_ = nullptr; + bool captureWindow_ = false; + int fps_ = 60; + int captureWidth_ = 0; + int captureHeight_ = 0; + int64_t frameIntervalHns_ = 0; + int64_t lastFrameTimeHns_ = 0; + + bool initializeForMonitor(HMONITOR monitor, int fps); + bool findOutputForMonitor(HMONITOR monitor, ComPtr& adapter, ComPtr& output); + bool createD3DDevice(IDXGIAdapter1* adapter); + bool createDuplication(IDXGIOutput1* output); + bool createCaptureTexture(); + bool initializeFullMonitorRect(); + bool initializeWindowRect(HWND hwnd); + bool updateWindowCaptureRect(); + void captureLoop(); + int64_t nowHns() const; +}; \ No newline at end of file diff --git a/electron/native/windows-capture/src/main.cpp b/electron/native/windows-capture/src/main.cpp new file mode 100644 index 000000000..2aac86a71 --- /dev/null +++ b/electron/native/windows-capture/src/main.cpp @@ -0,0 +1,324 @@ +#include "dxgi_session.h" +#include "mf_encoder.h" +#include "monitor_utils.h" +#include "wasapi_loopback.h" + +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_stopRequested{false}; +static std::atomic g_pauseRequested{false}; +static std::atomic g_resumePending{false}; +static std::atomic g_lastFrameTimestampHns{0}; +static std::atomic g_pauseStartTimestampHns{0}; +static std::atomic g_accumulatedPausedHns{0}; +static std::mutex g_stopMutex; +static std::condition_variable g_stopCv; + +struct CaptureConfig { + int64_t displayId = 0; + int64_t windowHandle = 0; + std::string outputPath; + std::string audioOutputPath; + std::string micOutputPath; + std::string micDeviceName; + int fps = 60; + int width = 0; + int height = 0; + bool captureSystemAudio = false; + bool captureMic = false; +}; + +static bool parseSimpleJson(const std::string& json, CaptureConfig& config) { + auto findInt = [&](const std::string& key) -> int { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return -1; + pos = json.find(':', pos); + if (pos == std::string::npos) return -1; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; + try { + return std::stoi(json.substr(pos)); + } catch (...) { + return -1; + } + }; + + auto findInt64 = [&](const std::string& key) -> int64_t { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return -1; + pos = json.find(':', pos); + if (pos == std::string::npos) return -1; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; + try { + return std::stoll(json.substr(pos)); + } catch (...) { + return -1; + } + }; + + auto findString = [&](const std::string& key) -> std::string { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return ""; + pos = json.find(':', pos); + if (pos == std::string::npos) return ""; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; + if (pos >= json.size() || json[pos] != '"') return ""; + pos++; + std::string result; + while (pos < json.size() && json[pos] != '"') { + if (json[pos] == '\\' && pos + 1 < json.size()) { + pos++; + if (json[pos] == 'n') result += '\n'; + else if (json[pos] == 't') result += '\t'; + else if (json[pos] == '\\') result += '\\'; + else if (json[pos] == '"') result += '"'; + else if (json[pos] == '/') result += '/'; + else result += json[pos]; + } else { + result += json[pos]; + } + pos++; + } + return result; + }; + + config.outputPath = findString("outputPath"); + if (config.outputPath.empty()) return false; + + int64_t displayId = findInt64("displayId"); + if (displayId >= 0) config.displayId = displayId; + + int64_t windowHandle = findInt64("windowHandle"); + if (windowHandle > 0) config.windowHandle = windowHandle; + + int fps = findInt("fps"); + if (fps > 0) config.fps = fps; + + int width = findInt("width"); + if (width > 0) config.width = width; + + int height = findInt("height"); + if (height > 0) config.height = height; + + config.audioOutputPath = findString("audioOutputPath"); + config.micOutputPath = findString("micOutputPath"); + config.micDeviceName = findString("micDeviceName"); + + auto findBool = [&](const std::string& key) -> bool { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return false; + auto colonPos = json.find(':', pos); + if (colonPos == std::string::npos) return false; + auto valStart = json.find_first_not_of(" \t", colonPos + 1); + return valStart != std::string::npos && json.substr(valStart, 4) == "true"; + }; + + config.captureSystemAudio = findBool("captureSystemAudio"); + config.captureMic = findBool("captureMic"); + + return true; +} + +static std::wstring utf8ToWide(const std::string& str) { + if (str.empty()) return L""; + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0); + std::wstring wstr(len, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), &wstr[0], len); + return wstr; +} + +static void stdinListenerThread() { + std::string line; + while (std::getline(std::cin, line)) { + // Trim whitespace + while (!line.empty() && (line.back() == '\r' || line.back() == '\n' || line.back() == ' ')) { + line.pop_back(); + } + + if (line == "pause") { + g_pauseRequested = true; + g_pauseStartTimestampHns = g_lastFrameTimestampHns.load(); + continue; + } + + if (line == "resume") { + g_pauseRequested = false; + g_resumePending = true; + continue; + } + + if (line == "stop") { + g_stopRequested = true; + g_stopCv.notify_all(); + return; + } + } + + // stdin closed (parent process died) + g_stopRequested = true; + g_stopCv.notify_all(); +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "ERROR: Missing JSON config argument" << std::endl; + return 1; + } + + CaptureConfig config; + if (!parseSimpleJson(argv[1], config)) { + std::cerr << "ERROR: Failed to parse config JSON" << std::endl; + return 1; + } + + DxgiSession session; + + if (config.windowHandle > 0) { + HWND hwnd = reinterpret_cast(static_cast(config.windowHandle)); + if (!IsWindow(hwnd)) { + std::cerr << "ERROR: Invalid window handle " << config.windowHandle << std::endl; + return 1; + } + if (!session.initialize(hwnd, config.fps)) { + std::cerr << "ERROR: Failed to initialize DXGI window capture session" << std::endl; + return 1; + } + } else { + HMONITOR monitor = findMonitorByDisplayId(config.displayId); + if (!monitor) { + std::cerr << "ERROR: Could not find monitor for displayId " << config.displayId << std::endl; + return 1; + } + if (!session.initialize(monitor, config.fps)) { + std::cerr << "ERROR: Failed to initialize DXGI capture session" << std::endl; + return 1; + } + } + + int captureWidth = config.width > 0 ? config.width : session.captureWidth(); + int captureHeight = config.height > 0 ? config.height : session.captureHeight(); + + // Ensure even dimensions for H.264 + captureWidth = (captureWidth / 2) * 2; + captureHeight = (captureHeight / 2) * 2; + + // Initialize encoder + MFEncoder encoder; + std::wstring outputPathW = utf8ToWide(config.outputPath); + if (!encoder.initialize(outputPathW, captureWidth, captureHeight, config.fps, + session.device(), session.context())) { + std::cerr << "ERROR: Failed to initialize Media Foundation encoder" << std::endl; + return 1; + } + + // Set up frame callback + std::atomic frameCount{0}; + session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) { + g_lastFrameTimestampHns = timestampHns; + if (g_stopRequested) return; + + if (g_pauseRequested) return; + + int64_t adjustedTimestampHns = timestampHns; + if (g_resumePending.exchange(false)) { + const int64_t pauseStart = g_pauseStartTimestampHns.load(); + if (pauseStart > 0 && timestampHns > pauseStart) { + g_accumulatedPausedHns += (timestampHns - pauseStart); + } + } + + adjustedTimestampHns -= g_accumulatedPausedHns.load(); + if (adjustedTimestampHns < 0) { + adjustedTimestampHns = 0; + } + + if (encoder.writeFrame(texture, adjustedTimestampHns)) { + frameCount++; + } + }); + + // Start stdin listener + std::thread stdinThread(stdinListenerThread); + stdinThread.detach(); + + // Initialize WASAPI captures (but don't start yet) + WasapiCapture loopback; + WasapiCapture micCapture; + bool audioActive = false; + bool audioInitialized = false; + bool micActive = false; + bool micInitialized = false; + + if (config.captureSystemAudio && !config.audioOutputPath.empty()) { + audioInitialized = loopback.initializeLoopback(config.audioOutputPath); + if (!audioInitialized) { + std::cerr << "WARNING: Failed to initialize WASAPI loopback" << std::endl; + } + } + + if (config.captureMic && !config.micOutputPath.empty()) { + micInitialized = micCapture.initializeMic(config.micOutputPath, config.micDeviceName); + if (!micInitialized) { + std::cerr << "WARNING: Failed to initialize WASAPI mic capture" << std::endl; + } + } + + // Start video capture, then audio immediately after for sync + if (!session.startCapture()) { + std::cerr << "ERROR: Failed to start DXGI capture" << std::endl; + return 1; + } + + if (audioInitialized) { + audioActive = loopback.start(); + } + if (micInitialized) { + micActive = micCapture.start(); + } + + std::cout << "Recording started" << std::endl; + std::cout.flush(); + + // Wait for stop signal + while (!g_stopRequested) { + if (g_pauseRequested) { + if (audioActive) loopback.pause(); + if (micActive) micCapture.pause(); + } else { + if (audioActive) loopback.resume(); + if (micActive) micCapture.resume(); + } + + std::unique_lock lock(g_stopMutex); + g_stopCv.wait_for(lock, std::chrono::milliseconds(20), [] { return g_stopRequested.load(); }); + } + + // Stop capture and finalize + session.stopCapture(); + if (audioActive) loopback.stop(); + if (micActive) micCapture.stop(); + encoder.finalize(); + + std::cout << "Recording stopped. Output path: " << config.outputPath << std::endl; + if (audioActive) { + std::cout << "Audio path: " << config.audioOutputPath << std::endl; + } + if (micActive) { + std::cout << "Mic path: " << config.micOutputPath << std::endl; + } + std::cout.flush(); + + // Allow pipe buffers to drain before forceful exit + Sleep(100); + + // Fast exit to avoid WinRT/COM teardown crashes during apartment cleanup + ExitProcess(0); +} diff --git a/electron/native/windows-capture/src/mf_encoder.cpp b/electron/native/windows-capture/src/mf_encoder.cpp new file mode 100644 index 000000000..bfd62f33b --- /dev/null +++ b/electron/native/windows-capture/src/mf_encoder.cpp @@ -0,0 +1,182 @@ +#include "mf_encoder.h" +#include +#include +#include +#include +#include +#include "../../common/bt709_video.h" + +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfreadwrite.lib") +#pragma comment(lib, "mf.lib") +#pragma comment(lib, "mfuuid.lib") + +MFEncoder::MFEncoder() {} + +MFEncoder::~MFEncoder() { + finalize(); +} + +bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height, int fps, + ID3D11Device* device, ID3D11DeviceContext* context) { + if (initialized_) return false; + + if (width % 2 != 0 || height % 2 != 0) { + std::cerr << "ERROR: Encoder dimensions must be even, got " << width << "x" << height << std::endl; + return false; + } + + width_ = width; + height_ = height; + fps_ = fps; + device_ = device; + context_ = context; + + HRESULT hr = MFStartup(MF_VERSION); + if (FAILED(hr)) { + std::cerr << "ERROR: MFStartup failed: 0x" << std::hex << hr << std::endl; + return false; + } + + // Output media type (H.264) + ComPtr outputType; + hr = MFCreateMediaType(&outputType); + if (FAILED(hr)) return false; + + outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264); + outputType->SetUINT32(MF_MT_AVG_BITRATE, 20000000); + MFSetAttributeSize(outputType.Get(), MF_MT_FRAME_SIZE, width_, height_); + MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, fps_, 1); + MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + hr = setBt709LimitedVideoAttributes(outputType.Get()); + if (FAILED(hr)) return false; + + // Input media type (NV12) + ComPtr inputType; + hr = MFCreateMediaType(&inputType); + if (FAILED(hr)) return false; + + inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12); + MFSetAttributeSize(inputType.Get(), MF_MT_FRAME_SIZE, width_, height_); + MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, fps_, 1); + MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + hr = setBt709LimitedVideoAttributes(inputType.Get()); + if (FAILED(hr)) return false; + + // Create SinkWriter with MPEG4 container + ComPtr writerAttrs; + hr = MFCreateAttributes(&writerAttrs, 1); + if (FAILED(hr)) return false; + + writerAttrs->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); + + hr = MFCreateSinkWriterFromURL(outputPath.c_str(), nullptr, writerAttrs.Get(), &sinkWriter_); + if (FAILED(hr)) { + std::cerr << "ERROR: MFCreateSinkWriterFromURL failed: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = sinkWriter_->AddStream(outputType.Get(), &streamIndex_); + if (FAILED(hr)) { + std::cerr << "ERROR: AddStream failed: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = sinkWriter_->SetInputMediaType(streamIndex_, inputType.Get(), nullptr); + if (FAILED(hr)) { + std::cerr << "ERROR: SetInputMediaType failed: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = sinkWriter_->BeginWriting(); + if (FAILED(hr)) { + std::cerr << "ERROR: BeginWriting failed: 0x" << std::hex << hr << std::endl; + return false; + } + + // Pre-allocate staging texture + D3D11_TEXTURE2D_DESC stagingDesc = {}; + stagingDesc.Width = width_; + stagingDesc.Height = height_; + stagingDesc.MipLevels = 1; + stagingDesc.ArraySize = 1; + stagingDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + stagingDesc.SampleDesc.Count = 1; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + hr = device_->CreateTexture2D(&stagingDesc, nullptr, &stagingTexture_); + if (FAILED(hr)) { + std::cerr << "ERROR: Failed to create staging texture: 0x" << std::hex << hr << std::endl; + return false; + } + + // Pre-allocate NV12 buffer + const int ySize = width_ * height_; + const int uvSize = (width_ / 2) * (height_ / 2) * 2; + nv12Buffer_.resize(ySize + uvSize); + + initialized_ = true; + return true; +} + +bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) { + if (!initialized_ || !sinkWriter_) return false; + + context_->CopyResource(stagingTexture_.Get(), texture); + + D3D11_MAPPED_SUBRESOURCE mapped; + HRESULT hr = context_->Map(stagingTexture_.Get(), 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED(hr)) return false; + + // Convert full-range desktop BGRA to explicitly tagged BT.709 video-range NV12. + const uint8_t* bgra = static_cast(mapped.pData); + const int bgraPitch = static_cast(mapped.RowPitch); + convertBgraToBt709LimitedNv12(bgra, bgraPitch, width_, height_, nv12Buffer_); + + context_->Unmap(stagingTexture_.Get(), 0); + + // Create MF sample + DWORD bufferSize = static_cast(nv12Buffer_.size()); + ComPtr buffer; + hr = MFCreateMemoryBuffer(bufferSize, &buffer); + if (FAILED(hr)) return false; + + BYTE* bufferData = nullptr; + hr = buffer->Lock(&bufferData, nullptr, nullptr); + if (FAILED(hr)) return false; + + std::memcpy(bufferData, nv12Buffer_.data(), bufferSize); + buffer->Unlock(); + buffer->SetCurrentLength(bufferSize); + + ComPtr sample; + hr = MFCreateSample(&sample); + if (FAILED(hr)) return false; + + sample->AddBuffer(buffer.Get()); + sample->SetSampleTime(timestampHns); + sample->SetSampleDuration(10000000LL / fps_); + + hr = sinkWriter_->WriteSample(streamIndex_, sample.Get()); + return SUCCEEDED(hr); +} + +bool MFEncoder::finalize() { + if (!initialized_) return false; + initialized_ = false; + + stagingTexture_.Reset(); + nv12Buffer_.clear(); + nv12Buffer_.shrink_to_fit(); + + if (!sinkWriter_) return false; + HRESULT hr = sinkWriter_->Finalize(); + sinkWriter_.Reset(); + MFShutdown(); + return SUCCEEDED(hr); +} diff --git a/electron/native/windows-capture/src/mf_encoder.h b/electron/native/windows-capture/src/mf_encoder.h new file mode 100644 index 000000000..7eb8e6420 --- /dev/null +++ b/electron/native/windows-capture/src/mf_encoder.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +class MFEncoder { +public: + MFEncoder(); + ~MFEncoder(); + + bool initialize(const std::wstring& outputPath, int width, int height, int fps, + ID3D11Device* device, ID3D11DeviceContext* context); + bool writeFrame(ID3D11Texture2D* texture, int64_t timestampHns); + bool finalize(); + +private: + ComPtr sinkWriter_; + ID3D11Device* device_ = nullptr; + ID3D11DeviceContext* context_ = nullptr; + ComPtr stagingTexture_; + std::vector nv12Buffer_; + DWORD streamIndex_ = 0; + int width_ = 0; + int height_ = 0; + int fps_ = 60; + bool initialized_ = false; +}; diff --git a/electron/native/windows-capture/src/monitor_utils.cpp b/electron/native/windows-capture/src/monitor_utils.cpp new file mode 100644 index 000000000..56962aeb7 --- /dev/null +++ b/electron/native/windows-capture/src/monitor_utils.cpp @@ -0,0 +1,61 @@ +#include "monitor_utils.h" +#include + +static BOOL CALLBACK enumMonitorCallback(HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) { + auto* monitors = reinterpret_cast*>(lParam); + + MONITORINFOEXW mi = {}; + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(hMonitor, &mi)) { + MonitorInfo info; + info.handle = hMonitor; + info.x = mi.rcMonitor.left; + info.y = mi.rcMonitor.top; + info.width = mi.rcMonitor.right - mi.rcMonitor.left; + info.height = mi.rcMonitor.bottom - mi.rcMonitor.top; + info.deviceName = mi.szDevice; + monitors->push_back(info); + } + + return TRUE; +} + +std::vector enumerateMonitors() { + std::vector monitors; + EnumDisplayMonitors(nullptr, nullptr, enumMonitorCallback, reinterpret_cast(&monitors)); + return monitors; +} + +// Electron uses the HMONITOR handle value cast to a number as the display ID. +HMONITOR findMonitorByDisplayId(int64_t displayId) { + auto monitors = enumerateMonitors(); + + for (const auto& m : monitors) { + if (static_cast(reinterpret_cast(m.handle)) == displayId) { + return m.handle; + } + } + + if (!monitors.empty()) { + return monitors[0].handle; + } + + return MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY); +} + +MonitorInfo getMonitorInfo(HMONITOR monitor) { + MonitorInfo info; + info.handle = monitor; + + MONITORINFOEXW mi = {}; + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(monitor, &mi)) { + info.x = mi.rcMonitor.left; + info.y = mi.rcMonitor.top; + info.width = mi.rcMonitor.right - mi.rcMonitor.left; + info.height = mi.rcMonitor.bottom - mi.rcMonitor.top; + info.deviceName = mi.szDevice; + } + + return info; +} diff --git a/electron/native/windows-capture/src/monitor_utils.h b/electron/native/windows-capture/src/monitor_utils.h new file mode 100644 index 000000000..8889410dd --- /dev/null +++ b/electron/native/windows-capture/src/monitor_utils.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include + +struct MonitorInfo { + HMONITOR handle; + int x; + int y; + int width; + int height; + std::wstring deviceName; +}; + +std::vector enumerateMonitors(); +HMONITOR findMonitorByDisplayId(int64_t displayId); +MonitorInfo getMonitorInfo(HMONITOR monitor); diff --git a/electron/native/windows-capture/src/wasapi_loopback.cpp b/electron/native/windows-capture/src/wasapi_loopback.cpp new file mode 100644 index 000000000..8ae15ad1f --- /dev/null +++ b/electron/native/windows-capture/src/wasapi_loopback.cpp @@ -0,0 +1,274 @@ +#include "wasapi_loopback.h" +#include +#include +#include + +#pragma comment(lib, "ole32.lib") + +static const CLSID CLSID_MMDeviceEnumerator_ = __uuidof(MMDeviceEnumerator); +static const IID IID_IMMDeviceEnumerator_ = __uuidof(IMMDeviceEnumerator); +static const IID IID_IAudioClient_ = __uuidof(IAudioClient); +static const IID IID_IAudioCaptureClient_ = __uuidof(IAudioCaptureClient); + +WasapiCapture::WasapiCapture() {} + +WasapiCapture::~WasapiCapture() { + stop(); + if (mixFormat_) CoTaskMemFree(mixFormat_); + if (captureClient_) captureClient_->Release(); + if (audioClient_) audioClient_->Release(); + if (device_) device_->Release(); + if (enumerator_) enumerator_->Release(); +} + +static std::wstring utf8ToWide(const std::string& str) { + if (str.empty()) return L""; + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0); + std::wstring wstr(len, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), &wstr[0], len); + return wstr; +} + +IMMDevice* WasapiCapture::findCaptureDeviceByName(const std::wstring& targetName) { + IMMDeviceCollection* collection = nullptr; + HRESULT hr = enumerator_->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &collection); + if (FAILED(hr)) return nullptr; + + UINT count = 0; + collection->GetCount(&count); + + for (UINT i = 0; i < count; i++) { + IMMDevice* dev = nullptr; + collection->Item(i, &dev); + + IPropertyStore* store = nullptr; + dev->OpenPropertyStore(STGM_READ, &store); + PROPVARIANT pv; + PropVariantInit(&pv); + store->GetValue(PKEY_Device_FriendlyName, &pv); + std::wstring name = pv.pwszVal ? pv.pwszVal : L""; + PropVariantClear(&pv); + store->Release(); + + if (name.find(targetName) != std::wstring::npos || targetName.find(name) != std::wstring::npos) { + collection->Release(); + return dev; + } + dev->Release(); + } + + collection->Release(); + return nullptr; +} + +bool WasapiCapture::initializeLoopback(const std::string& outputPath) { + outputPath_ = outputPath; + streamFlags_ = AUDCLNT_STREAMFLAGS_LOOPBACK; + + HRESULT hr = CoCreateInstance( + CLSID_MMDeviceEnumerator_, nullptr, CLSCTX_ALL, + IID_IMMDeviceEnumerator_, reinterpret_cast(&enumerator_)); + if (FAILED(hr)) return false; + + hr = enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device_); + if (FAILED(hr)) return false; + + return initializeCommon(); +} + +bool WasapiCapture::initializeMic(const std::string& outputPath, const std::string& deviceName) { + outputPath_ = outputPath; + streamFlags_ = 0; + + HRESULT hr = CoCreateInstance( + CLSID_MMDeviceEnumerator_, nullptr, CLSCTX_ALL, + IID_IMMDeviceEnumerator_, reinterpret_cast(&enumerator_)); + if (FAILED(hr)) return false; + + if (!deviceName.empty()) { + device_ = findCaptureDeviceByName(utf8ToWide(deviceName)); + } + if (!device_) { + hr = enumerator_->GetDefaultAudioEndpoint(eCapture, eCommunications, &device_); + if (FAILED(hr)) { + hr = enumerator_->GetDefaultAudioEndpoint(eCapture, eConsole, &device_); + if (FAILED(hr)) return false; + } + } + + return initializeCommon(); +} + +bool WasapiCapture::initializeCommon() { + HRESULT hr = device_->Activate(IID_IAudioClient_, CLSCTX_ALL, nullptr, + reinterpret_cast(&audioClient_)); + if (FAILED(hr)) return false; + + hr = audioClient_->GetMixFormat(&mixFormat_); + if (FAILED(hr)) return false; + + REFERENCE_TIME bufferDuration = 200000; // 20ms + hr = audioClient_->Initialize( + AUDCLNT_SHAREMODE_SHARED, + streamFlags_, + bufferDuration, 0, mixFormat_, nullptr); + if (FAILED(hr)) return false; + + hr = audioClient_->GetBufferSize(&bufferFrameCount_); + if (FAILED(hr)) return false; + + hr = audioClient_->GetService(IID_IAudioCaptureClient_, + reinterpret_cast(&captureClient_)); + if (FAILED(hr)) return false; + + return true; +} + +bool WasapiCapture::start() { + if (capturing_) return true; + + outputFile_ = CreateFileA( + outputPath_.c_str(), GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (outputFile_ == INVALID_HANDLE_VALUE) { + std::cerr << "ERROR: Cannot create audio output file" << std::endl; + return false; + } + + totalDataBytes_ = 0; + writeWavHeader(outputFile_, 0); + + HRESULT hr = audioClient_->Start(); + if (FAILED(hr)) { + CloseHandle(outputFile_); + outputFile_ = INVALID_HANDLE_VALUE; + return false; + } + + capturing_ = true; + paused_ = false; + thread_ = std::thread(&WasapiCapture::captureThread, this); + return true; +} + +bool WasapiCapture::pause() { + if (!capturing_ || paused_) return true; + paused_ = true; + return audioClient_ ? SUCCEEDED(audioClient_->Stop()) : false; +} + +bool WasapiCapture::resume() { + if (!capturing_ || !paused_) return true; + HRESULT hr = audioClient_ ? audioClient_->Start() : E_FAIL; + if (FAILED(hr)) return false; + paused_ = false; + return true; +} + +void WasapiCapture::stop() { + if (!capturing_) return; + capturing_ = false; + if (thread_.joinable()) thread_.join(); + if (audioClient_) audioClient_->Stop(); + + if (outputFile_ != INVALID_HANDLE_VALUE) { + SetFilePointer(outputFile_, 0, nullptr, FILE_BEGIN); + writeWavHeader(outputFile_, totalDataBytes_); + CloseHandle(outputFile_); + outputFile_ = INVALID_HANDLE_VALUE; + } + + paused_ = false; +} + +static int16_t floatToInt16(float v) { + v = v < -1.0f ? -1.0f : (v > 1.0f ? 1.0f : v); + return static_cast(v * 32767.0f); +} + +bool WasapiCapture::writeWavHeader(HANDLE file, DWORD dataSize) { + WORD channels = static_cast(mixFormat_->nChannels); + DWORD sampleRate = mixFormat_->nSamplesPerSec; + WORD bitsPerSample = 16; + WORD blockAlign = channels * (bitsPerSample / 8); + DWORD byteRate = sampleRate * blockAlign; + + DWORD written; + WriteFile(file, "RIFF", 4, &written, nullptr); + DWORD chunkSize = 36 + dataSize; + WriteFile(file, &chunkSize, 4, &written, nullptr); + WriteFile(file, "WAVE", 4, &written, nullptr); + WriteFile(file, "fmt ", 4, &written, nullptr); + DWORD fmtSize = 16; + WriteFile(file, &fmtSize, 4, &written, nullptr); + WORD audioFormat = 1; + WriteFile(file, &audioFormat, 2, &written, nullptr); + WriteFile(file, &channels, 2, &written, nullptr); + WriteFile(file, &sampleRate, 4, &written, nullptr); + WriteFile(file, &byteRate, 4, &written, nullptr); + WriteFile(file, &blockAlign, 2, &written, nullptr); + WriteFile(file, &bitsPerSample, 2, &written, nullptr); + WriteFile(file, "data", 4, &written, nullptr); + WriteFile(file, &dataSize, 4, &written, nullptr); + return true; +} + +void WasapiCapture::captureThread() { + WORD channels = static_cast(mixFormat_->nChannels); + bool isFloat = (mixFormat_->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) || + (mixFormat_->wFormatTag == WAVE_FORMAT_EXTENSIBLE && + reinterpret_cast(mixFormat_)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT); + + std::vector pcmBuffer; + + DWORD sleepMs = static_cast((static_cast(bufferFrameCount_) / mixFormat_->nSamplesPerSec) * 500.0); + if (sleepMs < 5) sleepMs = 5; + + while (capturing_) { + if (paused_) { + Sleep(10); + continue; + } + + Sleep(sleepMs); + + UINT32 packetLength = 0; + HRESULT hr = captureClient_->GetNextPacketSize(&packetLength); + if (FAILED(hr)) break; + + while (packetLength > 0) { + BYTE* data = nullptr; + UINT32 numFrames = 0; + DWORD flags = 0; + + hr = captureClient_->GetBuffer(&data, &numFrames, &flags, nullptr, nullptr); + if (FAILED(hr)) break; + + UINT32 totalSamples = numFrames * channels; + + if (flags & AUDCLNT_BUFFERFLAGS_SILENT) { + pcmBuffer.assign(totalSamples, 0); + } else if (isFloat) { + pcmBuffer.resize(totalSamples); + const float* src = reinterpret_cast(data); + for (UINT32 i = 0; i < totalSamples; i++) { + pcmBuffer[i] = floatToInt16(src[i]); + } + } else { + pcmBuffer.resize(totalSamples); + std::memcpy(pcmBuffer.data(), data, totalSamples * sizeof(int16_t)); + } + + captureClient_->ReleaseBuffer(numFrames); + + DWORD bytesToWrite = totalSamples * sizeof(int16_t); + DWORD written; + WriteFile(outputFile_, pcmBuffer.data(), bytesToWrite, &written, nullptr); + totalDataBytes_ += written; + + hr = captureClient_->GetNextPacketSize(&packetLength); + if (FAILED(hr)) break; + } + } +} diff --git a/electron/native/windows-capture/src/wasapi_loopback.h b/electron/native/windows-capture/src/wasapi_loopback.h new file mode 100644 index 000000000..a4facf139 --- /dev/null +++ b/electron/native/windows-capture/src/wasapi_loopback.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class WasapiCapture { +public: + WasapiCapture(); + ~WasapiCapture(); + + bool initializeLoopback(const std::string& outputPath); + bool initializeMic(const std::string& outputPath, const std::string& deviceName = ""); + bool start(); + bool pause(); + bool resume(); + void stop(); + +private: + bool initializeCommon(); + void captureThread(); + bool writeWavHeader(HANDLE file, DWORD dataSize); + IMMDevice* findCaptureDeviceByName(const std::wstring& name); + + std::string outputPath_; + std::thread thread_; + std::atomic capturing_{false}; + std::atomic paused_{false}; + HANDLE outputFile_ = INVALID_HANDLE_VALUE; + DWORD totalDataBytes_ = 0; + + IMMDeviceEnumerator* enumerator_ = nullptr; + IMMDevice* device_ = nullptr; + IAudioClient* audioClient_ = nullptr; + IAudioCaptureClient* captureClient_ = nullptr; + WAVEFORMATEX* mixFormat_ = nullptr; + DWORD streamFlags_ = 0; + + UINT32 bufferFrameCount_ = 0; +}; diff --git a/electron/navigationPolicy.test.ts b/electron/navigationPolicy.test.ts new file mode 100644 index 000000000..5c23c7375 --- /dev/null +++ b/electron/navigationPolicy.test.ts @@ -0,0 +1,269 @@ +import { describe, expect, it, vi } from "vitest"; +import { + createWillNavigateHandler, + createWillRedirectHandler, + createWindowOpenHandler, + hardenWebContentsNavigation, + isInternalRendererTarget, + normalizeExternalHttpUrl, + shouldHardenWebContentsType, +} from "./navigationPolicy"; + +describe("normalizeExternalHttpUrl", () => { + it.each([ + ["https://example.com/docs", "https://example.com/docs"], + ["http://127.0.0.1:3000/path?q=1", "http://127.0.0.1:3000/path?q=1"], + ["HTTPS://Example.COM:443/docs", "https://example.com/docs"], + ])("normalizes an external HTTP(S) URL: %s", (value, expected) => { + expect(normalizeExternalHttpUrl(value)).toBe(expected); + }); + + it.each([ + "", + "not a URL", + "file:///tmp/recordly.html", + "data:text/html,hello", + "javascript:alert(1)", + "mailto:security@example.com", + "https://user:password@example.com/", + ])("rejects an unsafe external URL: %s", (value) => { + expect(normalizeExternalHttpUrl(value)).toBeNull(); + }); +}); + +describe("shouldHardenWebContentsType", () => { + it("selects BrowserWindow contents only", () => { + expect(shouldHardenWebContentsType("window")).toBe(true); + expect(shouldHardenWebContentsType("webview")).toBe(false); + expect(shouldHardenWebContentsType("offscreen")).toBe(false); + }); +}); + +describe("isInternalRendererTarget", () => { + it.each([ + ["http://localhost:5173/?windowType=editor", "http://localhost:5173/editor?reload=1"], + ["http://127.0.0.1:43123/?windowType=editor", "http://127.0.0.1:43123/assets/index.js"], + [ + "file:///opt/Recordly/dist/index.html?windowType=editor", + "file:///opt/Recordly/dist/index.html?windowType=hud-overlay#status", + ], + ])("identifies the current renderer origin/file", (currentUrl, targetUrl) => { + expect(isInternalRendererTarget(currentUrl, targetUrl)).toBe(true); + }); + + it.each([ + ["http://localhost:5173/?windowType=editor", "http://localhost.example.com:5173/"], + ["http://localhost:5173/", "http://localhost:5174/"], + ["https://recordly.example/", "http://recordly.example/"], + ["https://recordly.example/", "https://user:pass@recordly.example/"], + ["file:///opt/Recordly/dist/index.html", "file:///etc/passwd"], + ["file:///opt/Recordly/dist/index.html", "data:text/html,hello"], + ["not a URL", "https://example.com/"], + ])("distinguishes a target outside the current renderer", (currentUrl, targetUrl) => { + expect(isInternalRendererTarget(currentUrl, targetUrl)).toBe(false); + }); +}); + +describe("navigation event handlers", () => { + it("preserves an exact renderer reload", () => { + const preventDefault = vi.fn(); + const openExternal = vi.fn(async () => undefined); + const handler = createWillNavigateHandler( + () => "http://localhost:5173/editor?windowType=editor", + openExternal, + ); + + handler({ + url: "http://localhost:5173/editor?windowType=editor", + preventDefault, + }); + + expect(preventDefault).not.toHaveBeenCalled(); + expect(openExternal).not.toHaveBeenCalled(); + }); + + it("stops same-origin renderer navigation without externalizing it", () => { + const preventDefault = vi.fn(); + const openExternal = vi.fn(async () => undefined); + const handler = createWillNavigateHandler( + () => "http://localhost:5173/editor", + openExternal, + ); + + handler({ url: "http://localhost:5173/settings", preventDefault }); + + expect(preventDefault).toHaveBeenCalledOnce(); + expect(openExternal).not.toHaveBeenCalled(); + }); + + it("stops same-file query mutation without externalizing it", () => { + const preventDefault = vi.fn(); + const openExternal = vi.fn(async () => undefined); + const handler = createWillNavigateHandler( + () => "file:///opt/Recordly/dist/index.html?windowType=editor", + openExternal, + ); + + handler({ + url: "file:///opt/Recordly/dist/index.html?smokeExport=1", + preventDefault, + }); + + expect(preventDefault).toHaveBeenCalledOnce(); + expect(openExternal).not.toHaveBeenCalled(); + }); + + it("stops cross-origin navigation and opens HTTP(S) in the system browser", () => { + const preventDefault = vi.fn(); + const openExternal = vi.fn(async () => undefined); + const handler = createWillNavigateHandler( + () => "http://localhost:5173/editor", + openExternal, + ); + + handler({ url: "https://example.com/docs", preventDefault }); + + expect(preventDefault).toHaveBeenCalledOnce(); + expect(openExternal).toHaveBeenCalledWith("https://example.com/docs"); + }); + + it("stops unsafe schemes without opening them externally", () => { + const preventDefault = vi.fn(); + const openExternal = vi.fn(async () => undefined); + const handler = createWillNavigateHandler( + () => "file:///opt/Recordly/dist/index.html", + openExternal, + ); + + handler({ url: "file:///etc/passwd", preventDefault }); + + expect(preventDefault).toHaveBeenCalledOnce(); + expect(openExternal).not.toHaveBeenCalled(); + }); + + it("stops server redirects without externalizing the target", () => { + const preventDefault = vi.fn(); + createWillRedirectHandler()({ url: "https://example.com/redirect", preventDefault }); + expect(preventDefault).toHaveBeenCalledOnce(); + }); + + it("always denies Electron child windows while preserving safe external links", () => { + const openExternal = vi.fn(async () => undefined); + const handler = createWindowOpenHandler(() => "http://localhost:5173/editor", openExternal); + + expect(handler({ url: "https://example.com/docs" })).toEqual({ action: "deny" }); + expect(handler({ url: "http://localhost:5173/settings" })).toEqual({ action: "deny" }); + expect(handler({ url: "javascript:alert(1)" })).toEqual({ action: "deny" }); + expect(openExternal).toHaveBeenCalledOnce(); + expect(openExternal).toHaveBeenCalledWith("https://example.com/docs"); + }); + + it("reports a system-browser failure without allowing the child window", async () => { + const error = new Error("browser unavailable"); + const reportOpenError = vi.fn(); + const handler = createWindowOpenHandler( + () => "http://localhost:5173/editor", + vi.fn(async () => { + throw error; + }), + reportOpenError, + ); + + expect(handler({ url: "https://example.com/docs" })).toEqual({ action: "deny" }); + await vi.waitFor(() => { + expect(reportOpenError).toHaveBeenCalledWith("https://example.com/docs", error); + }); + }); + + it("reports a synchronous browser-launch failure while still denying the child", () => { + const error = new Error("browser launch threw"); + const reportOpenError = vi.fn(); + const handler = createWindowOpenHandler( + () => "http://localhost:5173/editor", + vi.fn(() => { + throw error; + }), + reportOpenError, + ); + + expect(handler({ url: "https://example.com/docs" })).toEqual({ action: "deny" }); + expect(reportOpenError).toHaveBeenCalledWith("https://example.com/docs", error); + }); + + it("attaches navigation, redirect, and window-open policies", () => { + const on = vi.fn(); + const setWindowOpenHandler = vi.fn(); + const webContents = { + getURL: () => "http://localhost:5173/", + on, + setWindowOpenHandler, + }; + + hardenWebContentsNavigation( + webContents, + vi.fn(async () => undefined), + ); + + expect(on).toHaveBeenCalledWith("will-navigate", expect.any(Function)); + expect(on).toHaveBeenCalledWith("will-redirect", expect.any(Function)); + expect(on).toHaveBeenCalledWith("did-navigate", expect.any(Function)); + expect(setWindowOpenHandler).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("does not trust a renderer-mutated URL as an exact reload", () => { + let currentUrl = "file:///opt/Recordly/dist/index.html?windowType=editor"; + const on = vi.fn(); + const webContents = { + getURL: () => currentUrl, + on, + setWindowOpenHandler: vi.fn(), + }; + const openExternal = vi.fn(async () => undefined); + + hardenWebContentsNavigation(webContents, openExternal); + + // history.replaceState() changes getURL() without crossing a document-navigation boundary. + currentUrl = "file:///opt/Recordly/dist/index.html?windowType=source-selector"; + const willNavigate = on.mock.calls.find( + ([eventName]) => eventName === "will-navigate", + )?.[1]; + if (typeof willNavigate !== "function") { + throw new Error("will-navigate handler was not registered"); + } + + const preventDefault = vi.fn(); + willNavigate({ url: currentUrl, preventDefault }); + + expect(preventDefault).toHaveBeenCalledOnce(); + expect(openExternal).not.toHaveBeenCalled(); + }); + + it("trusts an exact reload after a completed document navigation", () => { + const on = vi.fn(); + const webContents = { + getURL: () => "", + on, + setWindowOpenHandler: vi.fn(), + }; + + hardenWebContentsNavigation( + webContents, + vi.fn(async () => undefined), + ); + + const didNavigate = on.mock.calls.find(([eventName]) => eventName === "did-navigate")?.[1]; + const willNavigate = on.mock.calls.find( + ([eventName]) => eventName === "will-navigate", + )?.[1]; + if (typeof didNavigate !== "function" || typeof willNavigate !== "function") { + throw new Error("navigation handlers were not registered"); + } + + const loadedUrl = "file:///opt/Recordly/dist/index.html?windowType=editor"; + didNavigate({}, loadedUrl); + const preventDefault = vi.fn(); + willNavigate({ url: loadedUrl, preventDefault }); + + expect(preventDefault).not.toHaveBeenCalled(); + }); +}); diff --git a/electron/navigationPolicy.ts b/electron/navigationPolicy.ts new file mode 100644 index 000000000..1bc9df4ee --- /dev/null +++ b/electron/navigationPolicy.ts @@ -0,0 +1,151 @@ +import type { WebContents } from "electron"; + +type OpenExternal = (url: string) => Promise; +type ReportOpenError = (url: string, error: unknown) => void; + +export type NavigationEvent = { + url: string; + preventDefault: () => void; +}; + +export function shouldHardenWebContentsType(type: ReturnType): boolean { + return type === "window"; +} + +export function normalizeExternalHttpUrl(value: string): string | null { + try { + const url = new URL(value); + if ( + (url.protocol !== "http:" && url.protocol !== "https:") || + !url.hostname || + url.username || + url.password + ) { + return null; + } + + return url.href; + } catch { + return null; + } +} + +export function isInternalRendererTarget(currentValue: string, targetValue: string): boolean { + try { + const currentUrl = new URL(currentValue); + const targetUrl = new URL(targetValue); + + if (targetUrl.username || targetUrl.password) { + return false; + } + + if ( + (currentUrl.protocol === "http:" || currentUrl.protocol === "https:") && + (targetUrl.protocol === "http:" || targetUrl.protocol === "https:") + ) { + return currentUrl.origin === targetUrl.origin; + } + + if (currentUrl.protocol === "file:" && targetUrl.protocol === "file:") { + return currentUrl.host === targetUrl.host && currentUrl.pathname === targetUrl.pathname; + } + + return currentUrl.href === targetUrl.href; + } catch { + return false; + } +} + +function isExactRendererLocation(currentValue: string, targetValue: string): boolean { + try { + const currentUrl = new URL(currentValue); + const targetUrl = new URL(targetValue); + return !targetUrl.username && !targetUrl.password && currentUrl.href === targetUrl.href; + } catch { + return false; + } +} + +function openExternalIfSafe( + value: string, + openExternal: OpenExternal, + reportOpenError: ReportOpenError, +): void { + const safeUrl = normalizeExternalHttpUrl(value); + if (!safeUrl) { + return; + } + + try { + void openExternal(safeUrl).catch((error) => reportOpenError(safeUrl, error)); + } catch (error) { + reportOpenError(safeUrl, error); + } +} + +const defaultReportOpenError: ReportOpenError = (url, error) => { + console.error("[navigation-policy] Failed to open external URL", { url, error }); +}; + +export function createWillNavigateHandler( + getTrustedRendererUrl: () => string, + openExternal: OpenExternal, + reportOpenError: ReportOpenError = defaultReportOpenError, +) { + return (event: NavigationEvent): void => { + const trustedRendererUrl = getTrustedRendererUrl(); + // Preserve an exact reload, but freeze all renderer-selected destination changes, + // including same-origin query mutations that can carry privileged local paths. + if (isExactRendererLocation(trustedRendererUrl, event.url)) { + return; + } + + // The internal-target check only prevents app URLs from leaking into the system browser. + event.preventDefault(); + if (isInternalRendererTarget(trustedRendererUrl, event.url)) { + return; + } + + openExternalIfSafe(event.url, openExternal, reportOpenError); + }; +} + +export function createWillRedirectHandler() { + return (event: NavigationEvent): void => { + event.preventDefault(); + }; +} + +export function createWindowOpenHandler( + getCurrentUrl: () => string, + openExternal: OpenExternal, + reportOpenError: ReportOpenError = defaultReportOpenError, +) { + return (details: { url: string }) => { + if (!isInternalRendererTarget(getCurrentUrl(), details.url)) { + openExternalIfSafe(details.url, openExternal, reportOpenError); + } + return { action: "deny" as const }; + }; +} + +export function hardenWebContentsNavigation( + webContents: Pick, + openExternal: OpenExternal, + reportOpenError: ReportOpenError = defaultReportOpenError, +): void { + // Renderer history APIs mutate getURL() without a document navigation. Keep the last + // main-frame document URL as the reload trust boundary instead of trusting that live value. + let trustedRendererUrl = webContents.getURL(); + webContents.on("did-navigate", (_event, url) => { + trustedRendererUrl = url; + }); + webContents.on( + "will-navigate", + createWillNavigateHandler(() => trustedRendererUrl, openExternal, reportOpenError), + ); + webContents.on("will-redirect", createWillRedirectHandler()); + webContents.setWindowOpenHandler( + createWindowOpenHandler(() => webContents.getURL(), openExternal, reportOpenError), + ); +} diff --git a/electron/permissionPolicy.test.ts b/electron/permissionPolicy.test.ts new file mode 100644 index 000000000..36b5dce63 --- /dev/null +++ b/electron/permissionPolicy.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, it } from "vitest"; + +import { + isTrustedCaptureDocumentUrl, + shouldGrantDisplayCapture, + shouldGrantMediaPermission, +} from "./permissionPolicy"; + +const TRUSTED_DOCUMENT_BASE_URLS = [ + "http://localhost:5173/", + "http://127.0.0.1:43127/", + "file:///C:/Program%20Files/Recordly/resources/app.asar/dist/index.html", +]; + +const DEV_HUD_URL = "http://localhost:5173/?windowType=hud-overlay"; +const PACKAGED_HUD_URL = "http://127.0.0.1:43127/?windowType=hud-overlay"; +const FILE_HUD_URL = + "file:///C:/Program%20Files/Recordly/resources/app.asar/dist/index.html?windowType=hud-overlay"; + +describe("isTrustedCaptureDocumentUrl", () => { + it.each([ + DEV_HUD_URL, + PACKAGED_HUD_URL, + FILE_HUD_URL, + `${DEV_HUD_URL}#microphone`, + ])("accepts a Recordly HUD document: %s", (candidateUrl) => { + expect(isTrustedCaptureDocumentUrl(candidateUrl, TRUSTED_DOCUMENT_BASE_URLS)).toBe(true); + }); + + it.each([ + "http://localhost:5173/", + "http://localhost:5173/?windowType=editor", + "http://localhost:5173/?windowType=HUD-OVERLAY", + "http://localhost:5173/?windowType=hud-overlay&debug=1", + "http://localhost:5173/?windowType=hud-overlay&windowType=hud-overlay", + "http://localhost:5174/?windowType=hud-overlay", + "http://localhost.evil.test:5173/?windowType=hud-overlay", + "http://localhost:5173.evil.test/?windowType=hud-overlay", + "http://user@localhost:5173/?windowType=hud-overlay", + "https://localhost:5173/?windowType=hud-overlay", + "http://127.0.0.1:43127/nested/?windowType=hud-overlay", + "file:///C:/Program%20Files/Recordly/resources/app.asar/dist/other.html?windowType=hud-overlay", + "file:///C:/Program%20Files/Recordly/resources/app.asar/dist/index.html/child?windowType=hud-overlay", + "data:text/html,recordly?windowType=hud-overlay", + "not a url", + ])("rejects a non-Recordly capture document: %s", (candidateUrl) => { + expect(isTrustedCaptureDocumentUrl(candidateUrl, TRUSTED_DOCUMENT_BASE_URLS)).toBe(false); + }); + + it("ignores malformed trusted base URLs instead of throwing", () => { + expect( + isTrustedCaptureDocumentUrl(DEV_HUD_URL, ["not a url", ...TRUSTED_DOCUMENT_BASE_URLS]), + ).toBe(true); + }); +}); + +describe("shouldGrantMediaPermission", () => { + const makeRequest = ( + overrides: Partial[0]> = {}, + ): Parameters[0] => ({ + permission: "media", + isTrustedCaptureWindow: true, + isMainFrame: true, + currentDocumentUrl: DEV_HUD_URL, + requestingUrl: DEV_HUD_URL, + securityOrigins: ["http://localhost:5173"], + ...overrides, + }); + + it("grants camera or microphone media to the trusted HUD main frame", () => { + expect(shouldGrantMediaPermission(makeRequest(), TRUSTED_DOCUMENT_BASE_URLS)).toBe(true); + }); + + it("accepts Chromium's trailing-slash HTTP origin serialization", () => { + expect( + shouldGrantMediaPermission( + makeRequest({ securityOrigins: ["http://localhost:5173/"] }), + TRUSTED_DOCUMENT_BASE_URLS, + ), + ).toBe(true); + }); + + it("accepts the packaged loopback renderer with its exact origin", () => { + expect( + shouldGrantMediaPermission( + makeRequest({ + currentDocumentUrl: PACKAGED_HUD_URL, + requestingUrl: PACKAGED_HUD_URL, + securityOrigins: ["http://127.0.0.1:43127"], + }), + TRUSTED_DOCUMENT_BASE_URLS, + ), + ).toBe(true); + }); + + it.each([ + "null", + "file://", + "file:///", + ])("accepts Chromium's packaged file origin form: %s", (securityOrigin) => { + expect( + shouldGrantMediaPermission( + makeRequest({ + currentDocumentUrl: FILE_HUD_URL, + requestingUrl: FILE_HUD_URL, + securityOrigins: [securityOrigin], + }), + TRUSTED_DOCUMENT_BASE_URLS, + ), + ).toBe(true); + }); + + it.each([ + ["another permission", { permission: "display-capture" }], + ["another BrowserWindow", { isTrustedCaptureWindow: false }], + ["a subframe", { isMainFrame: false }], + ["an untrusted current document", { currentDocumentUrl: "https://example.com/" }], + ["a missing requesting document", { requestingUrl: "" }], + ["an untrusted requesting document", { requestingUrl: "https://example.com/" }], + ["a different trusted document", { requestingUrl: PACKAGED_HUD_URL }], + ["a mismatched origin", { securityOrigins: ["http://localhost:5174"] }], + ["an origin lookalike", { securityOrigins: ["http://localhost:5173.evil.test"] }], + ["an origin with credentials", { securityOrigins: ["http://user@localhost:5173/"] }], + ["an origin with a path", { securityOrigins: ["http://localhost:5173/other"] }], + ["an origin with a query", { securityOrigins: ["http://localhost:5173/?debug=1"] }], + ["a missing security origin", { securityOrigins: [] }], + ["an empty origin", { securityOrigins: [""] }], + ] as const)("denies %s", (_label, overrides) => { + expect(shouldGrantMediaPermission(makeRequest(overrides), TRUSTED_DOCUMENT_BASE_URLS)).toBe( + false, + ); + }); +}); + +describe("shouldGrantDisplayCapture", () => { + const makeRequest = ( + overrides: Partial[0]> = {}, + ): Parameters[0] => ({ + isTrustedCaptureWindow: true, + isMainFrame: true, + currentDocumentUrl: DEV_HUD_URL, + securityOrigin: "http://localhost:5173", + videoRequested: true, + ...overrides, + }); + + it("grants selected-display video to the trusted HUD main frame", () => { + expect(shouldGrantDisplayCapture(makeRequest(), TRUSTED_DOCUMENT_BASE_URLS)).toBe(true); + }); + + it("accepts a trailing slash on the display security origin", () => { + expect( + shouldGrantDisplayCapture( + makeRequest({ securityOrigin: "http://localhost:5173/" }), + TRUSTED_DOCUMENT_BASE_URLS, + ), + ).toBe(true); + }); + + it("accepts the packaged loopback renderer with its exact origin", () => { + expect( + shouldGrantDisplayCapture( + makeRequest({ + currentDocumentUrl: PACKAGED_HUD_URL, + securityOrigin: "http://127.0.0.1:43127", + }), + TRUSTED_DOCUMENT_BASE_URLS, + ), + ).toBe(true); + }); + + it.each([ + "null", + "file://", + "file:///", + ])("accepts Chromium's packaged file origin form: %s", (securityOrigin) => { + expect( + shouldGrantDisplayCapture( + makeRequest({ currentDocumentUrl: FILE_HUD_URL, securityOrigin }), + TRUSTED_DOCUMENT_BASE_URLS, + ), + ).toBe(true); + }); + + it.each([ + ["another BrowserWindow", { isTrustedCaptureWindow: false }], + ["a subframe", { isMainFrame: false }], + ["a non-Recordly document", { currentDocumentUrl: "https://example.com/" }], + ["a mismatched origin", { securityOrigin: "http://127.0.0.1:43127" }], + ["a malformed origin", { securityOrigin: "not an origin" }], + ["an origin with a path", { securityOrigin: "http://localhost:5173/other" }], + ["an audio-only request", { videoRequested: false }], + ] as const)("denies %s", (_label, overrides) => { + expect(shouldGrantDisplayCapture(makeRequest(overrides), TRUSTED_DOCUMENT_BASE_URLS)).toBe( + false, + ); + }); +}); diff --git a/electron/permissionPolicy.ts b/electron/permissionPolicy.ts new file mode 100644 index 000000000..0c9ea07c5 --- /dev/null +++ b/electron/permissionPolicy.ts @@ -0,0 +1,157 @@ +const CAPTURE_WINDOW_TYPE = "hud-overlay"; +const TRUSTED_RENDERER_PROTOCOLS = new Set(["http:", "https:", "file:"]); +const FILE_SECURITY_ORIGINS = new Set(["null", "file://", "file:///"]); + +export interface MediaPermissionPolicyRequest { + permission: string; + isTrustedCaptureWindow: boolean; + isMainFrame: boolean; + currentDocumentUrl: string; + requestingUrl: string; + securityOrigins: readonly string[]; +} + +export interface DisplayCapturePolicyRequest { + isTrustedCaptureWindow: boolean; + isMainFrame: boolean; + currentDocumentUrl: string; + securityOrigin: string; + videoRequested: boolean; +} + +function parseUrl(value: string): URL | null { + try { + return new URL(value); + } catch { + return null; + } +} + +function hasExactCaptureWindowQuery(url: URL): boolean { + const entries = [...url.searchParams.entries()]; + return ( + entries.length === 1 && + entries[0]?.[0] === "windowType" && + entries[0][1] === CAPTURE_WINDOW_TYPE + ); +} + +function isValidTrustedBaseUrl(url: URL): boolean { + return ( + TRUSTED_RENDERER_PROTOCOLS.has(url.protocol) && + url.username === "" && + url.password === "" && + url.search === "" && + url.hash === "" + ); +} + +function hasSameBaseLocation(candidate: URL, trustedBase: URL): boolean { + return ( + candidate.protocol === trustedBase.protocol && + candidate.hostname === trustedBase.hostname && + candidate.port === trustedBase.port && + candidate.pathname === trustedBase.pathname + ); +} + +export function isTrustedCaptureDocumentUrl( + candidateUrl: string, + trustedDocumentBaseUrls: readonly string[], +): boolean { + const candidate = parseUrl(candidateUrl); + if ( + !candidate || + !TRUSTED_RENDERER_PROTOCOLS.has(candidate.protocol) || + candidate.username !== "" || + candidate.password !== "" || + !hasExactCaptureWindowQuery(candidate) + ) { + return false; + } + + return trustedDocumentBaseUrls.some((trustedBaseUrl) => { + const trustedBase = parseUrl(trustedBaseUrl); + return Boolean( + trustedBase && + isValidTrustedBaseUrl(trustedBase) && + hasSameBaseLocation(candidate, trustedBase), + ); + }); +} + +function isSameDocument(firstUrl: string, secondUrl: string): boolean { + const first = parseUrl(firstUrl); + const second = parseUrl(secondUrl); + if (!first || !second) { + return false; + } + + first.hash = ""; + second.hash = ""; + return first.href === second.href; +} + +function isSecurityOriginForDocument(securityOrigin: string, documentUrl: string): boolean { + const document = parseUrl(documentUrl); + if (!document) { + return false; + } + + if (document.protocol === "file:") { + return FILE_SECURITY_ORIGINS.has(securityOrigin); + } + + const origin = parseUrl(securityOrigin); + return Boolean( + origin && + (origin.protocol === "http:" || origin.protocol === "https:") && + origin.username === "" && + origin.password === "" && + origin.origin === document.origin && + origin.pathname === "/" && + origin.search === "" && + origin.hash === "", + ); +} + +export function shouldGrantMediaPermission( + request: MediaPermissionPolicyRequest, + trustedDocumentBaseUrls: readonly string[], +): boolean { + if ( + request.permission !== "media" || + !request.isTrustedCaptureWindow || + !request.isMainFrame || + !isTrustedCaptureDocumentUrl(request.currentDocumentUrl, trustedDocumentBaseUrls) + ) { + return false; + } + + if ( + !isTrustedCaptureDocumentUrl(request.requestingUrl, trustedDocumentBaseUrls) || + !isSameDocument(request.currentDocumentUrl, request.requestingUrl) + ) { + return false; + } + + return ( + request.securityOrigins.length > 0 && + request.securityOrigins.every((securityOrigin) => + isSecurityOriginForDocument(securityOrigin, request.currentDocumentUrl), + ) + ); +} + +export function shouldGrantDisplayCapture( + request: DisplayCapturePolicyRequest, + trustedDocumentBaseUrls: readonly string[], +): boolean { + return ( + request.isTrustedCaptureWindow && + request.isMainFrame && + request.videoRequested && + isTrustedCaptureDocumentUrl(request.currentDocumentUrl, trustedDocumentBaseUrls) && + isSecurityOriginForDocument(request.securityOrigin, request.currentDocumentUrl) + ); +} diff --git a/electron/preload.ts b/electron/preload.ts index 2b94f790e..46cef83c8 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,52 +1,1136 @@ -import { contextBridge, ipcRenderer } from 'electron' - -contextBridge.exposeInMainWorld('electronAPI', { - getAssetBasePath: async () => { - // ask main process for the correct base path (production vs dev) - return await ipcRenderer.invoke('get-asset-base-path') - }, - getSources: async (opts: Electron.SourcesOptions) => { - return await ipcRenderer.invoke('get-sources', opts) - }, - switchToEditor: () => { - return ipcRenderer.invoke('switch-to-editor') - }, - openSourceSelector: () => { - return ipcRenderer.invoke('open-source-selector') - }, - selectSource: (source: any) => { - return ipcRenderer.invoke('select-source', source) - }, - getSelectedSource: () => { - return ipcRenderer.invoke('get-selected-source') - }, - startMouseTracking: () => { - return ipcRenderer.invoke('start-mouse-tracking') - }, - stopMouseTracking: () => { - return ipcRenderer.invoke('stop-mouse-tracking') - }, - storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => { - return ipcRenderer.invoke('store-recorded-video', videoData, fileName) - }, - storeMouseTrackingData: (fileName: string) => { - return ipcRenderer.invoke('store-mouse-tracking-data', fileName) - }, - getRecordedVideoPath: () => { - return ipcRenderer.invoke('get-recorded-video-path') - }, - setRecordingState: (recording: boolean) => { - return ipcRenderer.invoke('set-recording-state', recording) - }, - onStopRecordingFromTray: (callback: () => void) => { - const listener = () => callback() - ipcRenderer.on('stop-recording-from-tray', listener) - return () => ipcRenderer.removeListener('stop-recording-from-tray', listener) - }, - openExternalUrl: (url: string) => { - return ipcRenderer.invoke('open-external-url', url) - }, - saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { - return ipcRenderer.invoke('save-exported-video', videoData, fileName) - }, -}) \ No newline at end of file +import { contextBridge, ipcRenderer } from "electron"; +import type { RecordingSessionData } from "./ipc/types"; + +type NativeVideoExportWriteResult = { success: boolean; error?: string }; +type NativeVideoAudioMuxMetrics = { + tempVideoWriteMs?: number; + tempEditedAudioWriteMs?: number; + ffmpegExecMs?: number; + muxedVideoReadMs?: number; + tempVideoBytes?: number; + tempEditedAudioBytes?: number; + muxedVideoBytes?: number; +}; +type WindowsGpuExportSummary = { + success?: boolean; + width?: number; + height?: number; + fps?: number; + seconds?: number; + mediaMs?: number; + frames?: number; + cursorOverlay?: boolean; + zoomOverlay?: boolean; + adapterVendorId?: number; + adapterDeviceId?: number; + adapterDedicatedVideoMemoryMB?: number; + initializeMs?: number; + initCoInitializeMs?: number; + initMfStartupMs?: number; + initD3DDeviceMs?: number; + initSourceReaderMs?: number; + initWebcamReaderMs?: number; + initVideoProcessorMs?: number; + initTexturesMs?: number; + initShaderPipelineMs?: number; + initSinkWriterMs?: number; + totalMs?: number; + readMs?: number; + clearMs?: number; + videoProcessMs?: number; + writeSampleMs?: number; + finalizeMs?: number; + realtimeMultiplier?: number; +}; +type NativeStaticLayoutChunkMetric = { + index: number; + startSec: number; + durationSec: number; + backend: + | "cuda-overlay" + | "cuda-scale-cpu-pad" + | "cuda-static-composite" + | "nvidia-cuda-compositor" + | "windows-d3d11-compositor"; + elapsedMs: number; + outputBytes: number; + fallbackReason?: string; + windowsGpuSummary?: WindowsGpuExportSummary; +}; +type NativeStaticLayoutMetrics = NativeVideoAudioMuxMetrics & { + chunkCount: number; + chunkDurationSec: number; + chunkExecMs: number; + concatExecMs?: number; + staticAssetExecMs?: number; + fallbackChunkCount: number; + videoOnlyBytes?: number; + chunks: NativeStaticLayoutChunkMetric[]; +}; +type NativeStaticLayoutProgress = { + sessionId?: string; + backend?: NativeStaticLayoutChunkMetric["backend"]; + stage?: "preparing" | "finalizing"; + elapsedMs?: number; + averageFps?: number; + currentFrame: number; + totalFrames: number; + percentage: number; +}; +type NativeVideoMetadataProbe = { + width: number; + height: number; + duration: number; + mediaStartTime?: number; + streamStartTime?: number; + streamDuration?: number; + frameRate: number; + codec: string; + hasAudio: boolean; + audioCodec?: string; + audioSampleRate?: number; +}; +type NativeExportCapabilities = { + platform: NodeJS.Platform; + nvidiaCuda: { + available: boolean; + skipReason: string | null; + hasNvidiaGpu: boolean | null; + hasWrapper: boolean; + explicitEnabled: boolean; + explicitDisabled: boolean; + userOptInRequired: boolean; + }; +}; +type ExportHardwareInfo = { + platform: NodeJS.Platform; + release: string; + arch: string; + cpuModel: string | null; + logicalProcessors: number; + totalMemoryGb: number; + machineModel: string | null; + gpus: Array<{ + name: string; + vendor: string | null; + active: boolean | null; + }>; + gpuFeatures: { + videoDecode: string | null; + videoEncode: string | null; + webgl: string | null; + webgpu: string | null; + }; +}; + +const nativeVideoExportWriteRequests = new Map< + number, + { + sessionId: string; + resolve: (result: NativeVideoExportWriteResult) => void; + } +>(); + +let nextNativeVideoExportWriteRequestId = 1; +let nativeVideoExportWriteResultListenerAttached = false; + +function ensureNativeVideoExportWriteResultListener() { + if (nativeVideoExportWriteResultListenerAttached) { + return; + } + + nativeVideoExportWriteResultListenerAttached = true; + ipcRenderer.on( + "native-video-export-write-frame-result", + ( + _event, + payload: { + sessionId?: string; + requestId?: number; + success?: boolean; + error?: string; + }, + ) => { + if (typeof payload?.requestId !== "number") { + return; + } + + const pendingRequest = nativeVideoExportWriteRequests.get(payload.requestId); + if (!pendingRequest) { + return; + } + + nativeVideoExportWriteRequests.delete(payload.requestId); + pendingRequest.resolve({ + success: payload.success === true, + error: payload.error, + }); + }, + ); +} + +function settleNativeVideoExportPendingRequests( + sessionId: string, + result: NativeVideoExportWriteResult, +) { + for (const [requestId, pendingRequest] of nativeVideoExportWriteRequests.entries()) { + if (pendingRequest.sessionId !== sessionId) { + continue; + } + + nativeVideoExportWriteRequests.delete(requestId); + pendingRequest.resolve(result); + } +} + +contextBridge.exposeInMainWorld("electronAPI", { + hudOverlaySetIgnoreMouse: (ignore: boolean) => { + ipcRenderer.send("hud-overlay-set-ignore-mouse", ignore); + }, + hudOverlaySetSourceSelectionActive: (active: boolean) => { + ipcRenderer.send("hud-overlay-set-source-selection-active", active); + }, + hudOverlayDrag: (phase: "start" | "move" | "end", screenX: number, screenY: number) => { + ipcRenderer.send("hud-overlay-drag", phase, screenX, screenY); + }, + hudOverlayHide: () => { + ipcRenderer.send("hud-overlay-hide"); + }, + hudOverlayClose: () => { + ipcRenderer.send("hud-overlay-close"); + }, + getEditorMode: () => ipcRenderer.invoke("get-editor-mode"), + onEditorModeChanged: (callback: (inEditor: boolean) => void) => { + const listener = (_event: Electron.IpcRendererEvent, inEditor: boolean) => + callback(inEditor); + ipcRenderer.on("editor-mode-changed", listener); + return () => ipcRenderer.removeListener("editor-mode-changed", listener); + }, + hudOverlayRendererReady: () => { + ipcRenderer.send("hud-overlay-renderer-ready"); + }, + hudOverlaySetWebcamPreviewVisible: (visible: boolean) => { + ipcRenderer.send("hud-overlay-set-webcam-preview-visible", visible); + }, + getHudOverlayCaptureProtection: () => { + return ipcRenderer.invoke("get-hud-overlay-capture-protection"); + }, + getHudOverlayMousePassthroughSupported: () => { + return ipcRenderer.invoke("get-hud-overlay-mouse-passthrough-supported"); + }, + setHudOverlayCaptureProtection: (enabled: boolean) => { + return ipcRenderer.invoke("set-hud-overlay-capture-protection", enabled); + }, + getAssetBasePath: async () => { + return await ipcRenderer.invoke("get-asset-base-path"); + }, + listAssetDirectory: (relativeDir: string) => { + return ipcRenderer.invoke("list-asset-directory", relativeDir); + }, + readLocalFile: (filePath: string) => { + return ipcRenderer.invoke("read-local-file", filePath); + }, + generateWallpaperThumbnail: (filePath: string) => { + return ipcRenderer.invoke("generate-wallpaper-thumbnail", filePath); + }, + probeNativeVideoMetadata: (filePath: string) => { + return ipcRenderer.invoke("probe-native-video-metadata", filePath) as Promise<{ + success: boolean; + metadata?: NativeVideoMetadataProbe; + error?: string; + }>; + }, + getNativeExportCapabilities: () => { + return ipcRenderer.invoke("get-native-export-capabilities") as Promise<{ + success: boolean; + capabilities?: NativeExportCapabilities; + error?: string; + }>; + }, + getExportHardwareInfo: () => { + return ipcRenderer.invoke("get-export-hardware-info") as Promise<{ + success: boolean; + hardware?: ExportHardwareInfo; + error?: string; + }>; + }, + nativeStaticLayoutExport: (options: { + sessionId?: string; + inputPath: string; + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: "fast" | "balanced" | "quality"; + durationSec: number; + contentWidth: number; + contentHeight: number; + offsetX: number; + offsetY: number; + sourceCropX?: number; + sourceCropY?: number; + sourceCropWidth?: number; + sourceCropHeight?: number; + backgroundColor: string; + backgroundImagePath?: string | null; + backgroundBlurPx?: number; + borderRadius?: number; + shadowIntensity?: number; + webcamInputPath?: string | null; + webcamLeft?: number; + webcamTop?: number; + webcamSize?: number; + webcamRadius?: number; + webcamShadowIntensity?: number; + webcamMirror?: boolean; + webcamTimeOffsetMs?: number; + cursorTelemetry?: Array<{ + timeMs: number; + cx: number; + cy: number; + cursorTypeIndex?: number; + bounceScale?: number; + visible?: boolean; + }>; + cursorSize?: number; + cursorAtlasPngDataUrl?: string | null; + cursorAtlasEntries?: Array<{ + index: number; + x: number; + y: number; + width: number; + height: number; + anchorX: number; + anchorY: number; + aspectRatio: number; + }>; + zoomTelemetry?: Array<{ timeMs: number; scale: number; x: number; y: number }>; + timelineSegments?: Array<{ + sourceStartMs: number; + sourceEndMs: number; + outputStartMs: number; + outputEndMs: number; + speed: number; + }>; + chunkDurationSec?: number; + experimentalWindowsGpuCompositor?: boolean; + experimentalNvidiaCudaExport?: boolean; + audioOptions?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }; + }) => { + return ipcRenderer.invoke("native-static-layout-export", options) as Promise<{ + success: boolean; + tempPath?: string; + encoderName?: string; + error?: string; + metrics?: NativeStaticLayoutMetrics; + }>; + }, + nativeStaticLayoutExportCancel: (sessionId: string) => { + return ipcRenderer.invoke("native-static-layout-export-cancel", sessionId) as Promise<{ + success: boolean; + }>; + }, + onNativeStaticLayoutExportProgress: ( + callback: (progress: NativeStaticLayoutProgress) => void, + ) => { + const listener = (_event: Electron.IpcRendererEvent, payload: NativeStaticLayoutProgress) => + callback(payload); + ipcRenderer.on("native-static-layout-export-progress", listener); + return () => ipcRenderer.removeListener("native-static-layout-export-progress", listener); + }, + nativeVideoExportStart: (options: { + width: number; + height: number; + frameRate: number; + bitrate: number; + encodingMode: "fast" | "balanced" | "quality"; + inputMode?: "rawvideo" | "h264-stream"; + }) => { + return ipcRenderer.invoke("native-video-export-start", options); + }, + nativeVideoExportWriteFrame: (sessionId: string, frameData: Uint8Array) => { + ensureNativeVideoExportWriteResultListener(); + + return new Promise((resolve) => { + const requestId = nextNativeVideoExportWriteRequestId++; + nativeVideoExportWriteRequests.set(requestId, { + sessionId, + resolve, + }); + + ipcRenderer.send("native-video-export-write-frame-async", { + sessionId, + requestId, + frameData, + }); + }); + }, + nativeVideoExportWriteFrames: (sessionId: string, frameDataList: Uint8Array[]) => { + ensureNativeVideoExportWriteResultListener(); + + return new Promise((resolve) => { + const requestId = nextNativeVideoExportWriteRequestId++; + nativeVideoExportWriteRequests.set(requestId, { + sessionId, + resolve, + }); + + ipcRenderer.send("native-video-export-write-frames-async", { + sessionId, + requestId, + frameDataList, + }); + }); + }, + nativeVideoExportFinish: ( + sessionId: string, + options?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }, + ) => { + return ipcRenderer + .invoke("native-video-export-finish", sessionId, options) + .then((result) => { + settleNativeVideoExportPendingRequests( + sessionId, + result?.success + ? { success: true } + : { + success: false, + error: + typeof result?.error === "string" + ? result.error + : "Native video export session finished before all frame writes settled.", + }, + ); + + return result; + }) as Promise<{ + success: boolean; + data?: Uint8Array; + encoderName?: string; + error?: string; + metrics?: NativeVideoAudioMuxMetrics; + }>; + }, + nativeVideoExportCancel: (sessionId: string) => { + return ipcRenderer.invoke("native-video-export-cancel", sessionId).finally(() => { + settleNativeVideoExportPendingRequests(sessionId, { + success: false, + error: "Native video export session was cancelled", + }); + }); + }, + muxExportedVideoAudio: ( + videoData: ArrayBuffer, + options?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }, + ) => { + return ipcRenderer.invoke("mux-exported-video-audio", videoData, options) as Promise<{ + success: boolean; + tempPath?: string; + error?: string; + metrics?: NativeVideoAudioMuxMetrics; + }>; + }, + muxExportedVideoAudioFromPath: ( + videoPath: string, + options?: { + audioMode?: "none" | "copy-source" | "trim-source" | "edited-track"; + audioSourcePath?: string | null; + audioSourceCodec?: string | null; + audioSourceSampleRate?: number; + outputDurationSec?: number; + trimSegments?: Array<{ startMs: number; endMs: number }>; + editedTrackStrategy?: "filtergraph-fast-path" | "offline-render-fallback"; + editedTrackSegments?: Array<{ startMs: number; endMs: number; speed: number }>; + editedAudioData?: ArrayBuffer; + editedAudioMimeType?: string | null; + }, + ) => { + return ipcRenderer.invoke("mux-exported-video-audio-from-path", videoPath, options); + }, + openExportStream: (options?: { extension?: string }) => { + return ipcRenderer.invoke("export-stream-open", options); + }, + writeExportStreamChunk: (streamId: string, position: number, chunk: Uint8Array) => { + return ipcRenderer.invoke("export-stream-write", streamId, position, chunk); + }, + closeExportStream: (streamId: string, options?: { abort?: boolean }) => { + return ipcRenderer.invoke("export-stream-close", streamId, options); + }, + finalizeExportedVideo: (payload: { + tempPath: string; + fileName: string; + outputPath?: string | null; + captionSidecar?: { + format: "srt" | "vtt" | "both"; + cues: Array<{ + startMs: number; + endMs: number; + text: string; + }>; + }; + }) => { + return ipcRenderer.invoke("finalize-exported-video", payload); + }, + discardExportedTemp: (tempPath: string) => { + return ipcRenderer.invoke("discard-exported-temp", tempPath); + }, + getVideoAudioFallbackPaths: (videoPath: string) => { + return ipcRenderer.invoke("get-video-audio-fallback-paths", videoPath); + }, + getSources: async (opts: Electron.SourcesOptions) => { + return await ipcRenderer.invoke("get-sources", opts); + }, + showRecordingPermissions: () => ipcRenderer.invoke("show-recording-permissions"), + onRecordingPermissionsRequested: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("recording-permissions-requested", listener); + return () => ipcRenderer.removeListener("recording-permissions-requested", listener); + }, + showRecordingHud: () => ipcRenderer.invoke("show-recording-hud"), + createProjectFile: (data: unknown, thumbnail?: string | null) => + ipcRenderer.invoke("create-project-file", data, thumbnail), + renameLibraryProject: (path: string, name: string) => + ipcRenderer.invoke("rename-library-project", path, name), + trashProjectFiles: (paths: string[]) => ipcRenderer.invoke("trash-project-files", paths), + showProjectDashboard: () => ipcRenderer.invoke("show-project-dashboard"), + switchToEditor: () => { + return ipcRenderer.invoke("switch-to-editor"); + }, + openSourceSelector: () => { + return ipcRenderer.invoke("open-source-selector"); + }, + selectSource: (source: ProcessedDesktopSource) => { + return ipcRenderer.invoke("select-source", source); + }, + showSourceHighlight: (source: ProcessedDesktopSource) => { + return ipcRenderer.invoke("show-source-highlight", source); + }, + pickCaptureTarget: () => { + return ipcRenderer.invoke("pick-capture-target"); + }, + completeCapturePick: (pick: CapturePick | null) => { + return ipcRenderer.invoke("complete-capture-pick", pick); + }, + getCapturePickerContext: (displayId: number) => { + return ipcRenderer.invoke("get-capture-picker-context", displayId); + }, + capturePickerReady: () => { + ipcRenderer.send("capture-picker-ready"); + }, + getSelectedSource: () => { + return ipcRenderer.invoke("get-selected-source"); + }, + onSelectedSourceChanged: (callback: (source: ProcessedDesktopSource | null) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: ProcessedDesktopSource | null, + ) => callback(payload); + ipcRenderer.on("selected-source-changed", listener); + return () => ipcRenderer.removeListener("selected-source-changed", listener); + }, + startNativeScreenRecording: ( + source: ProcessedDesktopSource, + options?: { + capturesSystemAudio?: boolean; + capturesMicrophone?: boolean; + microphoneDeviceId?: string; + microphoneLabel?: string; + }, + ) => { + return ipcRenderer.invoke("start-native-screen-recording", source, options); + }, + stopNativeScreenRecording: () => { + return ipcRenderer.invoke("stop-native-screen-recording"); + }, + recoverNativeScreenRecording: () => { + return ipcRenderer.invoke("recover-native-screen-recording"); + }, + getLastNativeCaptureDiagnostics: () => { + return ipcRenderer.invoke("get-last-native-capture-diagnostics"); + }, + pauseNativeScreenRecording: () => { + return ipcRenderer.invoke("pause-native-screen-recording"); + }, + resumeNativeScreenRecording: () => { + return ipcRenderer.invoke("resume-native-screen-recording"); + }, + pauseCursorCapture: (pausedAtMs?: number) => { + return ipcRenderer.invoke("pause-cursor-capture", pausedAtMs); + }, + resumeCursorCapture: (resumedAtMs?: number) => { + return ipcRenderer.invoke("resume-cursor-capture", resumedAtMs); + }, + startFfmpegRecording: (source: ProcessedDesktopSource) => { + return ipcRenderer.invoke("start-ffmpeg-recording", source); + }, + stopFfmpegRecording: () => { + return ipcRenderer.invoke("stop-ffmpeg-recording"); + }, + storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => { + return ipcRenderer.invoke("store-recorded-video", videoData, fileName); + }, + storeMicrophoneSidecar: ( + audioData: ArrayBuffer, + videoPath: string, + options?: { + startDelayMs?: number; + browserMicrophoneProfile?: string; + requestedBrowserMicrophoneProfile?: string | null; + requestedConstraints?: unknown; + mediaTrackSettings?: Record; + audioInputDevices?: Array<{ + deviceId: string; + groupId?: string; + label: string; + }>; + mediaRecorder?: { + mimeType?: string; + audioBitsPerSecond?: number; + timesliceMs?: number; + }; + chunkEvents?: Array<{ + index: number; + size: number; + elapsedMs: number; + deltaMs: number | null; + }>; + }, + ) => { + return ipcRenderer.invoke("store-microphone-sidecar", audioData, videoPath, options); + }, + getRecordedVideoPath: () => { + return ipcRenderer.invoke("get-recorded-video-path"); + }, + setRecordingState: (recording: boolean) => { + return ipcRenderer.invoke("set-recording-state", recording); + }, + setCursorScale: (scale: number) => { + return ipcRenderer.invoke("set-cursor-scale", scale); + }, + getCursorTelemetry: (videoPath?: string) => { + return ipcRenderer.invoke("get-cursor-telemetry", videoPath); + }, + setCursorTelemetry: (videoPath: string | undefined, samples: CursorTelemetryPoint[]) => { + return ipcRenderer.invoke("set-cursor-telemetry", videoPath, samples); + }, + getSystemCursorAssets: () => { + return ipcRenderer.invoke("get-system-cursor-assets"); + }, + onStopRecordingFromTray: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("stop-recording-from-tray", listener); + return () => ipcRenderer.removeListener("stop-recording-from-tray", listener); + }, + onRecordingStateChanged: ( + callback: (state: { recording: boolean; sourceName: string }) => void, + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: { recording: boolean; sourceName: string }, + ) => callback(payload); + ipcRenderer.on("recording-state-changed", listener); + return () => ipcRenderer.removeListener("recording-state-changed", listener); + }, + onRecordingInterrupted: (callback: (state: { reason: string; message: string }) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: { reason: string; message: string }, + ) => callback(payload); + ipcRenderer.on("recording-interrupted", listener); + return () => ipcRenderer.removeListener("recording-interrupted", listener); + }, + onCursorStateChanged: ( + callback: (state: { cursorType: CursorTelemetryPoint["cursorType"] }) => void, + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: { cursorType: CursorTelemetryPoint["cursorType"] }, + ) => callback(payload); + ipcRenderer.on("cursor-state-changed", listener); + return () => ipcRenderer.removeListener("cursor-state-changed", listener); + }, + openExternalUrl: (url: string) => { + return ipcRenderer.invoke("open-external-url", url); + }, + getAccessibilityPermissionStatus: () => { + return ipcRenderer.invoke("get-accessibility-permission-status"); + }, + requestAccessibilityPermission: () => { + return ipcRenderer.invoke("request-accessibility-permission"); + }, + getScreenRecordingPermissionStatus: () => { + return ipcRenderer.invoke("get-screen-recording-permission-status"); + }, + openScreenRecordingPreferences: () => { + return ipcRenderer.invoke("open-screen-recording-preferences"); + }, + openAccessibilityPreferences: () => { + return ipcRenderer.invoke("open-accessibility-preferences"); + }, + saveExportedVideo: ( + videoData: ArrayBuffer, + fileName: string, + captionSidecar?: { + format: "srt" | "vtt" | "both"; + cues: Array<{ + startMs: number; + endMs: number; + text: string; + }>; + }, + ) => { + return ipcRenderer.invoke("save-exported-video", videoData, fileName, captionSidecar); + }, + writeExportedVideoToPath: ( + videoData: ArrayBuffer, + outputPath: string, + captionSidecar?: { + format: "srt" | "vtt" | "both"; + cues: Array<{ + startMs: number; + endMs: number; + text: string; + }>; + }, + ) => { + return ipcRenderer.invoke( + "write-exported-video-to-path", + videoData, + outputPath, + captionSidecar, + ); + }, + openVideoFilePicker: (options?: { includeProjects?: boolean }) => { + return ipcRenderer.invoke("open-video-file-picker", options); + }, + openAudioFilePicker: () => { + return ipcRenderer.invoke("open-audio-file-picker"); + }, + openWhisperExecutablePicker: () => { + return ipcRenderer.invoke("open-whisper-executable-picker"); + }, + openWhisperModelPicker: () => { + return ipcRenderer.invoke("open-whisper-model-picker"); + }, + getWhisperSmallModelStatus: () => { + return ipcRenderer.invoke("get-whisper-small-model-status"); + }, + downloadWhisperSmallModel: () => { + return ipcRenderer.invoke("download-whisper-small-model"); + }, + deleteWhisperSmallModel: () => { + return ipcRenderer.invoke("delete-whisper-small-model"); + }, + onWhisperSmallModelDownloadProgress: ( + callback: (state: { + status: "idle" | "downloading" | "downloaded" | "error"; + progress: number; + path?: string | null; + error?: string; + }) => void, + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: { + status: "idle" | "downloading" | "downloaded" | "error"; + progress: number; + path?: string | null; + error?: string; + }, + ) => callback(payload); + ipcRenderer.on("whisper-small-model-download-progress", listener); + return () => ipcRenderer.removeListener("whisper-small-model-download-progress", listener); + }, + generateAutoCaptions: (options: { + videoPath: string; + whisperExecutablePath?: string; + whisperModelPath: string; + language?: string; + }) => { + return ipcRenderer.invoke("generate-auto-captions", options); + }, + setCurrentVideoPath: ( + path: string, + options?: { + preserveProjectPath?: boolean; + hideOverlayCursorByDefault?: boolean; + }, + ) => { + return ipcRenderer.invoke("set-current-video-path", path, options); + }, + setCurrentRecordingSession: ( + session: { + videoPath: string; + webcamPath?: string | null; + timeOffsetMs?: number; + hideOverlayCursorByDefault?: boolean; + }, + options?: { preserveProjectPath?: boolean }, + ) => { + return ipcRenderer.invoke("set-current-recording-session", session, options); + }, + onRecordingSessionChanged: (callback: (session: RecordingSessionData | null) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: RecordingSessionData | null, + ) => callback(payload); + ipcRenderer.on("recording-session-changed", listener); + return () => ipcRenderer.removeListener("recording-session-changed", listener); + }, + getCurrentRecordingSession: () => { + return ipcRenderer.invoke("get-current-recording-session"); + }, + getCurrentVideoPath: () => { + return ipcRenderer.invoke("get-current-video-path"); + }, + clearCurrentVideoPath: () => { + return ipcRenderer.invoke("clear-current-video-path"); + }, + getRecordingThumbnail: (filePath: string) => + ipcRenderer.invoke("get-recording-thumbnail", filePath), + finishRecordingImport: (keepPath: string, commit?: boolean) => + ipcRenderer.invoke("finish-recording-import", keepPath, commit), + cancelRecordingImport: () => ipcRenderer.invoke("cancel-recording-import"), + getProjectPreview: (projectPath: string) => + ipcRenderer.invoke("get-project-preview", projectPath), + listRecordings: (includeSources?: boolean) => + ipcRenderer.invoke("list-recordings", includeSources), + setRecordingsRemoved: (paths: string[], removed: boolean) => + ipcRenderer.invoke("set-recordings-removed", paths, removed), + importRecording: ( + currentPath: string, + recordingPath: string, + webcam?: import("../src/types/recordingLibrary").RecordingWebcamSource, + ) => ipcRenderer.invoke("import-recording", currentPath, recordingPath, webcam), + deleteRecordingFile: (filePath: string) => { + return ipcRenderer.invoke("delete-recording-file", filePath); + }, + getLocalMediaUrl: (filePath: string) => { + return ipcRenderer.invoke("get-local-media-url", filePath) as Promise< + { success: true; url: string } | { success: false } + >; + }, + saveProjectFile: ( + projectData: unknown, + suggestedName?: string, + existingProjectPath?: string, + thumbnailDataUrl?: string | null, + ) => { + return ipcRenderer.invoke( + "save-project-file", + projectData, + suggestedName, + existingProjectPath, + thumbnailDataUrl, + ); + }, + saveProjectFileNamed: ( + projectData: unknown, + projectName: string, + thumbnailDataUrl?: string | null, + mode?: "rename" | "copy", + ) => { + return ipcRenderer.invoke( + "save-project-file-named", + projectData, + projectName, + thumbnailDataUrl, + mode, + ); + }, + loadProjectFile: () => { + return ipcRenderer.invoke("load-project-file"); + }, + loadCurrentProjectFile: () => { + return ipcRenderer.invoke("load-current-project-file"); + }, + getProjectsDirectory: () => { + return ipcRenderer.invoke("get-projects-directory"); + }, + onProjectThumbnailReady: ( + callback: (ready: { path: string; thumbnailPath: string; updatedAt: number }) => void, + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + ready: { path: string; thumbnailPath: string; updatedAt: number }, + ) => callback(ready); + ipcRenderer.on("project-library-thumbnail-ready", listener); + return () => ipcRenderer.removeListener("project-library-thumbnail-ready", listener); + }, + listProjectFiles: () => { + return ipcRenderer.invoke("list-project-files"); + }, + openProjectFileAtPath: (filePath: string) => { + return ipcRenderer.invoke("open-project-file-at-path", filePath); + }, + openProjectsDirectory: () => { + return ipcRenderer.invoke("open-projects-directory"); + }, + installDownloadedUpdate: () => { + return ipcRenderer.invoke("install-downloaded-update"); + }, + downloadAvailableUpdate: (installAfterDownload?: boolean) => { + return ipcRenderer.invoke("download-available-update", installAfterDownload); + }, + deferDownloadedUpdate: (delayMs?: number) => { + return ipcRenderer.invoke("defer-downloaded-update", delayMs); + }, + dismissUpdateToast: () => { + return ipcRenderer.invoke("dismiss-update-toast"); + }, + skipUpdateVersion: () => { + return ipcRenderer.invoke("skip-update-version"); + }, + getCurrentUpdateToastPayload: () => { + return ipcRenderer.invoke("get-current-update-toast-payload"); + }, + getUpdateStatusSummary: () => { + return ipcRenderer.invoke("get-update-status-summary"); + }, + getExperimentalUpdatesEnabled: () => { + return ipcRenderer.invoke("get-experimental-updates-enabled"); + }, + setExperimentalUpdatesEnabled: (enabled: boolean) => { + return ipcRenderer.invoke("set-experimental-updates-enabled", enabled); + }, + previewUpdateToast: () => { + return ipcRenderer.invoke("preview-update-toast"); + }, + checkForAppUpdates: () => { + return ipcRenderer.invoke("check-for-app-updates"); + }, + onUpdateToastStateChanged: ( + callback: ( + payload: { + version: string; + detail: string; + phase: "available" | "downloading" | "ready" | "error"; + delayMs: number; + isPreview?: boolean; + progressPercent?: number; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; + } | null, + ) => void, + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: { + version: string; + detail: string; + phase: "available" | "downloading" | "ready" | "error"; + delayMs: number; + isPreview?: boolean; + progressPercent?: number; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; + } | null, + ) => callback(payload); + ipcRenderer.on("update-toast-state", listener); + return () => ipcRenderer.removeListener("update-toast-state", listener); + }, + onUpdateReadyToast: ( + callback: (payload: { + version: string; + detail: string; + delayMs: number; + isPreview?: boolean; + }) => void, + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: { version: string; detail: string; delayMs: number; isPreview?: boolean }, + ) => callback(payload); + ipcRenderer.on("update-ready-toast", listener); + return () => ipcRenderer.removeListener("update-ready-toast", listener); + }, + onMenuLoadProject: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-load-project", listener); + return () => ipcRenderer.removeListener("menu-load-project", listener); + }, + onMenuSaveProject: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-save-project", listener); + return () => ipcRenderer.removeListener("menu-save-project", listener); + }, + onMenuSaveProjectAs: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-save-project-as", listener); + return () => ipcRenderer.removeListener("menu-save-project-as", listener); + }, + getWindowChrome: () => ipcRenderer.invoke("get-window-chrome"), + onWindowChromeChanged: (callback: (chrome: { trafficLightsVisible: boolean }) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + chrome: { trafficLightsVisible: boolean }, + ) => callback(chrome); + ipcRenderer.on("window-chrome-changed", listener); + return () => ipcRenderer.removeListener("window-chrome-changed", listener); + }, + getPlatform: () => { + return ipcRenderer.invoke("get-platform"); + }, + isWindowFullscreen: () => { + return ipcRenderer.invoke("get-window-fullscreen"); + }, + onWindowFullscreenChanged: (callback: (isFullscreen: boolean) => void) => { + const listener = (_event: Electron.IpcRendererEvent, isFullscreen: boolean) => + callback(isFullscreen); + ipcRenderer.on("window-fullscreen-changed", listener); + return () => ipcRenderer.removeListener("window-fullscreen-changed", listener); + }, + getLinuxWindowSystem: () => { + return ipcRenderer.invoke("get-linux-window-system"); + }, + ackAuthCallbackUrl: (url: string) => ipcRenderer.invoke("auth:ack-callback", url), + getPendingAuthCallbackUrl: () => ipcRenderer.invoke("auth:get-pending-callback"), + onAuthCallbackUrl: (callback: (url: string) => void) => { + const listener = (_event: Electron.IpcRendererEvent, url: string) => callback(url); + ipcRenderer.on("auth:callback", listener); + return () => ipcRenderer.removeListener("auth:callback", listener); + }, + revealInFolder: (filePath: string) => { + return ipcRenderer.invoke("reveal-in-folder", filePath); + }, + cloudShareUpload: (input: { + filePath: string; + endpoint: string; + token?: string; + title?: string; + notes?: string; + uploadId?: string; + }) => ipcRenderer.invoke("cloud-share-upload", input), + cloudShareManage: (input: { + endpoint: string; + token: string; + action: "list" | "delete"; + shareCode?: string; + }) => ipcRenderer.invoke("cloud-share-manage", input), + cloudShareCancel: (uploadId: string) => ipcRenderer.invoke("cloud-share-cancel", uploadId), + onCloudShareProgress: ( + callback: (progress: { + uploadId: string; + uploadedBytes: number; + totalBytes: number; + }) => void, + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + progress: { uploadId: string; uploadedBytes: number; totalBytes: number }, + ) => callback(progress); + ipcRenderer.on("cloud-share-progress", listener); + return () => ipcRenderer.removeListener("cloud-share-progress", listener); + }, + openRecordingsFolder: () => { + return ipcRenderer.invoke("open-recordings-folder"); + }, + getRecordingsDirectory: () => { + return ipcRenderer.invoke("get-recordings-directory"); + }, + chooseRecordingsDirectory: () => { + return ipcRenderer.invoke("choose-recordings-directory"); + }, + getShortcuts: () => { + return ipcRenderer.invoke("get-shortcuts"); + }, + saveShortcuts: (shortcuts: unknown) => { + return ipcRenderer.invoke("save-shortcuts", shortcuts); + }, + getEditorStartupView: (): "home" | "editor" => ipcRenderer.sendSync("get-editor-startup-view"), + getAppSetting: (key: string) => { + const result = ipcRenderer.sendSync("app-settings:get", key) as { + success?: boolean; + value?: unknown; + }; + return result?.success ? (result.value ?? null) : null; + }, + setAppSetting: (key: string, value: unknown) => { + const result = ipcRenderer.sendSync("app-settings:set", key, value) as { + success?: boolean; + }; + return result?.success === true; + }, + setHasUnsavedChanges: (hasChanges: boolean) => { + ipcRenderer.send("set-has-unsaved-changes", hasChanges); + }, + onRequestSaveBeforeClose: (callback: () => Promise) => { + const listener = async (_event: Electron.IpcRendererEvent, requestId: unknown) => { + let saved = false; + try { + saved = await callback(); + } catch { + saved = false; + } + ipcRenderer.send("save-before-close-done", saved, requestId); + }; + ipcRenderer.on("request-save-before-close", listener); + return () => ipcRenderer.removeListener("request-save-before-close", listener); + }, + isNativeWindowsCaptureAvailable: () => + ipcRenderer.invoke("is-native-windows-capture-available"), + muxNativeWindowsRecording: (expectedDurationMs?: number) => + ipcRenderer.invoke("mux-native-windows-recording", expectedDurationMs), + hideOsCursor: () => ipcRenderer.invoke("hide-cursor"), + getAppVersion: () => ipcRenderer.invoke("app:getVersion"), + getAnnouncements: () => ipcRenderer.invoke("announcements:get"), + getRecordingPreferences: () => ipcRenderer.invoke("get-recording-preferences"), + getRecordingAudioLabConfig: () => ipcRenderer.invoke("get-recording-audio-lab-config"), + setRecordingPreferences: (prefs: { + microphoneEnabled?: boolean; + microphoneDeviceId?: string; + systemAudioEnabled?: boolean; + webcamEnabled?: boolean; + webcamDeviceId?: string; + }) => ipcRenderer.invoke("set-recording-preferences", prefs), + getCountdownDelay: () => ipcRenderer.invoke("get-countdown-delay"), + setCountdownDelay: (delay: number) => ipcRenderer.invoke("set-countdown-delay", delay), + finishRecordingStartup: () => ipcRenderer.invoke("finish-recording-startup"), + startCountdown: (seconds: number) => ipcRenderer.invoke("start-countdown", seconds), + cancelCountdown: () => ipcRenderer.invoke("cancel-countdown"), + getActiveCountdown: () => ipcRenderer.invoke("get-active-countdown"), + onCountdownTick: (callback: (seconds: number) => void) => { + const listener = (_event: Electron.IpcRendererEvent, seconds: number) => callback(seconds); + ipcRenderer.on("countdown-tick", listener); + return () => ipcRenderer.removeListener("countdown-tick", listener); + }, +}); diff --git a/electron/recordingEditorNavigation.test.ts b/electron/recordingEditorNavigation.test.ts new file mode 100644 index 000000000..e5848d8d8 --- /dev/null +++ b/electron/recordingEditorNavigation.test.ts @@ -0,0 +1,50 @@ +import type { BrowserWindow } from "electron"; +import { expect, it, vi } from "vitest"; +import { createRecordingEditorNavigation } from "./recordingEditorNavigation"; + +it("returns a finished HUD recording to its originating editor and consumes the destination", () => { + const create = vi.fn(); + const target = { + isDestroyed: () => false, + isMinimized: () => true, + reload: vi.fn(), + restore: vi.fn(), + show: vi.fn(), + focus: vi.fn(), + }; + const navigation = createRecordingEditorNavigation(create); + navigation.setReturnWindow(target as unknown as BrowserWindow); + navigation.open(); + expect(target.reload).toHaveBeenCalledOnce(); + expect(target.restore).toHaveBeenCalledOnce(); + expect(target.focus).toHaveBeenCalledOnce(); + expect(create).not.toHaveBeenCalled(); + navigation.open(); + expect(create).toHaveBeenCalledOnce(); +}); +it("opens an editor if the originating dashboard has closed", () => { + const create = vi.fn(); + const navigation = createRecordingEditorNavigation(create); + navigation.setReturnWindow({ isDestroyed: () => true } as BrowserWindow); + navigation.open(); + expect(create).toHaveBeenCalledOnce(); +}); + +it("opens Home without reloading and retains the destination for recording completion", () => { + const create = vi.fn(); + const target = { + isDestroyed: () => false, + isMinimized: () => false, + reload: vi.fn(), + show: vi.fn(), + focus: vi.fn(), + }; + const navigation = createRecordingEditorNavigation(create); + navigation.setReturnWindow(target as unknown as BrowserWindow); + navigation.open(false); + expect(target.reload).not.toHaveBeenCalled(); + expect(target.show).toHaveBeenCalledOnce(); + navigation.open(); + expect(target.reload).toHaveBeenCalledOnce(); + expect(create).not.toHaveBeenCalled(); +}); diff --git a/electron/recordingEditorNavigation.ts b/electron/recordingEditorNavigation.ts new file mode 100644 index 000000000..bcf3afc70 --- /dev/null +++ b/electron/recordingEditorNavigation.ts @@ -0,0 +1,24 @@ +import type { BrowserWindow } from "electron"; + +export function createRecordingEditorNavigation(createEditor: () => void) { + let returnWindow: BrowserWindow | null = null; + return { + setReturnWindow(window: BrowserWindow | null) { + returnWindow = window; + }, + open(reload = true) { + const target = returnWindow; + if (reload) returnWindow = null; + if (!target || target.isDestroyed()) { + createEditor(); + return; + } + // Home flushed this editor's project before launching the HUD. Reload + // from the finalized recording session, clearing the previous project UI. + if (reload) target.reload(); + if (target.isMinimized()) target.restore(); + target.show(); + target.focus(); + }, + }; +} diff --git a/electron/rendererServer.ts b/electron/rendererServer.ts new file mode 100644 index 000000000..f3453a06f --- /dev/null +++ b/electron/rendererServer.ts @@ -0,0 +1,146 @@ +import fs from "node:fs/promises"; +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import path from "node:path"; + +const MIME_TYPES: Record = { + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".mjs": "text/javascript; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml", + ".txt": "text/plain; charset=utf-8", + ".wasm": "application/wasm", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; + +let packagedRendererBaseUrl: string | null = null; +let packagedRendererServerStartPromise: Promise | null = null; + +function getContentType(filePath: string): string { + return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream"; +} + +function getCacheControl(filePath: string): string { + // Vite fingerprints production assets, so they can be reused across the HUD, + // picker, toast, and editor windows without revalidation. Keep index.html + // uncached because it points at the current fingerprinted files. + return /-[A-Za-z0-9_-]{8,}\.[^.]+$/.test(path.basename(filePath)) + ? "public, max-age=31536000, immutable" + : "no-cache"; +} + +function resolveRequestedFilePath(rootDir: string, requestPathname: string): string | null { + const trimmedPathname = requestPathname === "/" ? "/index.html" : requestPathname; + + // path.normalize() on Windows converts / to \, making the leading-slash + // regex fail and causing path.resolve to escape to the drive root. + const normalizedPosix = path.posix.normalize(decodeURIComponent(trimmedPathname)); + const relativePath = normalizedPosix.replace(/^\/+/, ""); + + if (!relativePath) { + return null; + } + + const resolvedRootDir = path.resolve(rootDir); + const resolvedFilePath = path.resolve(resolvedRootDir, relativePath); + + if ( + resolvedFilePath !== resolvedRootDir && + !resolvedFilePath.startsWith(`${resolvedRootDir}${path.sep}`) + ) { + return null; + } + + return resolvedFilePath; +} + +async function servePackagedRendererRequest( + rootDir: string, + request: IncomingMessage, + response: ServerResponse, +): Promise { + try { + const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1"); + const resolvedFilePath = resolveRequestedFilePath(rootDir, requestUrl.pathname); + + if (!resolvedFilePath) { + response.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Forbidden"); + return; + } + + const filePath = resolvedFilePath; + const fileContents = await fs.readFile(filePath); + + response.writeHead(200, { + "Cache-Control": getCacheControl(filePath), + "Content-Type": getContentType(filePath), + }); + + if (request.method === "HEAD") { + response.end(); + return; + } + + response.end(fileContents); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not Found"); + return; + } + + response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Internal Server Error"); + } +} + +export function getPackagedRendererBaseUrl(): string | null { + return packagedRendererBaseUrl; +} + +export async function ensurePackagedRendererServer(rootDir: string): Promise { + if (packagedRendererBaseUrl) { + return packagedRendererBaseUrl; + } + + if (packagedRendererServerStartPromise) { + return packagedRendererServerStartPromise; + } + + packagedRendererServerStartPromise = new Promise((resolve, reject) => { + const server = createServer((request, response) => { + void servePackagedRendererRequest(rootDir, request, response); + }); + + server.once("error", (error) => { + reject(error); + }); + + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Renderer server did not expose a TCP address")); + return; + } + + packagedRendererBaseUrl = `http://127.0.0.1:${address.port}`; + resolve(packagedRendererBaseUrl); + }); + }); + + try { + return await packagedRendererServerStartPromise; + } finally { + packagedRendererServerStartPromise = null; + } +} diff --git a/electron/saveBeforeClose.test.ts b/electron/saveBeforeClose.test.ts new file mode 100644 index 000000000..43799b8be --- /dev/null +++ b/electron/saveBeforeClose.test.ts @@ -0,0 +1,71 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { createSaveBeforeCloseController } from "./saveBeforeClose"; +afterEach(() => vi.useRealTimers()); +function fixture(discard = false) { + let reply!: (saved: boolean) => void; + const unsubscribe = vi.fn(); + const options = { + requestSave: vi.fn(), + subscribe: vi.fn((done: (saved: boolean) => void) => { + reply = done; + return unsubscribe; + }), + offerDiscard: vi.fn().mockResolvedValue(discard), + close: vi.fn(), + cancel: vi.fn(), + timeoutMs: 100, + }; + return { + options, + unsubscribe, + reply: (saved: boolean) => reply(saved), + controller: createSaveBeforeCloseController(options), + }; +} +it("closes once after saving and removes the listener", () => { + const f = fixture(); + f.controller.start(); + f.controller.start(); + f.reply(true); + f.reply(true); + expect(f.options.requestSave).toHaveBeenCalledTimes(1); + expect(f.options.close).toHaveBeenCalledTimes(1); + expect(f.unsubscribe).toHaveBeenCalledTimes(1); + expect(f.options.offerDiscard).not.toHaveBeenCalled(); +}); +it.each([true, false])("offers discard/cancel after failed save (discard=%s)", async (discard) => { + const f = fixture(discard); + f.controller.start(); + f.reply(false); + await vi.waitFor(() => + expect(discard ? f.options.close : f.options.cancel).toHaveBeenCalledTimes(1), + ); + expect(f.unsubscribe).toHaveBeenCalledTimes(1); + if (!discard) { + f.controller.start(); + expect(f.options.requestSave).toHaveBeenCalledTimes(2); + } +}); +it("recovers a hung renderer on timeout and ignores late replies", async () => { + vi.useFakeTimers(); + const f = fixture(); + f.controller.start(); + await vi.advanceTimersByTimeAsync(100); + f.reply(true); + expect(f.options.offerDiscard).toHaveBeenCalledTimes(1); + expect(f.options.cancel).toHaveBeenCalledTimes(1); + expect(f.options.close).not.toHaveBeenCalled(); + f.controller.start(); + expect(f.options.requestSave).toHaveBeenCalledTimes(2); +}); +it("removes a pending timeout/listener when the window is destroyed", async () => { + vi.useFakeTimers(); + const f = fixture(); + f.controller.start(); + f.controller.dispose(); + await vi.advanceTimersByTimeAsync(100); + f.reply(true); + expect(f.unsubscribe).toHaveBeenCalledTimes(1); + expect(f.options.close).not.toHaveBeenCalled(); + expect(f.options.offerDiscard).not.toHaveBeenCalled(); +}); diff --git a/electron/saveBeforeClose.ts b/electron/saveBeforeClose.ts new file mode 100644 index 000000000..05f6ea7e7 --- /dev/null +++ b/electron/saveBeforeClose.ts @@ -0,0 +1,65 @@ +type Options = { + requestSave: () => void; + subscribe: (done: (saved: boolean) => void) => () => void; + offerDiscard: () => Promise; + close: () => void; + cancel: () => void; + timeoutMs?: number; +}; + +/** Saves before closing, with a cancellable recovery path for failed or missing replies. */ +export function createSaveBeforeCloseController(options: Options) { + let pending = false; + let recovering = false; + let disposed = false; + let timer: ReturnType | undefined; + let unsubscribe: (() => void) | undefined; + const cleanup = () => { + if (timer) clearTimeout(timer); + timer = undefined; + unsubscribe?.(); + unsubscribe = undefined; + }; + const done = (saved: boolean) => { + if (!pending || recovering || disposed) return; + cleanup(); + if (saved) { + pending = false; + options.close(); + return; + } + recovering = true; + void Promise.resolve() + .then(options.offerDiscard) + .then((discard) => { + if (disposed) return; + if (discard) options.close(); + else options.cancel(); + }) + .catch(() => { + if (!disposed) options.cancel(); + }) + .finally(() => { + pending = false; + recovering = false; + }); + }; + return { + start() { + if (pending || disposed) return; + pending = true; + try { + unsubscribe = options.subscribe(done); + timer = setTimeout(() => done(false), options.timeoutMs ?? 30_000); + options.requestSave(); + } catch { + done(false); + } + }, + dispose() { + disposed = true; + pending = false; + cleanup(); + }, + }; +} diff --git a/electron/sourceListPicker.test.ts b/electron/sourceListPicker.test.ts new file mode 100644 index 000000000..d6dcfddc9 --- /dev/null +++ b/electron/sourceListPicker.test.ts @@ -0,0 +1,35 @@ +import { EventEmitter } from "node:events"; +import type { BrowserWindow, WebContents } from "electron"; +import { describe, expect, it, vi } from "vitest"; +import { createSourceListPicker } from "./sourceListPicker"; + +function fixture() { + const win = Object.assign(new EventEmitter(), { webContents: {} as WebContents }); + const create = vi.fn(() => win as unknown as BrowserWindow); + return { win, create, picker: createSourceListPicker(create) }; +} +describe("source list picker lifecycle", () => { + it("coalesces opens, accepts its own sender, and waits for a selection", async () => { + const { win, create, picker } = fixture(); + const first = picker.open(); + expect(picker.open()).toBe(first); + expect(create).toHaveBeenCalledOnce(); + expect(picker.isSender(win.webContents)).toBe(true); + expect(picker.isSender({} as WebContents)).toBe(false); + const source = { id: "window:101:0", name: "Document" }; + picker.complete(source); + win.emit("closed"); + expect(await first).toEqual({ success: true, source }); + expect(picker.isSender(win.webContents)).toBe(false); + }); + it("resolves dismissal and allows another picker afterwards", async () => { + const { win, create, picker } = fixture(); + const first = picker.open(); + win.emit("closed"); + expect(await first).toEqual({ success: false, canceled: true }); + const second = picker.open(); + expect(create).toHaveBeenCalledTimes(2); + win.emit("closed"); + expect(await second).toEqual({ success: false, canceled: true }); + }); +}); diff --git a/electron/sourceListPicker.ts b/electron/sourceListPicker.ts new file mode 100644 index 000000000..29e74781b --- /dev/null +++ b/electron/sourceListPicker.ts @@ -0,0 +1,38 @@ +import type { BrowserWindow, WebContents } from "electron"; +import type { SelectedSource } from "./ipc/types"; + +export type SourceListPickResult = { + success: boolean; + canceled?: boolean; + source?: SelectedSource; +}; + +/** Keeps the existing Linux source-list window open until selection or dismissal. */ +export function createSourceListPicker(createWindow: () => BrowserWindow) { + let activeWindow: BrowserWindow | null = null; + let pending: Promise | null = null; + let finish: ((result: SourceListPickResult) => void) | null = null; + return { + open() { + if (pending) return pending; + activeWindow = createWindow(); + pending = new Promise((resolve) => { + finish = resolve; + }); + activeWindow.once("closed", () => { + finish?.({ success: false, canceled: true }); + finish = null; + pending = null; + activeWindow = null; + }); + return pending; + }, + isSender(sender: WebContents) { + return activeWindow?.webContents === sender; + }, + complete(source: SelectedSource) { + finish?.({ success: true, source }); + finish = null; + }, + }; +} diff --git a/electron/uiohook-napi.d.ts b/electron/uiohook-napi.d.ts new file mode 100644 index 000000000..a6e65fcdd --- /dev/null +++ b/electron/uiohook-napi.d.ts @@ -0,0 +1 @@ +declare module "uiohook-napi"; diff --git a/electron/updateChannel.test.ts b/electron/updateChannel.test.ts new file mode 100644 index 000000000..646c65c3d --- /dev/null +++ b/electron/updateChannel.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { EXPERIMENTAL_UPDATE_DESCRIPTION, getUpdateChannelConfiguration } from "./updateChannel"; + +describe("getUpdateChannelConfiguration", () => { + it("keeps regular clients on stable metadata", () => { + expect(getUpdateChannelConfiguration(false)).toEqual({ + channel: "latest", + allowPrerelease: false, + allowDowngrade: false, + }); + }); + + it("uses beta metadata only after the client opts in", () => { + expect(getUpdateChannelConfiguration(true)).toEqual({ + channel: "beta", + allowPrerelease: true, + allowDowngrade: false, + }); + }); + + it("uses the approved experimental update description", () => { + expect(EXPERIMENTAL_UPDATE_DESCRIPTION).toBe( + "You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.", + ); + }); +}); diff --git a/electron/updateChannel.ts b/electron/updateChannel.ts new file mode 100644 index 000000000..9ca9ce96e --- /dev/null +++ b/electron/updateChannel.ts @@ -0,0 +1,20 @@ +export const STABLE_UPDATE_CHANNEL = "latest"; +export const EXPERIMENTAL_UPDATE_CHANNEL = "beta"; +export const EXPERIMENTAL_UPDATE_DESCRIPTION = + "You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available."; + +export interface UpdateChannelConfiguration { + channel: typeof STABLE_UPDATE_CHANNEL | typeof EXPERIMENTAL_UPDATE_CHANNEL; + allowPrerelease: boolean; + allowDowngrade: false; +} + +export function getUpdateChannelConfiguration( + experimentalUpdatesEnabled: boolean, +): UpdateChannelConfiguration { + return { + channel: experimentalUpdatesEnabled ? EXPERIMENTAL_UPDATE_CHANNEL : STABLE_UPDATE_CHANNEL, + allowPrerelease: experimentalUpdatesEnabled, + allowDowngrade: false, + }; +} diff --git a/electron/updater.ts b/electron/updater.ts new file mode 100644 index 000000000..b9c4e5dca --- /dev/null +++ b/electron/updater.ts @@ -0,0 +1,930 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { MessageBoxOptions, MessageBoxReturnValue } from "electron"; +import { app, BrowserWindow, dialog } from "electron"; +import { autoUpdater } from "electron-updater"; +import { USER_DATA_PATH } from "./appPaths"; +import { readAppSetting, writeAppSetting } from "./appSettingsStore"; +import { EXPERIMENTAL_UPDATE_DESCRIPTION, getUpdateChannelConfiguration } from "./updateChannel"; + +const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; +const INITIAL_UPDATE_CHECK_DELAY_MS = 15 * 1000; +export const UPDATE_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000; +const DISMISSED_READY_REMINDER_DELAY_MS = 5 * 60 * 1000; +const AUTO_UPDATES_DISABLED = process.env.RECORDLY_DISABLE_AUTO_UPDATES === "1"; +const UPDATE_FEED_URL_OVERRIDE = process.env.RECORDLY_UPDATE_FEED_URL?.trim() ?? ""; +const UPDATER_LOG_PATH = + process.env.RECORDLY_UPDATER_LOG_PATH?.trim() || path.join(USER_DATA_PATH, "updater.log"); +const DEV_UPDATE_PREVIEW_VERSION = "9.9.9"; +const DEV_UPDATE_PREVIEW_IS_EXPERIMENTAL = + process.env.RECORDLY_DEV_PREVIEW_EXPERIMENTAL_UPDATE === "1"; +const DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS = 300; +const DEV_UPDATE_PREVIEW_PROGRESS_INCREMENT = 20; +const ONE_MEGABYTE = 1024 * 1024; +const EXPERIMENTAL_UPDATES_SETTING_KEY = "experimentalUpdatesEnabled"; + +export type UpdateToastPhase = "available" | "downloading" | "ready" | "error"; + +export type UpdateStatusKind = + | "idle" + | "checking" + | "up-to-date" + | "available" + | "downloading" + | "ready" + | "error"; + +export interface UpdateStatusSummary { + status: UpdateStatusKind; + currentVersion: string; + availableVersion: string | null; + detail?: string; +} + +export interface UpdateToastPayload { + version: string; + detail: string; + phase: UpdateToastPhase; + delayMs: number; + isPreview?: boolean; + isExperimental?: boolean; + progressPercent?: number; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; +} + +interface DownloadProgressSnapshot { + progressPercent?: number; + transferredBytes?: number; + totalBytes?: number; + bytesPerSecond?: number; +} + +type UpdateToastSender = ( + channel: "update-toast-state", + payload: UpdateToastPayload | null, +) => boolean; + +let updaterInitialized = false; +let updateCheckInProgress = false; +let manualCheckRequested = false; +let periodicCheckTimer: NodeJS.Timeout | null = null; +let initialCheckTimer: NodeJS.Timeout | null = null; +let deferredReminderTimer: NodeJS.Timeout | null = null; +let devPreviewProgressTimer: NodeJS.Timeout | null = null; +let currentToastPayload: UpdateToastPayload | null = null; +let availableVersion: string | null = null; +let pendingDownloadedVersion: string | null = null; +let downloadInProgress = false; +let downloadToastDismissed = false; +let skippedVersion: string | null = null; +let installAfterDownloadRequested = false; +let updateStatusSummary: UpdateStatusSummary = { + status: "idle", + currentVersion: app.getVersion(), + availableVersion: null, +}; + +function setUpdateStatusSummary(summary: Partial) { + updateStatusSummary = { + ...updateStatusSummary, + currentVersion: app.getVersion(), + ...summary, + }; +} + +function summarizeError(error: unknown) { + if (error instanceof Error) { + return error.stack || `${error.name}: ${error.message}`; + } + + return String(error); +} + +function writeUpdaterLog(message: string, detail?: unknown) { + try { + fs.mkdirSync(path.dirname(UPDATER_LOG_PATH), { recursive: true }); + const suffix = detail === undefined ? "" : ` ${summarizeError(detail)}`; + fs.appendFileSync( + UPDATER_LOG_PATH, + `${new Date().toISOString()} ${message}${suffix}\n`, + "utf8", + ); + } catch (logError) { + console.error("Failed to write updater log:", logError); + } +} + +function configureUpdateFeed() { + if (!UPDATE_FEED_URL_OVERRIDE) { + writeUpdaterLog("Using published GitHub update feed."); + return; + } + + autoUpdater.setFeedURL({ + provider: "generic", + url: UPDATE_FEED_URL_OVERRIDE, + channel: "latest", + }); + writeUpdaterLog(`Using overridden update feed: ${UPDATE_FEED_URL_OVERRIDE}`); +} + +export function getExperimentalUpdatesEnabled() { + return readAppSetting(EXPERIMENTAL_UPDATES_SETTING_KEY) === true; +} + +function applyExperimentalUpdatesPreference() { + const enabled = getExperimentalUpdatesEnabled(); + const { channel, allowPrerelease, allowDowngrade } = getUpdateChannelConfiguration(enabled); + autoUpdater.channel = channel; + autoUpdater.allowPrerelease = allowPrerelease; + // Changing channels enables downgrades inside electron-updater. Recordly never + // needs that behaviour: opting out waits for the next stable version instead. + autoUpdater.allowDowngrade = allowDowngrade; + writeUpdaterLog( + `Update channel configured: ${enabled ? "experimental" : "stable"} (${channel}).`, + ); + return enabled; +} + +export function setExperimentalUpdatesEnabled(enabled: boolean) { + writeAppSetting(EXPERIMENTAL_UPDATES_SETTING_KEY, enabled); + applyExperimentalUpdatesPreference(); + skippedVersion = null; + writeUpdaterLog(`Experimental updates ${enabled ? "enabled" : "disabled"} by user.`); + return enabled; +} + +function canUseAutoUpdates() { + return !AUTO_UPDATES_DISABLED && app.isPackaged && !process.mas; +} + +export function isAutoUpdateFeatureEnabled() { + return !AUTO_UPDATES_DISABLED; +} + +function getDialogWindow(getMainWindow: () => BrowserWindow | null) { + const window = getMainWindow(); + return window && !window.isDestroyed() ? window : undefined; +} + +function showMessageBox( + getMainWindow: () => BrowserWindow | null, + options: MessageBoxOptions, +): Promise { + if (process.platform !== "darwin") { + return dialog.showMessageBox(options); + } + + const window = getDialogWindow(getMainWindow); + return window ? dialog.showMessageBox(window, options) : dialog.showMessageBox(options); +} + +function clearDeferredReminderTimer() { + if (deferredReminderTimer) { + clearTimeout(deferredReminderTimer); + deferredReminderTimer = null; + } +} + +function clearDevPreviewProgressTimer() { + if (devPreviewProgressTimer) { + clearInterval(devPreviewProgressTimer); + devPreviewProgressTimer = null; + } +} + +function emitUpdateToastState( + sendToRenderer: UpdateToastSender | undefined, + payload: UpdateToastPayload | null, +) { + currentToastPayload = payload; + if (!sendToRenderer) { + return false; + } + + return sendToRenderer("update-toast-state", payload); +} + +function getCurrentToastExperimentalFlag() { + return currentToastPayload?.isExperimental ?? getExperimentalUpdatesEnabled(); +} + +function createAvailableUpdateToastPayload( + version: string, + isExperimental = getExperimentalUpdatesEnabled(), +): UpdateToastPayload { + return { + version, + phase: "available", + detail: isExperimental + ? EXPERIMENTAL_UPDATE_DESCRIPTION + : "Install the latest version now, or remind yourself to come back to it later.", + delayMs: UPDATE_REMINDER_DELAY_MS, + isExperimental, + primaryAction: "install-and-restart", + }; +} + +function createDownloadingUpdateToastPayload( + version: string, + progress: DownloadProgressSnapshot = {}, + isExperimental = getCurrentToastExperimentalFlag(), +): UpdateToastPayload { + const normalizedProgress = Math.max( + 0, + Math.min(100, Math.round(progress.progressPercent ?? 0)), + ); + const transferredBytes = + typeof progress.transferredBytes === "number" && Number.isFinite(progress.transferredBytes) + ? Math.max(0, progress.transferredBytes) + : undefined; + const totalBytes = + typeof progress.totalBytes === "number" && Number.isFinite(progress.totalBytes) + ? Math.max(0, progress.totalBytes) + : undefined; + const remainingBytes = + totalBytes !== undefined && transferredBytes !== undefined + ? Math.max(totalBytes - transferredBytes, 0) + : undefined; + const bytesPerSecond = + typeof progress.bytesPerSecond === "number" && Number.isFinite(progress.bytesPerSecond) + ? Math.max(0, progress.bytesPerSecond) + : undefined; + const remainingMb = + remainingBytes !== undefined ? Math.max(0, remainingBytes / ONE_MEGABYTE) : null; + return { + version, + phase: "downloading", + detail: + normalizedProgress >= 100 + ? "Finishing the update download. Recordly will restart as soon as the installer is ready." + : remainingMb !== null + ? `${remainingMb.toFixed(1)} MB left before Recordly restarts.` + : "Downloading the update now. Recordly will restart when it finishes.", + delayMs: UPDATE_REMINDER_DELAY_MS, + isExperimental, + progressPercent: normalizedProgress, + transferredBytes, + totalBytes, + remainingBytes, + bytesPerSecond, + primaryAction: "install-and-restart", + }; +} + +function createDownloadedUpdateToastPayload( + version: string, + isExperimental = getCurrentToastExperimentalFlag(), +): UpdateToastPayload { + return { + version, + phase: "ready", + detail: "The update is ready. Install and restart now, or remind yourself later.", + delayMs: UPDATE_REMINDER_DELAY_MS, + isExperimental, + primaryAction: "install-and-restart", + }; +} + +function createUpdateErrorToastPayload( + version: string, + error: unknown, + isExperimental = getCurrentToastExperimentalFlag(), +): UpdateToastPayload { + return { + version, + phase: "error", + detail: `The update could not be downloaded. ${String(error)}`, + delayMs: UPDATE_REMINDER_DELAY_MS, + isExperimental, + primaryAction: "install-and-restart", + }; +} + +function getReminderPayload(): UpdateToastPayload | null { + if (pendingDownloadedVersion) { + return createDownloadedUpdateToastPayload(pendingDownloadedVersion); + } + + if (availableVersion && !downloadInProgress) { + return createAvailableUpdateToastPayload(availableVersion); + } + + return null; +} + +function clearVisibleUpdateToast(sendToRenderer?: UpdateToastSender) { + emitUpdateToastState(sendToRenderer, null); +} + +export function getCurrentUpdateToastPayload() { + return currentToastPayload; +} + +export function getUpdaterLogPath() { + return UPDATER_LOG_PATH; +} + +export function getUpdateStatusSummary() { + return updateStatusSummary; +} + +function resetDevPreviewState(sendToRenderer?: UpdateToastSender) { + clearDevPreviewProgressTimer(); + availableVersion = null; + pendingDownloadedVersion = null; + downloadInProgress = false; + downloadToastDismissed = false; + skippedVersion = null; + installAfterDownloadRequested = false; + clearVisibleUpdateToast(sendToRenderer); +} + +function simulateDevPreviewDownload(sendToRenderer?: UpdateToastSender) { + availableVersion = DEV_UPDATE_PREVIEW_VERSION; + pendingDownloadedVersion = null; + downloadInProgress = true; + downloadToastDismissed = false; + clearDeferredReminderTimer(); + clearDevPreviewProgressTimer(); + + let progressPercent = 0; + emitUpdateToastState(sendToRenderer, { + ...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, { + progressPercent, + transferredBytes: 0, + totalBytes: 20 * ONE_MEGABYTE, + bytesPerSecond: 5 * ONE_MEGABYTE, + }), + isPreview: true, + }); + + devPreviewProgressTimer = setInterval(() => { + progressPercent = Math.min(100, progressPercent + DEV_UPDATE_PREVIEW_PROGRESS_INCREMENT); + + if (progressPercent >= 100) { + clearDevPreviewProgressTimer(); + downloadInProgress = false; + pendingDownloadedVersion = DEV_UPDATE_PREVIEW_VERSION; + emitUpdateToastState(sendToRenderer, { + ...createDownloadedUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION), + isPreview: true, + detail: "Development preview: the update is ready to install. No real update will be installed.", + }); + return; + } + + if (downloadToastDismissed) { + return; + } + + emitUpdateToastState(sendToRenderer, { + ...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, { + progressPercent, + transferredBytes: (progressPercent / 100) * 20 * ONE_MEGABYTE, + totalBytes: 20 * ONE_MEGABYTE, + bytesPerSecond: 5 * ONE_MEGABYTE, + }), + isPreview: true, + }); + }, DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS); + + return { success: true }; +} + +export function dismissUpdateToast( + getMainWindow: () => BrowserWindow | null, + sendToRenderer?: UpdateToastSender, +) { + if (currentToastPayload?.isPreview) { + resetDevPreviewState(sendToRenderer); + return { success: true }; + } + + if (downloadInProgress) { + installAfterDownloadRequested = false; + downloadToastDismissed = true; + clearVisibleUpdateToast(sendToRenderer); + return { success: true }; + } + + if (currentToastPayload?.phase === "ready") { + return deferUpdateReminder( + getMainWindow, + sendToRenderer, + DISMISSED_READY_REMINDER_DELAY_MS, + ); + } + + if (currentToastPayload?.phase === "available" || currentToastPayload?.phase === "error") { + return deferUpdateReminder(getMainWindow, sendToRenderer, UPDATE_REMINDER_DELAY_MS); + } + + clearVisibleUpdateToast(sendToRenderer); + return { success: true }; +} + +export function installDownloadedUpdateNow(sendToRenderer?: UpdateToastSender) { + if (currentToastPayload?.isPreview) { + resetDevPreviewState(sendToRenderer); + return; + } + + clearDeferredReminderTimer(); + downloadToastDismissed = false; + installAfterDownloadRequested = false; + clearVisibleUpdateToast(sendToRenderer); + setUpdateStatusSummary({ status: "ready", availableVersion: pendingDownloadedVersion }); + writeUpdaterLog("Installing downloaded update."); + autoUpdater.quitAndInstall(); +} + +export async function downloadAvailableUpdate( + sendToRenderer?: UpdateToastSender, + options?: { installAfterDownload?: boolean }, +) { + if (currentToastPayload?.isPreview) { + return simulateDevPreviewDownload(sendToRenderer); + } + + if (!availableVersion) { + return { success: false, message: "No update is ready to download." }; + } + + if (pendingDownloadedVersion === availableVersion) { + return { success: false, message: "This update has already been downloaded." }; + } + + if (downloadInProgress) { + return { success: false, message: "This update is already downloading." }; + } + + clearDeferredReminderTimer(); + downloadInProgress = true; + downloadToastDismissed = false; + installAfterDownloadRequested = + Boolean(options?.installAfterDownload) || installAfterDownloadRequested; + setUpdateStatusSummary({ + status: "downloading", + availableVersion, + detail: `Downloading Recordly ${availableVersion}`, + }); + emitUpdateToastState( + sendToRenderer, + createDownloadingUpdateToastPayload(availableVersion, { + progressPercent: 0, + transferredBytes: 0, + }), + ); + writeUpdaterLog(`Starting update download for ${availableVersion}.`); + + try { + await autoUpdater.downloadUpdate(); + writeUpdaterLog(`Update download requested for ${availableVersion}.`); + return { success: true }; + } catch (error) { + downloadInProgress = false; + setUpdateStatusSummary({ + status: "error", + availableVersion, + detail: String(error), + }); + writeUpdaterLog(`Update download failed for ${availableVersion}.`, error); + emitUpdateToastState( + sendToRenderer, + createUpdateErrorToastPayload(availableVersion, error), + ); + return { success: false, message: String(error) }; + } +} + +export function deferUpdateReminder( + getMainWindow: () => BrowserWindow | null, + sendToRenderer?: UpdateToastSender, + delayMs = UPDATE_REMINDER_DELAY_MS, +) { + const payload = getReminderPayload(); + if (!payload) { + return { success: false, message: "No update reminder is ready yet." }; + } + + clearDeferredReminderTimer(); + installAfterDownloadRequested = false; + clearVisibleUpdateToast(sendToRenderer); + deferredReminderTimer = setTimeout(() => { + const nextPayload = getReminderPayload(); + if (!nextPayload) { + return; + } + + if (sendToRenderer && emitUpdateToastState(sendToRenderer, nextPayload)) { + return; + } + + if (nextPayload.phase === "ready") { + void showDownloadedUpdateDialog(getMainWindow, nextPayload.version); + return; + } + + void showAvailableUpdateDialog(getMainWindow, nextPayload.version, sendToRenderer); + }, delayMs); + + return { success: true }; +} + +export function skipAvailableUpdateVersion(sendToRenderer?: UpdateToastSender) { + const versionToSkip = pendingDownloadedVersion ?? availableVersion; + if (!versionToSkip) { + return { success: false, message: "No update is available to skip." }; + } + + skippedVersion = versionToSkip; + if (pendingDownloadedVersion === versionToSkip) { + pendingDownloadedVersion = null; + } + if (availableVersion === versionToSkip) { + availableVersion = null; + } + downloadInProgress = false; + downloadToastDismissed = false; + installAfterDownloadRequested = false; + clearDeferredReminderTimer(); + clearVisibleUpdateToast(sendToRenderer); + + return { success: true }; +} + +export function previewUpdateToast(sendToRenderer: UpdateToastSender) { + clearDeferredReminderTimer(); + clearDevPreviewProgressTimer(); + availableVersion = DEV_UPDATE_PREVIEW_VERSION; + pendingDownloadedVersion = null; + downloadInProgress = false; + downloadToastDismissed = false; + installAfterDownloadRequested = false; + return emitUpdateToastState(sendToRenderer, { + version: DEV_UPDATE_PREVIEW_VERSION, + phase: "available", + detail: DEV_UPDATE_PREVIEW_IS_EXPERIMENTAL + ? EXPERIMENTAL_UPDATE_DESCRIPTION + : "This is a development preview of the in-app update toast.", + delayMs: UPDATE_REMINDER_DELAY_MS, + isPreview: true, + isExperimental: DEV_UPDATE_PREVIEW_IS_EXPERIMENTAL, + }); +} + +async function showAvailableUpdateDialog( + getMainWindow: () => BrowserWindow | null, + version: string, + sendToRenderer?: UpdateToastSender, + options?: { isPreview?: boolean; isExperimental?: boolean }, +) { + const isPreview = Boolean(options?.isPreview); + const isExperimental = options?.isExperimental ?? getExperimentalUpdatesEnabled(); + const result = await showMessageBox(getMainWindow, { + type: "info", + title: isExperimental ? "Experimental Update Available" : "Update Available", + message: `Recordly ${version} is available${isExperimental ? " on the experimental channel" : ""}.`, + detail: isPreview + ? `${isExperimental ? EXPERIMENTAL_UPDATE_DESCRIPTION : "This is a development preview of the standard update flow."} No real update will be installed.` + : isExperimental + ? EXPERIMENTAL_UPDATE_DESCRIPTION + : "Install and restart now, or remind me later.", + buttons: ["Install & Restart", "Later"], + defaultId: 0, + cancelId: 1, + noLink: true, + }); + + if (result.response === 0) { + if (isPreview) { + await showMessageBox(getMainWindow, { + type: "info", + title: "Preview Only", + message: "No real update was installed.", + detail: "This was only a manual development preview of the update prompt.", + }); + return; + } + + await downloadAvailableUpdate(sendToRenderer, { installAfterDownload: true }); + return; + } + + if (isPreview) { + return; + } + + deferUpdateReminder(getMainWindow, sendToRenderer, UPDATE_REMINDER_DELAY_MS); +} + +async function showDownloadedUpdateDialog( + getMainWindow: () => BrowserWindow | null, + version: string, + options?: { isPreview?: boolean }, +) { + const isPreview = Boolean(options?.isPreview); + const result = await showMessageBox(getMainWindow, { + type: "info", + title: "Update Ready", + message: isPreview + ? `Recordly ${version} is ready to install.` + : `Recordly ${version} has been downloaded.`, + detail: isPreview + ? "Development preview of the native update prompt. No real update will be installed." + : "Install and restart now, or remind me later.", + buttons: ["Install & Restart", "Later"], + defaultId: 0, + cancelId: 1, + noLink: true, + }); + + if (result.response === 0) { + if (isPreview) { + await showMessageBox(getMainWindow, { + type: "info", + title: "Preview Only", + message: "No real update was installed.", + detail: "This was only a manual development preview of the update prompt.", + }); + return; + } + + clearDeferredReminderTimer(); + setImmediate(() => { + installDownloadedUpdateNow(); + }); + return; + } + + if (result.response === 1) { + if (isPreview) { + return; + } + + deferUpdateReminder(getMainWindow, undefined, UPDATE_REMINDER_DELAY_MS); + } +} + +export function previewNativeUpdateDialog(getMainWindow: () => BrowserWindow | null) { + return showAvailableUpdateDialog(getMainWindow, DEV_UPDATE_PREVIEW_VERSION, undefined, { + isPreview: true, + isExperimental: DEV_UPDATE_PREVIEW_IS_EXPERIMENTAL, + }); +} + +async function showUpdateErrorDialog( + getMainWindow: () => BrowserWindow | null, + version: string, + error: unknown, +) { + await showMessageBox(getMainWindow, { + type: "error", + title: "Update Failed", + message: `Recordly ${version} could not be downloaded.`, + detail: String(error), + buttons: ["OK"], + defaultId: 0, + noLink: true, + }); +} + +export async function checkForAppUpdates( + getMainWindow: () => BrowserWindow | null, + options?: { manual?: boolean }, +) { + if (!canUseAutoUpdates()) { + writeUpdaterLog( + `Skipped update check because auto-updates are unavailable. packaged=${app.isPackaged} mas=${process.mas ? "yes" : "no"} disabled=${AUTO_UPDATES_DISABLED ? "yes" : "no"}`, + ); + if (options?.manual) { + await showMessageBox(getMainWindow, { + type: "info", + title: "Updates Not Enabled", + message: "Auto-updates are only available in packaged releases.", + detail: AUTO_UPDATES_DISABLED + ? "This build disabled auto-updates through RECORDLY_DISABLE_AUTO_UPDATES=1." + : "Development builds do not ship the packaged update metadata required by electron-updater.", + }); + } + return; + } + + if (updateCheckInProgress) { + writeUpdaterLog("Skipped update check because a previous check is still running."); + return; + } + + manualCheckRequested = Boolean(options?.manual); + updateCheckInProgress = true; + applyExperimentalUpdatesPreference(); + setUpdateStatusSummary({ status: "checking", detail: "Checking for updates..." }); + writeUpdaterLog(`Starting ${manualCheckRequested ? "manual" : "automatic"} update check.`); + + try { + await autoUpdater.checkForUpdates(); + writeUpdaterLog("Update check request completed."); + } catch (error) { + updateCheckInProgress = false; + manualCheckRequested = false; + setUpdateStatusSummary({ + status: "error", + availableVersion, + detail: String(error), + }); + writeUpdaterLog("Update check failed.", error); + console.error("Auto-update check failed:", error); + } +} + +export function setupAutoUpdates( + getMainWindow: () => BrowserWindow | null, + sendToRenderer: UpdateToastSender, +) { + if (updaterInitialized) { + return; + } + + if (!canUseAutoUpdates()) { + setUpdateStatusSummary({ status: "idle", availableVersion: null, detail: undefined }); + return; + } + + updaterInitialized = true; + configureUpdateFeed(); + applyExperimentalUpdatesPreference(); + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = false; + writeUpdaterLog(`Updater initialized. logPath=${UPDATER_LOG_PATH}`); + + autoUpdater.on("checking-for-update", () => { + setUpdateStatusSummary({ + status: "checking", + availableVersion: null, + detail: "Checking for updates...", + }); + writeUpdaterLog("electron-updater emitted checking-for-update."); + }); + + autoUpdater.on("update-available", (info) => { + writeUpdaterLog(`Update available: version=${info.version}`); + updateCheckInProgress = false; + availableVersion = info.version; + pendingDownloadedVersion = null; + downloadInProgress = false; + downloadToastDismissed = false; + installAfterDownloadRequested = false; + setUpdateStatusSummary({ + status: "available", + availableVersion: info.version, + detail: `Recordly ${info.version} is available.`, + }); + if (skippedVersion === info.version) { + manualCheckRequested = false; + return; + } + + const payload = createAvailableUpdateToastPayload(info.version); + if (emitUpdateToastState(sendToRenderer, payload)) { + manualCheckRequested = false; + return; + } + + void showAvailableUpdateDialog(getMainWindow, info.version, sendToRenderer); + manualCheckRequested = false; + }); + + autoUpdater.on("update-not-available", () => { + writeUpdaterLog("No update available."); + updateCheckInProgress = false; + availableVersion = null; + pendingDownloadedVersion = null; + downloadInProgress = false; + downloadToastDismissed = false; + installAfterDownloadRequested = false; + setUpdateStatusSummary({ + status: "up-to-date", + availableVersion: null, + detail: `Recordly ${app.getVersion()} is up to date.`, + }); + clearVisibleUpdateToast(sendToRenderer); + manualCheckRequested = false; + }); + + autoUpdater.on("download-progress", (progress) => { + if (!availableVersion) { + return; + } + + downloadInProgress = true; + setUpdateStatusSummary({ + status: "downloading", + availableVersion, + detail: `Downloading Recordly ${availableVersion}`, + }); + writeUpdaterLog( + `Download progress for ${availableVersion}: ${progress.percent.toFixed(1)}%`, + ); + if (downloadToastDismissed) { + return; + } + + emitUpdateToastState( + sendToRenderer, + createDownloadingUpdateToastPayload(availableVersion, { + progressPercent: progress.percent, + transferredBytes: progress.transferred, + totalBytes: progress.total, + bytesPerSecond: progress.bytesPerSecond, + }), + ); + }); + + autoUpdater.on("error", (error) => { + updateCheckInProgress = false; + manualCheckRequested = false; + setUpdateStatusSummary({ + status: "error", + availableVersion, + detail: String(error), + }); + writeUpdaterLog("electron-updater emitted error.", error); + console.error("Auto-updater error:", error); + if (downloadInProgress && availableVersion) { + downloadInProgress = false; + downloadToastDismissed = false; + installAfterDownloadRequested = false; + const shownInRenderer = emitUpdateToastState( + sendToRenderer, + createUpdateErrorToastPayload(availableVersion, error), + ); + if (!shownInRenderer) { + void showUpdateErrorDialog(getMainWindow, availableVersion, error); + } + } + }); + + autoUpdater.on("update-downloaded", (info) => { + writeUpdaterLog(`Update downloaded: version=${info.version}`); + updateCheckInProgress = false; + manualCheckRequested = false; + downloadInProgress = false; + downloadToastDismissed = false; + if (skippedVersion === info.version) { + installAfterDownloadRequested = false; + return; + } + availableVersion = info.version; + pendingDownloadedVersion = info.version; + setUpdateStatusSummary({ + status: "ready", + availableVersion: info.version, + detail: `Recordly ${info.version} is ready to install.`, + }); + clearDeferredReminderTimer(); + + if (installAfterDownloadRequested && !currentToastPayload?.isPreview) { + installAfterDownloadRequested = false; + clearVisibleUpdateToast(sendToRenderer); + writeUpdaterLog(`Auto-installing downloaded update: version=${info.version}`); + setImmediate(() => { + installDownloadedUpdateNow(sendToRenderer); + }); + return; + } + + if ( + emitUpdateToastState(sendToRenderer, createDownloadedUpdateToastPayload(info.version)) + ) { + return; + } + + void showDownloadedUpdateDialog(getMainWindow, info.version); + }); + + initialCheckTimer = setTimeout(() => { + initialCheckTimer = null; + void checkForAppUpdates(getMainWindow); + }, INITIAL_UPDATE_CHECK_DELAY_MS); + periodicCheckTimer = setInterval(() => { + void checkForAppUpdates(getMainWindow); + }, UPDATE_CHECK_INTERVAL_MS); + + app.on("before-quit", () => { + clearDeferredReminderTimer(); + clearDevPreviewProgressTimer(); + if (initialCheckTimer) { + clearTimeout(initialCheckTimer); + initialCheckTimer = null; + } + if (periodicCheckTimer) { + clearInterval(periodicCheckTimer); + periodicCheckTimer = null; + } + }); +} diff --git a/electron/windows.ts b/electron/windows.ts index cf6366974..745d6caa4 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -1,118 +1,1348 @@ -import { BrowserWindow, screen } from 'electron' -import path from 'node:path' -import { fileURLToPath } from 'node:url' +import { resolveEditorStartupView } from "./editorStartupView"; +import { currentProjectPath, currentVideoPath, currentRecordingSession } from "./ipc/state"; +import { isHudInEditorMode } from "./hudEditorMode"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { app, BrowserWindow, ipcMain, type WebContents } from "electron"; +import { + supportsHudCaptureProtection, + shouldProtectHudCapture, +} from "../src/lib/hudCaptureProtection"; +import { USER_DATA_PATH } from "./appPaths"; +import { + getHudOverlayWindowBounds, + resizeHudOverlayFallbackBounds, + shouldExpandHudOverlayFallback, +} from "./hudOverlayBounds"; +import { getHudOverlayTaskbarOptions } from "./hudOverlayWindowOptions"; +import { getPackagedRendererBaseUrl } from "./rendererServer"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url)); +const nodeRequire = createRequire(import.meta.url); -const APP_ROOT = path.join(__dirname, '..') -const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] -const RENDERER_DIST = path.join(APP_ROOT, 'dist') +const APP_ROOT = path.join(electronWindowsDir, ".."); +const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; +const RENDERER_DIST = path.join(APP_ROOT, "dist"); +const WINDOW_ICON_FILENAME = + process.platform === "darwin" ? "recordlymac-512.png" : "recordly-512.png"; +const WINDOW_ICON_PATH = path.join( + process.env.VITE_PUBLIC || RENDERER_DIST, + "app-icons", + WINDOW_ICON_FILENAME, +); + +let hudOverlayWindow: BrowserWindow | null = null; +let hudOverlayHiddenFromCapture = true; +let hudOverlayCaptureProtectionLoaded = false; +let hudOverlayFallbackExpanded = false; +let hudOverlayIgnoringMouse = true; +let hudOverlaySourceSelectionActive = false; +let hudOverlayMouseReassertTimer: NodeJS.Timeout | null = null; +let hudOverlayRecordingActive = false; +let hudCaptureStarting = false; +let hudOverlayWebcamPreviewVisible = false; +let countdownWindow: BrowserWindow | null = null; +let capturePickerWindows: BrowserWindow[] = []; +let areaRecordingBorderWindow: BrowserWindow | null = null; +let updateToastWindow: BrowserWindow | null = null; +let hudWasVisibleBeforeUpdateToast = false; + +const HUD_OVERLAY_SETTINGS_FILE = path.join(USER_DATA_PATH, "hud-overlay-settings.json"); +const HUD_EDGE_MARGIN_DIP = 16; +const UPDATE_TOAST_WIDTH = 420; +const UPDATE_TOAST_HEIGHT = 172; +/** Shows a capture picker that never reported ready. */ +const CAPTURE_PICKER_READY_TIMEOUT_MS = 1500; + +// Synchronous and metadata-only so the HTML boot shell matches its destination, +// including reloads of a Home window after a fresh recording finishes. +ipcMain.on("get-editor-startup-view", (event) => { + event.returnValue = resolveEditorStartupView({ + projectPath: currentProjectPath, + videoPath: currentVideoPath, + recordingVideoPath: currentRecordingSession?.videoPath, + devInput: process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT, + smokeExport: process.env.RECORDLY_SMOKE_EXPORT === "1", + }); +}); + +function getEditorWindowQuery(): Record { + const query: Record = { + windowType: "editor", + }; + + if (process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT) { + query.devOpenInput = process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT; + } + if (process.env.RECORDLY_DEV_OPEN_RECORDING_WEBCAM) { + query.devOpenWebcam = process.env.RECORDLY_DEV_OPEN_RECORDING_WEBCAM; + } + + if (process.env.RECORDLY_SMOKE_EXPORT === "1") { + query.smokeExport = "1"; + if (process.env.RECORDLY_SMOKE_EXPORT_INPUT) { + query.smokeInput = process.env.RECORDLY_SMOKE_EXPORT_INPUT; + } + if (process.env.RECORDLY_SMOKE_EXPORT_OUTPUT) { + query.smokeOutput = process.env.RECORDLY_SMOKE_EXPORT_OUTPUT; + } + if (process.env.RECORDLY_SMOKE_EXPORT_USE_NATIVE === "1") { + query.smokeUseNativeExport = "1"; + } + if (process.env.RECORDLY_SMOKE_EXPORT_ENCODING_MODE) { + query.smokeEncodingMode = process.env.RECORDLY_SMOKE_EXPORT_ENCODING_MODE; + } + if (process.env.RECORDLY_SMOKE_EXPORT_SHADOW_INTENSITY) { + query.smokeShadowIntensity = process.env.RECORDLY_SMOKE_EXPORT_SHADOW_INTENSITY; + } + if (process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT) { + query.smokeWebcamInput = process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT; + } + if (process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SHADOW) { + query.smokeWebcamShadow = process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SHADOW; + } + if (process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SIZE) { + query.smokeWebcamSize = process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SIZE; + } + if (process.env.RECORDLY_SMOKE_EXPORT_PIPELINE) { + query.smokePipelineModel = process.env.RECORDLY_SMOKE_EXPORT_PIPELINE; + } + if (process.env.RECORDLY_SMOKE_EXPORT_BACKEND) { + query.smokeBackendPreference = process.env.RECORDLY_SMOKE_EXPORT_BACKEND; + } + if (process.env.RECORDLY_SMOKE_EXPORT_RENDER_BACKEND) { + query.smokeRenderBackend = process.env.RECORDLY_SMOKE_EXPORT_RENDER_BACKEND; + } + if (process.env.RECORDLY_SMOKE_EXPORT_MAX_ENCODE_QUEUE) { + query.smokeMaxEncodeQueue = process.env.RECORDLY_SMOKE_EXPORT_MAX_ENCODE_QUEUE; + } + if (process.env.RECORDLY_SMOKE_EXPORT_MAX_DECODE_QUEUE) { + query.smokeMaxDecodeQueue = process.env.RECORDLY_SMOKE_EXPORT_MAX_DECODE_QUEUE; + } + if (process.env.RECORDLY_SMOKE_EXPORT_MAX_PENDING_FRAMES) { + query.smokeMaxPendingFrames = process.env.RECORDLY_SMOKE_EXPORT_MAX_PENDING_FRAMES; + } + if (process.env.RECORDLY_SMOKE_EXPORT_PROJECT) { + query.smokeProject = process.env.RECORDLY_SMOKE_EXPORT_PROJECT; + } + if (process.env.RECORDLY_SMOKE_EXPORT_QUALITY) { + query.smokeQuality = process.env.RECORDLY_SMOKE_EXPORT_QUALITY; + } + if (process.env.RECORDLY_SMOKE_EXPORT_FPS) { + query.smokeFps = process.env.RECORDLY_SMOKE_EXPORT_FPS; + } + } + + return query; +} + +export function isHudOverlayMousePassthroughSupported(): boolean { + return process.platform !== "linux"; +} + +function loadHudOverlayCaptureProtectionSetting(): boolean { + if (hudOverlayCaptureProtectionLoaded) { + return hudOverlayHiddenFromCapture; + } + + hudOverlayCaptureProtectionLoaded = true; + + try { + if (!fs.existsSync(HUD_OVERLAY_SETTINGS_FILE)) { + return hudOverlayHiddenFromCapture; + } + + const raw = fs.readFileSync(HUD_OVERLAY_SETTINGS_FILE, "utf-8"); + const parsed = JSON.parse(raw) as { hiddenFromCapture?: unknown }; + if (typeof parsed.hiddenFromCapture === "boolean") { + hudOverlayHiddenFromCapture = parsed.hiddenFromCapture; + } + } catch { + // Ignore settings read failures and fall back to defaults. + } + + return hudOverlayHiddenFromCapture; +} + +export function getHudOverlayCaptureProtectionEnabled(): boolean { + return loadHudOverlayCaptureProtectionSetting(); +} + +function applyHudOverlayCaptureProtectionToWindow(hud: BrowserWindow, enabled: boolean): void { + if (!supportsHudCaptureProtection(process.platform)) { + return; + } + + try { + // Keep the idle HUD visible to screenshots and other capture applications. + hud.setContentProtection( + shouldProtectHudCapture(enabled, hudOverlayRecordingActive, hudCaptureStarting), + ); + } catch (error) { + console.warn("Failed to apply HUD capture protection:", error); + } +} + +export function beginHudCaptureProtection(): void { + hudCaptureStarting = true; + reassertHudOverlayCaptureProtection(); +} +ipcMain.handle("finish-recording-startup", () => { + hudCaptureStarting = false; + reassertHudOverlayCaptureProtection(); +}); + +export function reassertHudOverlayCaptureProtection(): boolean { + const enabled = loadHudOverlayCaptureProtectionSetting(); + const hud = getHudOverlayWindow(); + if (!hud) { + return enabled; + } + + applyHudOverlayCaptureProtectionToWindow(hud, enabled); + + return enabled; +} + +function persistHudOverlayCaptureProtectionSetting(enabled: boolean): void { + try { + fs.writeFileSync( + HUD_OVERLAY_SETTINGS_FILE, + JSON.stringify({ hiddenFromCapture: enabled }, null, 2), + "utf-8", + ); + } catch { + // Ignore settings write failures and keep runtime state working. + } +} + +function getScreen() { + if (!app.isReady()) { + throw new Error( + "getScreen() called before app is ready. Ensure all screen access happens after app.whenReady().", + ); + } + return nodeRequire("electron").screen as typeof import("electron").screen; +} + +function getHudOverlayDisplay() { + const hudWindow = getHudOverlayWindow(); + if (hudWindow) { + return getScreen().getDisplayMatching(hudWindow.getBounds()); + } + return getScreen().getPrimaryDisplay(); +} + +function getHudOverlayBounds() { + const { workArea } = getHudOverlayDisplay(); + const fallbackExpanded = shouldExpandHudOverlayFallback({ + fallbackExpanded: hudOverlayFallbackExpanded, + recordingActive: hudOverlayRecordingActive, + webcamPreviewVisible: hudOverlayWebcamPreviewVisible, + }); + return getHudOverlayWindowBounds( + workArea, + isHudOverlayMousePassthroughSupported(), + fallbackExpanded, + ); +} + +function applyHudOverlayBounds() { + if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) { + return; + } + hudOverlayWindow.setBounds(getHudOverlayBounds(), false); + + positionUpdateToastWindow(); + if (!hudOverlayWindow.isVisible()) { + return; + } + hudOverlayWindow.moveTop(); +} + +function getUpdateToastBounds() { + const hudWindow = getHudOverlayWindow(); + if (hudWindow) { + const hudBounds = hudWindow.getBounds(); + const display = getScreen().getDisplayMatching(hudBounds); + const { workArea } = display; + const x = Math.round(workArea.x + (workArea.width - UPDATE_TOAST_WIDTH) / 2); + const y = Math.round( + workArea.y + workArea.height - UPDATE_TOAST_HEIGHT - HUD_EDGE_MARGIN_DIP, + ); + + return { + x, + y, + width: UPDATE_TOAST_WIDTH, + height: UPDATE_TOAST_HEIGHT, + }; + } + + const primaryDisplay = getScreen().getPrimaryDisplay(); + const { workArea } = primaryDisplay; + return { + x: Math.round(workArea.x + (workArea.width - UPDATE_TOAST_WIDTH) / 2), + y: Math.round(workArea.y + workArea.height - UPDATE_TOAST_HEIGHT - HUD_EDGE_MARGIN_DIP), + width: UPDATE_TOAST_WIDTH, + height: UPDATE_TOAST_HEIGHT, + }; +} + +function positionUpdateToastWindow() { + if (!updateToastWindow || updateToastWindow.isDestroyed()) { + return; + } + + updateToastWindow.setBounds(getUpdateToastBounds(), false); + updateToastWindow.moveTop(); +} + +function setHudOverlayFallbackExpanded(expanded: boolean) { + if (hudOverlayRecordingActive) { + hudOverlayFallbackExpanded = false; + return; + } + + hudOverlayFallbackExpanded = expanded; + if ( + !hudOverlayWindow || + hudOverlayWindow.isDestroyed() || + isHudOverlayMousePassthroughSupported() + ) { + return; + } + + const { workArea } = getHudOverlayDisplay(); + const nextBounds = resizeHudOverlayFallbackBounds( + workArea, + hudOverlayWindow.getBounds(), + expanded, + ); + hudOverlayWindow.setBounds(nextBounds, false); + positionUpdateToastWindow(); + if (hudOverlayWindow.isVisible()) { + hudOverlayWindow.moveTop(); + } +} + +function setHudOverlayMousePassthrough(ignore: boolean) { + hudOverlayIgnoringMouse = + hudOverlaySourceSelectionActive && !hudOverlayRecordingActive ? true : ignore; + + if (hudOverlayMouseReassertTimer) { + clearTimeout(hudOverlayMouseReassertTimer); + hudOverlayMouseReassertTimer = null; + } + + if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) { + return; + } + + if (hudOverlayRecordingActive) { + hudOverlayFallbackExpanded = false; + applyHudOverlayBounds(); + } + + if (!isHudOverlayMousePassthroughSupported()) { + if (process.platform !== "linux") { + setHudOverlayFallbackExpanded(!ignore); + } + hudOverlayWindow.setIgnoreMouseEvents(false); + return; + } + + if (ignore) { + hudOverlayWindow.setIgnoreMouseEvents(true, { forward: true }); + return; + } + + hudOverlayWindow.setIgnoreMouseEvents(false); +} + +ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => { + setHudOverlayMousePassthrough(Boolean(ignore)); +}); + +ipcMain.on("hud-overlay-set-source-selection-active", (_event, active: boolean) => { + hudOverlaySourceSelectionActive = Boolean(active); + if (hudOverlaySourceSelectionActive) { + hudOverlayFallbackExpanded = false; + applyHudOverlayBounds(); + return; + } + + setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); +}); + +// Keep compatibility with existing drag IPC/state. +let hudUserPosition: { x: number; y: number } | null = null; +let hudDragOffset: { x: number; y: number } | null = null; +let hudDragLastCursor: { x: number; y: number } | null = null; +let hudDragFixedSize: { width: number; height: number } | null = null; + +ipcMain.on("hud-overlay-drag", (_event, phase: string, screenX: number, screenY: number) => { + if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) return; + + // On Linux the compositor (especially Wayland) refuses programmatic window + // placement, so BrowserWindow.setBounds() with x/y is silently ignored and + // the HUD appears "stuck". The renderer marks the drag handle as + // -webkit-app-region: drag on Linux, letting the OS move the window for us. + // The resulting position is captured by the win.on("moved", ...) listener + // below so `hudUserPosition` stays in sync. + if (process.platform === "linux") { + return; + } + + if (phase === "start") { + const bounds = hudOverlayWindow.getBounds(); + hudDragOffset = { x: screenX - bounds.x, y: screenY - bounds.y }; + hudDragLastCursor = { x: screenX, y: screenY }; + hudDragFixedSize = { width: bounds.width, height: bounds.height }; + } else if (phase === "move" && hudDragOffset) { + if ( + hudDragLastCursor && + hudDragLastCursor.x === screenX && + hudDragLastCursor.y === screenY + ) { + return; + } + + hudDragLastCursor = { x: screenX, y: screenY }; + const targetX = Math.round(screenX - hudDragOffset.x); + const targetY = Math.round(screenY - hudDragOffset.y); + const fixedWidth = hudDragFixedSize?.width ?? hudOverlayWindow.getBounds().width; + const fixedHeight = hudDragFixedSize?.height ?? hudOverlayWindow.getBounds().height; + hudOverlayWindow.setBounds( + { + x: targetX, + y: targetY, + width: fixedWidth, + height: fixedHeight, + }, + false, + ); + } else if (phase === "end") { + const finalBounds = hudOverlayWindow.getBounds(); + hudUserPosition = { x: finalBounds.x, y: finalBounds.y }; + + hudDragOffset = null; + hudDragLastCursor = null; + hudDragFixedSize = null; + } +}); + +ipcMain.on("hud-overlay-hide", () => { + if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) { + hudOverlayWindow.minimize(); + } +}); + +ipcMain.handle("get-hud-overlay-capture-protection", () => { + const enabled = loadHudOverlayCaptureProtectionSetting(); + + return { + success: true, + enabled, + }; +}); + +ipcMain.handle("get-hud-overlay-mouse-passthrough-supported", () => { + return { + success: true, + supported: isHudOverlayMousePassthroughSupported(), + }; +}); + +ipcMain.on("hud-overlay-set-webcam-preview-visible", (_event, visible: boolean) => { + const nextVisible = Boolean(visible); + if (hudOverlayWebcamPreviewVisible === nextVisible) { + return; + } + + hudOverlayWebcamPreviewVisible = nextVisible; + if (hudOverlayRecordingActive) { + applyHudOverlayBounds(); + } +}); + +ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => { + loadHudOverlayCaptureProtectionSetting(); + hudOverlayHiddenFromCapture = Boolean(enabled); + persistHudOverlayCaptureProtectionSetting(hudOverlayHiddenFromCapture); + + reassertHudOverlayCaptureProtection(); + + return { + success: true, + enabled: hudOverlayHiddenFromCapture, + }; +}); + +const editorWindows = new Set(); +let recordingPreparationActive = false; +function getHudEditorMode() { + return isHudInEditorMode( + editorWindows.size, + recordingPreparationActive, + hudOverlayRecordingActive, + ); +} +export function setHudRecordingPreparationActive(active: boolean) { + recordingPreparationActive = active; + notifyEditorMode(); +} +function notifyEditorMode() { + if (hudOverlayWindow && !hudOverlayWindow.webContents.isDestroyed()) { + hudOverlayWindow.webContents.send("editor-mode-changed", getHudEditorMode()); + } +} +ipcMain.handle("get-editor-mode", getHudEditorMode); export function createHudOverlayWindow(): BrowserWindow { - const win = new BrowserWindow({ - width: 250, - height: 80, - minWidth: 250, - maxWidth: 250, - minHeight: 80, - maxHeight: 80, - frame: false, - transparent: true, - resizable: false, - alwaysOnTop: true, - skipTaskbar: true, - hasShadow: false, - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - nodeIntegration: false, - contextIsolation: true, - backgroundThrottling: false, - }, - }) - - - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', (new Date).toLocaleString()) - }) - - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL + '?windowType=hud-overlay') - } else { - win.loadFile(path.join(RENDERER_DIST, 'index.html'), { - query: { windowType: 'hud-overlay' } - }) - } - - return win + const perfStart = Date.now(); + loadHudOverlayCaptureProtectionSetting(); + hudOverlayFallbackExpanded = false; + hudOverlayWebcamPreviewVisible = false; + const initialBounds = getHudOverlayBounds(); + let hasShownHudWindow = false; + + const win = new BrowserWindow({ + width: initialBounds.width, + height: initialBounds.height, + x: initialBounds.x, + y: initialBounds.y, + frame: false, + transparent: true, + backgroundColor: "#00000000", + resizable: false, + alwaysOnTop: true, + // The HUD is Recordly's persistent top-level window, so it owns the + // Windows taskbar entry while auxiliary overlays stay hidden there. + ...getHudOverlayTaskbarOptions(process.platform), + hasShadow: false, + show: false, + webPreferences: { + preload: path.join(electronWindowsDir, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + webSecurity: false, + backgroundThrottling: false, + }, + }); + // Keep the recording controls and webcam above normal and full-screen apps. + // Transparent regions remain click-through via setIgnoreMouseEvents(). + win.setAlwaysOnTop(true, "screen-saver"); + if (process.platform === "darwin") { + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + skipTransformProcessType: true, + }); + } + + const showHudWindow = () => { + if (hasShownHudWindow || win.isDestroyed()) { + return; + } + if ( + updateToastWindow && + !updateToastWindow.isDestroyed() && + updateToastWindow.isVisible() + ) { + hudWasVisibleBeforeUpdateToast = true; + return; + } + hasShownHudWindow = true; + // Showing or changing native window state can recreate platform window + // flags. Reassert capture protection on both sides of the transition. + applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture); + if (process.platform === "win32") { + // A focusable window is required for a Windows taskbar entry, but the + // always-on-top HUD must not steal focus when Recordly starts. + win.showInactive(); + } else { + win.show(); + } + win.moveTop(); + applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture); + if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { + win.setIgnoreMouseEvents(false); + setTimeout(() => { + if (!win.isDestroyed()) { + setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); + } + }, 50); + } + }; + + applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture); + win.on("show", () => { + if (!win.isDestroyed()) { + applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture); + } + }); + + if (isHudOverlayMousePassthroughSupported()) { + if (hudOverlayRecordingActive) { + hudOverlayIgnoringMouse = false; + win.setIgnoreMouseEvents(false); + } else { + hudOverlayIgnoringMouse = true; + win.setIgnoreMouseEvents(true, { forward: true }); + } + } + + // On Windows 11+, focus changes (e.g. showing a native notification) can break + // setIgnoreMouseEvents forwarding on a transparent always-on-top window, making + // it permanently click-through without hover detection. Re-initialise the + // pass-through-with-forwarding state whenever the window gains focus by toggling + // the flag off then back on so the native WS_EX_TRANSPARENT flag is fully reset. + if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { + win.on("focus", () => { + if (!win.isDestroyed()) { + win.setIgnoreMouseEvents(false); + setTimeout(() => { + if (!win.isDestroyed()) { + setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); + } + }, 50); + } + }); + } + + win.webContents.on("did-finish-load", () => { + console.log(`[PERF:MAIN] HUD Window: did-finish-load in ${Date.now() - perfStart}ms`); + win?.webContents.send("main-process-message", new Date().toLocaleString()); + // Safety fallback if renderer-ready signal never arrives. + setTimeout(() => { + showHudWindow(); + }, 1800); + }); + + // Safety net: on Linux the renderer may fail to fire did-finish-load + // (for example due to GPU/VAAPI startup issues). Show the window after + // ready-to-show as a fallback so the HUD still appears. + win.once("ready-to-show", () => { + setTimeout(() => { + if (!win.isDestroyed() && !win.isVisible()) { + showHudWindow(); + } + }, 500); + }); + + const handleHudRendererReady = () => { + if (!win.isDestroyed()) { + console.log(`[PERF:MAIN] HUD Window: renderer-ready in ${Date.now() - perfStart}ms`); + showHudWindow(); + } + }; + ipcMain.on("hud-overlay-renderer-ready", handleHudRendererReady); + + hudOverlayWindow = win; + + // On Linux the HUD is dragged by the OS via -webkit-app-region (Wayland + // forbids client-side positioning). Mirror moved bounds into drag state. + if (process.platform === "linux") { + win.on("moved", () => { + if (win.isDestroyed()) return; + const { x, y } = win.getBounds(); + hudUserPosition = { x, y }; + }); + } + + // Reset the user's saved HUD position when displays change so the bar + // doesn't end up stranded off-screen after a monitor is disconnected. + const screen = getScreen(); + const handleDisplayRemoved = () => { + hudUserPosition = null; + }; + const handleDisplayMetricsChanged = () => { + if (hudUserPosition) { + const displays = screen.getAllDisplays(); + const onScreen = displays.some( + (d) => + hudUserPosition!.x >= d.workArea.x && + hudUserPosition!.x < d.workArea.x + d.workArea.width && + hudUserPosition!.y >= d.workArea.y && + hudUserPosition!.y < d.workArea.y + d.workArea.height, + ); + if (!onScreen) { + hudUserPosition = null; + } + } + applyHudOverlayBounds(); + }; + screen.on("display-removed", handleDisplayRemoved); + screen.on("display-metrics-changed", handleDisplayMetricsChanged); + + win.on("closed", () => { + ipcMain.removeListener("hud-overlay-renderer-ready", handleHudRendererReady); + screen.removeListener("display-removed", handleDisplayRemoved); + screen.removeListener("display-metrics-changed", handleDisplayMetricsChanged); + if (hudOverlayWindow === win) { + hudOverlayWindow = null; + recordingPreparationActive = false; + hudCaptureStarting = false; + hudOverlayRecordingActive = false; + } + }); + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=hud-overlay"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "hud-overlay" }, + }); + } + + return win; +} + +export function getHudOverlayWindow(): BrowserWindow | null { + return hudOverlayWindow && !hudOverlayWindow.isDestroyed() ? hudOverlayWindow : null; +} + +/** + * Re-initialise the HUD overlay's mouse passthrough state. + * + * On Windows 11+, any new BrowserWindow appearing (even focusable:false ones + * like the source highlight overlay) can silently corrupt the + * WS_EX_TRANSPARENT flag that backs setIgnoreMouseEvents forwarding. Call + * this after any operation that creates or destroys a sibling window so that + * hover detection on the HUD is immediately restored without requiring the + * user to move their mouse over the bar. + */ +export function reassertHudOverlayMousePassthrough(): void { + if (process.platform !== "win32" || !isHudOverlayMousePassthroughSupported()) { + return; + } + + const hud = getHudOverlayWindow(); + if (!hud) { + return; + } + + // Toggle off then back on so the native WS_EX_TRANSPARENT flag is fully + // re-initialised rather than merely re-asserted in a potentially broken state. + hud.setIgnoreMouseEvents(false); + if (hudOverlayMouseReassertTimer) { + clearTimeout(hudOverlayMouseReassertTimer); + } + hudOverlayMouseReassertTimer = setTimeout(() => { + hudOverlayMouseReassertTimer = null; + if (!hud.isDestroyed()) { + setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); + } + }, 50); +} + +export function setHudOverlayRecordingActive(recording: boolean): void { + hudCaptureStarting = false; + hudOverlayRecordingActive = Boolean(recording); + notifyEditorMode(); + hudOverlayFallbackExpanded = false; + applyHudOverlayBounds(); + reassertHudOverlayCaptureProtection(); + // Start in passthrough mode. Forwarded pointer movement lets the renderer + // make the visible HUD controls interactive when the pointer reaches them, + // while transparent parts never block the recorded application. + setHudOverlayMousePassthrough(true); +} + +export function createUpdateToastWindow(): BrowserWindow { + const initialBounds = getUpdateToastBounds(); + + const win = new BrowserWindow({ + width: initialBounds.width, + height: initialBounds.height, + x: initialBounds.x, + y: initialBounds.y, + frame: false, + transparent: true, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, + show: false, + focusable: true, + backgroundColor: "#00000000", + webPreferences: { + preload: path.join(electronWindowsDir, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + backgroundThrottling: false, + }, + }); + + if (process.platform === "darwin") { + win.setAlwaysOnTop(true, "status"); + } + + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + // Keep Recordly a foreground application so macOS does not temporarily + // remove its Dock icon while showing an overlay window. + skipTransformProcessType: process.platform === "darwin", + }); + updateToastWindow = win; + + win.on("closed", () => { + if (updateToastWindow === win) { + updateToastWindow = null; + } + restoreHudAfterUpdateToast(); + }); + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=update-toast"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "update-toast" }, + }); + } + + return win; +} + +export function getUpdateToastWindow(): BrowserWindow | null { + return updateToastWindow && !updateToastWindow.isDestroyed() ? updateToastWindow : null; +} + +export function showUpdateToastWindow(): BrowserWindow { + const win = getUpdateToastWindow() ?? createUpdateToastWindow(); + const hud = getHudOverlayWindow(); + if (!win.isVisible()) { + hudWasVisibleBeforeUpdateToast = Boolean(hud?.isVisible()); + } + if (hud?.isVisible()) { + hud.hide(); + } + positionUpdateToastWindow(); + if (!win.isVisible()) { + if (process.platform === "win32") { + win.show(); + } else { + win.showInactive(); + } + } + win.moveTop(); + + return win; +} + +function restoreHudAfterUpdateToast(): void { + if (!hudWasVisibleBeforeUpdateToast) { + return; + } + + hudWasVisibleBeforeUpdateToast = false; + const hud = getHudOverlayWindow(); + if (!hud) { + return; + } + + if (process.platform === "win32") { + hud.showInactive(); + } else { + hud.show(); + } + hud.moveTop(); + setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); +} + +export function hideUpdateToastWindow(): void { + if (updateToastWindow && !updateToastWindow.isDestroyed()) { + updateToastWindow.hide(); + } + restoreHudAfterUpdateToast(); +} + +function loadPackagedEditorWindow(win: BrowserWindow) { + const query = getEditorWindowQuery(); + const queryString = new URLSearchParams(query).toString(); + const indexHtmlPath = path.join(RENDERER_DIST, "index.html"); + const packagedRendererBaseUrl = getPackagedRendererBaseUrl(); + const webContents = win.webContents; + + const loadFromFile = () => { + if (win.isDestroyed()) { + return; + } + + console.log("[editor-window] load-file", indexHtmlPath); + void win.loadFile(indexHtmlPath, { query }); + }; + + if (!packagedRendererBaseUrl) { + loadFromFile(); + return; + } + + const targetUrl = `${packagedRendererBaseUrl}/?${queryString}`; + let settled = false; + let timeoutId: NodeJS.Timeout | null = setTimeout(() => { + fallbackToFile("load-timeout"); + }, 5000); + + const clearTimeoutIfNeeded = () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + + const detachLoadListeners = () => { + clearTimeoutIfNeeded(); + if (webContents.isDestroyed()) { + return; + } + + webContents.removeListener("did-fail-load", handleDidFailLoad); + webContents.removeListener("did-finish-load", handleDidFinishLoad); + }; + + const fallbackToFile = (reason: string, details?: Record) => { + if (settled || win.isDestroyed()) { + return; + } + + settled = true; + detachLoadListeners(); + console.warn("[editor-window] packaged renderer URL failed, falling back to file", { + reason, + targetUrl, + ...details, + }); + loadFromFile(); + }; + + const handleDidFailLoad = ( + _event: Electron.Event, + errorCode: number, + errorDescription: string, + validatedURL: string, + isMainFrame: boolean, + ) => { + if (!isMainFrame || validatedURL !== targetUrl) { + return; + } + + fallbackToFile("did-fail-load", { + errorCode, + errorDescription, + validatedURL, + }); + }; + + const handleDidFinishLoad = () => { + if (webContents.getURL() !== targetUrl) { + return; + } + + settled = true; + detachLoadListeners(); + }; + + webContents.on("did-fail-load", handleDidFailLoad); + webContents.on("did-finish-load", handleDidFinishLoad); + win.once("closed", clearTimeoutIfNeeded); + + console.log("[editor-window] load-url", targetUrl); + void win.loadURL(targetUrl).catch((error) => { + fallbackToFile("load-url-rejected", { + error: error instanceof Error ? error.message : String(error), + }); + }); } export function createEditorWindow(): BrowserWindow { - const win = new BrowserWindow({ - width: 1200, - height: 800, - minWidth: 800, - minHeight: 600, - frame: true, - transparent: false, - resizable: true, - alwaysOnTop: false, - skipTaskbar: false, - title: '', - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - nodeIntegration: false, - contextIsolation: true, - webSecurity: false, - }, - }) - - // Maximize the window by default - win.maximize(); - - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', (new Date).toLocaleString()) - }) - - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL + '?windowType=editor') - } else { - win.loadFile(path.join(RENDERER_DIST, 'index.html'), { - query: { windowType: 'editor' } - }) - } - - return win + const perfStart = Date.now(); + console.log("[PERF:MAIN] createEditorWindow: STARTED"); + const isMac = process.platform === "darwin"; + const { workArea, workAreaSize } = getScreen().getPrimaryDisplay(); + const initialWidth = isMac ? Math.round(workAreaSize.width * 0.85) : workArea.width; + const initialHeight = isMac ? Math.round(workAreaSize.height * 0.85) : workArea.height; + + const win = new BrowserWindow({ + width: initialWidth, + height: initialHeight, + ...(!isMac && { + x: workArea.x, + y: workArea.y, + }), + minWidth: 800, + minHeight: 600, + ...(process.platform !== "darwin" && { + icon: WINDOW_ICON_PATH, + }), + ...(isMac && { + titleBarStyle: "hiddenInset", + trafficLightPosition: { x: 16, y: 20 }, + }), + autoHideMenuBar: !isMac, + transparent: false, + resizable: true, + alwaysOnTop: false, + skipTaskbar: false, + title: "Recordly", + show: false, + backgroundColor: "#000000", + webPreferences: { + preload: path.join(electronWindowsDir, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + webSecurity: false, + backgroundThrottling: false, + }, + }); + + recordingPreparationActive = false; + editorWindows.add(win); + notifyEditorMode(); + win.once("closed", () => { + editorWindows.delete(win); + notifyEditorMode(); + }); + + const publishWindowChrome = () => { + if (!win.isDestroyed()) + win.webContents.send("window-chrome-changed", { + trafficLightsVisible: isMac && !win.isFullScreen() && !win.isSimpleFullScreen(), + }); + }; + win.on("enter-full-screen", publishWindowChrome); + win.on("leave-full-screen", publishWindowChrome); + win.on("resize", publishWindowChrome); + win.webContents.on("did-finish-load", publishWindowChrome); + + win.once("ready-to-show", () => { + console.log(`[PERF:MAIN] Editor Window: ready-to-show in ${Date.now() - perfStart}ms`); + win.show(); + }); + + win.webContents.on("did-finish-load", () => { + console.log(`[PERF:MAIN] Editor Window: did-finish-load in ${Date.now() - perfStart}ms`); + win?.webContents.send("main-process-message", new Date().toLocaleString()); + // Fallback for Linux/Wayland where `ready-to-show` may not fire reliably. + if (!win.isDestroyed() && !win.isVisible()) { + console.log("[editor-window] forcing show after did-finish-load"); + win.show(); + } + }); + + win.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL) => { + console.error("[editor-window] did-fail-load", { + errorCode, + errorDescription, + validatedURL, + }); + }); + + win.webContents.on("render-process-gone", (_event, details) => { + console.error("[editor-window] render-process-gone", details); + }); + + win.on("show", () => { + console.log("[editor-window] show"); + }); + + win.on("focus", () => { + console.log("[editor-window] focus"); + }); + + win.on("enter-full-screen", () => { + if (!win.isDestroyed()) win.webContents.send("window-fullscreen-changed", true); + }); + + win.on("leave-full-screen", () => { + if (!win.isDestroyed()) win.webContents.send("window-fullscreen-changed", false); + }); + + if (VITE_DEV_SERVER_URL) { + const query = new URLSearchParams(getEditorWindowQuery()); + win.loadURL(`${VITE_DEV_SERVER_URL}?${query.toString()}`); + } else { + loadPackagedEditorWindow(win); + } + + return win; } export function createSourceSelectorWindow(): BrowserWindow { - const { width, height } = screen.getPrimaryDisplay().workAreaSize - - const win = new BrowserWindow({ - width: 620, - height: 420, - minHeight: 350, - maxHeight: 500, - x: Math.round((width - 620) / 2), - y: Math.round((height - 420) / 2), - frame: false, - resizable: false, - alwaysOnTop: true, - transparent: true, - backgroundColor: '#00000000', - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - nodeIntegration: false, - contextIsolation: true, - }, - }) - - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL + '?windowType=source-selector') - } else { - win.loadFile(path.join(RENDERER_DIST, 'index.html'), { - query: { windowType: 'source-selector' } - }) - } - - return win + const { width, height } = getScreen().getPrimaryDisplay().workAreaSize; + + const win = new BrowserWindow({ + width: 620, + height: 420, + minHeight: 350, + maxHeight: 500, + x: Math.round((width - 620) / 2), + y: Math.round((height - 420) / 2), + frame: false, + resizable: false, + alwaysOnTop: true, + transparent: true, + show: false, + ...(process.platform !== "darwin" && { + icon: WINDOW_ICON_PATH, + }), + backgroundColor: "#00000000", + webPreferences: { + preload: path.join(electronWindowsDir, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + }, + }); + + win.webContents.on("did-finish-load", () => { + setTimeout(() => { + if (!win.isDestroyed()) { + win.show(); + } + }, 100); + }); + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=source-selector"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "source-selector" }, + }); + } + + return win; +} + +export function createCountdownWindow(): BrowserWindow { + const primaryDisplay = getScreen().getPrimaryDisplay(); + const { width, height } = primaryDisplay.workAreaSize; + + const windowSize = 200; + const x = Math.floor((width - windowSize) / 2); + const y = Math.floor((height - windowSize) / 2); + + const win = new BrowserWindow({ + width: windowSize, + height: windowSize, + x: x, + y: y, + frame: false, + transparent: true, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, + focusable: true, + show: false, + webPreferences: { + preload: path.join(electronWindowsDir, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + }, + }); + + countdownWindow = win; + + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + // Keep Recordly a foreground application so macOS does not temporarily + // remove its Dock icon while showing the countdown. + skipTransformProcessType: process.platform === "darwin", + }); + + win.webContents.on("did-finish-load", () => { + if (!win.isDestroyed()) { + if (process.platform === "win32") { + win.showInactive(); + win.moveTop(); + } else { + win.show(); + } + } + }); + + win.on("closed", () => { + if (countdownWindow === win) { + countdownWindow = null; + } + }); + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=countdown"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "countdown" }, + }); + } + + return win; +} + +export function getCountdownWindow(): BrowserWindow | null { + return countdownWindow; +} + +export function closeCountdownWindow(): void { + if (countdownWindow && !countdownWindow.isDestroyed()) { + countdownWindow.close(); + countdownWindow = null; + } +} + +/** + * Covers every display with a transparent overlay for picking what to record. + * Each overlay renders the `capture-picker` window and reports back over IPC. + */ +export function createCapturePickerWindows(onClosed: () => void): BrowserWindow[] { + closeCapturePickerWindows(); + const cursorDisplayId = getScreen().getDisplayNearestPoint( + getScreen().getCursorScreenPoint(), + ).id; + capturePickerWindows = getScreen() + .getAllDisplays() + .map((display) => { + const win = new BrowserWindow({ + ...display.bounds, + frame: false, + transparent: true, + resizable: false, + movable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, + enableLargerThanScreen: true, + show: false, + backgroundColor: "#00000000", + ...(process.platform === "darwin" ? { type: "panel" as const } : {}), + webPreferences: { + preload: path.join(electronWindowsDir, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + }, + }); + // Above the menu bar and the HUD, on every Space and over full-screen apps. + win.setAlwaysOnTop(true, "screen-saver"); + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + skipTransformProcessType: true, + }); + win.setBounds(display.bounds); + // The page paints the app's opaque background before React makes it + // transparent, so the overlay stays hidden until the picker has painted. + let shown = false; + const show = () => { + if (shown || win.isDestroyed()) return; + shown = true; + win.show(); + if (display.id === cursorDisplayId) { + app.focus({ steal: true }); + win.focus(); + } + }; + win.webContents.ipc.once("capture-picker-ready", show); + win.webContents.once("did-finish-load", () => { + setTimeout(show, CAPTURE_PICKER_READY_TIMEOUT_MS); + }); + win.on("closed", () => { + const wasOpen = capturePickerWindows.includes(win); + capturePickerWindows = capturePickerWindows.filter( + (candidate) => candidate !== win, + ); + if (wasOpen) onClosed(); + }); + + const query = { windowType: "capture-picker", displayId: String(display.id) }; + if (VITE_DEV_SERVER_URL) { + win.loadURL(`${VITE_DEV_SERVER_URL}?${new URLSearchParams(query).toString()}`); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { query }); + } + return win; + }); + return capturePickerWindows; +} + +export function getCapturePickerWindows(): BrowserWindow[] { + return [...capturePickerWindows]; +} + +export function closeCapturePickerWindows(): void { + const windows = capturePickerWindows; + capturePickerWindows = []; + for (const win of windows) { + if (!win.isDestroyed()) win.close(); + } +} + +/** Whether a message came from one of the open picker overlays. */ +export function isCapturePickerWebContents(contents: WebContents): boolean { + return capturePickerWindows.some((win) => !win.isDestroyed() && win.webContents === contents); +} + +/** + * Outlines the area being recorded. The outline sits just outside the recorded + * rectangle and belongs to Recordly, which recordings exclude, so it never shows + * up in the video. + */ +export function showAreaRecordingBorder(area: { + x: number; + y: number; + width: number; + height: number; +}): void { + hideAreaRecordingBorder(); + const outset = 3; + const win = new BrowserWindow({ + x: Math.round(area.x) - outset, + y: Math.round(area.y) - outset, + width: Math.round(area.width) + outset * 2, + height: Math.round(area.height) + outset * 2, + frame: false, + transparent: true, + resizable: false, + movable: false, + focusable: false, + skipTaskbar: true, + hasShadow: false, + enableLargerThanScreen: true, + show: false, + backgroundColor: "#00000000", + ...(process.platform === "darwin" ? { type: "panel" as const } : {}), + webPreferences: { nodeIntegration: false, contextIsolation: true }, + }); + win.setIgnoreMouseEvents(true); + win.setAlwaysOnTop(true, "screen-saver"); + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + skipTransformProcessType: true, + }); + win.setContentProtection(true); + areaRecordingBorderWindow = win; + win.on("closed", () => { + if (areaRecordingBorderWindow === win) areaRecordingBorderWindow = null; + }); + + const html = `
`; + void win + .loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`) + .then(() => { + if (!win.isDestroyed()) win.showInactive(); + }) + .catch(() => { + if (!win.isDestroyed()) win.close(); + }); +} + +export function hideAreaRecordingBorder(): void { + const win = areaRecordingBorderWindow; + areaRecordingBorderWindow = null; + if (win && !win.isDestroyed()) win.close(); } diff --git a/electron/windowsCmakeGenerators.test.mjs b/electron/windowsCmakeGenerators.test.mjs new file mode 100644 index 000000000..2f651ba92 --- /dev/null +++ b/electron/windowsCmakeGenerators.test.mjs @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + configureWithWindowsCmakeGenerator, + WINDOWS_CMAKE_GENERATORS, + WINDOWS_VISUAL_STUDIO_INSTALL_DIRS, +} from "../scripts/windows-cmake-generators.mjs"; + +describe("configureWithWindowsCmakeGenerator", () => { + it("uses Visual Studio 2026 first", () => { + const configure = vi.fn(); + const clearCache = vi.fn(); + + const selected = configureWithWindowsCmakeGenerator({ + prefix: "test", + configure, + clearCache, + }); + + expect(selected).toBe("Visual Studio 18 2026"); + expect(WINDOWS_VISUAL_STUDIO_INSTALL_DIRS[0]).toBe("18"); + expect(configure).toHaveBeenCalledWith("Visual Studio 18 2026", "v143"); + expect(clearCache).toHaveBeenCalledTimes(1); + }); + + it("falls back in supported-version order and clears stale CMake state", () => { + const attempted = []; + const clearCache = vi.fn(); + const log = vi.fn(); + + const selected = configureWithWindowsCmakeGenerator({ + prefix: "test", + clearCache, + log, + configure: (generator, toolset) => { + attempted.push({ name: generator, toolset }); + if (generator !== "Visual Studio 16 2019") { + throw new Error(`${generator} unavailable`); + } + }, + }); + + expect(attempted).toEqual( + WINDOWS_CMAKE_GENERATORS.map(({ name, toolset }) => ({ name, toolset })), + ); + expect(clearCache).toHaveBeenCalledTimes(3); + expect(log).toHaveBeenCalledTimes(2); + expect(selected).toBe("Visual Studio 16 2019"); + }); + + it("rethrows the final configure error when no supported generator exists", () => { + const finalError = new Error("VS 2019 unavailable"); + + expect(() => + configureWithWindowsCmakeGenerator({ + prefix: "test", + clearCache: vi.fn(), + log: vi.fn(), + configure: (generator) => { + if (generator === "Visual Studio 16 2019") throw finalError; + throw new Error(`${generator} unavailable`); + }, + }), + ).toThrow(finalError); + }); +}); diff --git a/icons/icons/mac/icon.icns b/icons/icons/mac/icon.icns index bb9f0c497..b9f34ea75 100644 Binary files a/icons/icons/mac/icon.icns and b/icons/icons/mac/icon.icns differ diff --git a/icons/icons/png/1024x1024.png b/icons/icons/png/1024x1024.png index d4536ea02..7cc41c028 100644 Binary files a/icons/icons/png/1024x1024.png and b/icons/icons/png/1024x1024.png differ diff --git a/icons/icons/png/128x128.png b/icons/icons/png/128x128.png index 350021397..82b456e9d 100644 Binary files a/icons/icons/png/128x128.png and b/icons/icons/png/128x128.png differ diff --git a/icons/icons/png/16x16.png b/icons/icons/png/16x16.png index ee106dfdc..e1f45c1f4 100644 Binary files a/icons/icons/png/16x16.png and b/icons/icons/png/16x16.png differ diff --git a/icons/icons/png/24x24.png b/icons/icons/png/24x24.png index 2650b7166..e146b337a 100644 Binary files a/icons/icons/png/24x24.png and b/icons/icons/png/24x24.png differ diff --git a/icons/icons/png/256x256.png b/icons/icons/png/256x256.png index db4cb3d56..0592161ab 100644 Binary files a/icons/icons/png/256x256.png and b/icons/icons/png/256x256.png differ diff --git a/icons/icons/png/32x32.png b/icons/icons/png/32x32.png index 47994b94b..6fb056f8a 100644 Binary files a/icons/icons/png/32x32.png and b/icons/icons/png/32x32.png differ diff --git a/icons/icons/png/48x48.png b/icons/icons/png/48x48.png index 92cd97e6d..48af277bf 100644 Binary files a/icons/icons/png/48x48.png and b/icons/icons/png/48x48.png differ diff --git a/icons/icons/png/512x512.png b/icons/icons/png/512x512.png index 8f2ac4aa8..4016ff592 100644 Binary files a/icons/icons/png/512x512.png and b/icons/icons/png/512x512.png differ diff --git a/icons/icons/png/64x64.png b/icons/icons/png/64x64.png index 7944bc00c..e132d69a4 100644 Binary files a/icons/icons/png/64x64.png and b/icons/icons/png/64x64.png differ diff --git a/icons/icons/win/icon.ico b/icons/icons/win/icon.ico index 5793cce79..52e47c894 100644 Binary files a/icons/icons/win/icon.ico and b/icons/icons/win/icon.ico differ diff --git a/index.html b/index.html index ce1c274aa..ebcb3cc7f 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,106 @@ - - - - - - - - - -
- - - + + + + + + + Recordly + + + + +
+
+ + + +
+
+ + +
+
+ + + + diff --git a/openscreen.png b/openscreen.png deleted file mode 100644 index 7e44bbb80..000000000 Binary files a/openscreen.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 23fb3cfbe..7a2f7e9d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10407 +1,8589 @@ { - "name": "openscreen", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "openscreen", - "version": "0.0.0", - "dependencies": { - "@fix-webm-duration/fix": "^1.0.1", - "@pixi/filter-drop-shadow": "^5.2.0", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@uiw/react-color-colorful": "^2.9.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "dnd-timeline": "^2.2.0", - "lucide-react": "^0.545.0", - "mp4-muxer": "^5.2.2", - "mp4box": "^2.2.0", - "pixi.js": "^8.14.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-icons": "^5.5.0", - "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1", - "tailwindcss-animate": "^1.0.7", - "uiohook-napi": "^1.5.4" - }, - "devDependencies": { - "@types/react": "^18.2.64", - "@types/react-dom": "^18.2.21", - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.21", - "electron": "^30.0.1", - "electron-builder": "^24.13.3", - "electron-rebuild": "^3.2.9", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "typescript": "^5.2.2", - "vite": "^5.1.6", - "vite-plugin-electron": "^0.28.6", - "vite-plugin-electron-renderer": "^0.14.5" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@electron/notarize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", - "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", - "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/universal": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", - "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.2.1", - "@malept/cross-spawn-promise": "^1.1.0", - "debug": "^4.3.1", - "dir-compare": "^3.0.0", - "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@fix-webm-duration/fix": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@fix-webm-duration/fix/-/fix-1.0.1.tgz", - "integrity": "sha512-rRN4CpWQaXRbCXYqKIxnsUq8OSWSGq/SVlnxxkx0HxMZZ1u0qnB24P766o8QS5YaMMqAOFAzmsMmfZ2OWucOLQ==", - "license": "MIT", - "dependencies": { - "@fix-webm-duration/parser": "1.0.1", - "tslib": "^2.3.0" - } - }, - "node_modules/@fix-webm-duration/parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@fix-webm-duration/parser/-/parser-1.0.1.tgz", - "integrity": "sha512-Z6el7ohBBpym4iOmxDxumN715cHzLcAbtOtYfIbvoyToVtAuxvHt3ELXGff83iAXEU1Yj7g+obQBdiKJYVhbWw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@pixi/color": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz", - "integrity": "sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@pixi/colord": "^2.9.6" - } - }, - "node_modules/@pixi/colord": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", - "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", - "license": "MIT" - }, - "node_modules/@pixi/constants": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.3.tgz", - "integrity": "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@pixi/core": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.3.tgz", - "integrity": "sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@pixi/color": "7.4.3", - "@pixi/constants": "7.4.3", - "@pixi/extensions": "7.4.3", - "@pixi/math": "7.4.3", - "@pixi/runner": "7.4.3", - "@pixi/settings": "7.4.3", - "@pixi/ticker": "7.4.3", - "@pixi/utils": "7.4.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/pixijs" - } - }, - "node_modules/@pixi/extensions": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.3.tgz", - "integrity": "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==", - "license": "MIT", - "peer": true - }, - "node_modules/@pixi/filter-drop-shadow": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.2.0.tgz", - "integrity": "sha512-cYS2KDER7cwCu0V4VNSxTHGvzmNcEXdC9j3031YBOkUAE3+p17LMS/TAt6XeMfJV7KaPuusvXy2NFgGkv3RDbw==", - "license": "MIT", - "dependencies": { - "@pixi/filter-kawase-blur": "5.1.1" - }, - "peerDependencies": { - "@pixi/core": "^7.0.0-X" - } - }, - "node_modules/@pixi/filter-kawase-blur": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.1.1.tgz", - "integrity": "sha512-nPnJ1ChBFP+4pgFCwC0RJgHAJCetiHcQU3INH7zCdq88cFABmVmhN+wCKRNg4H7lF1EJjaXgFDkTrTreOD/bnw==", - "license": "MIT", - "peerDependencies": { - "@pixi/core": "^7.0.0-X" - } - }, - "node_modules/@pixi/math": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.3.tgz", - "integrity": "sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw==", - "license": "MIT", - "peer": true - }, - "node_modules/@pixi/runner": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.3.tgz", - "integrity": "sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw==", - "license": "MIT", - "peer": true - }, - "node_modules/@pixi/settings": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.3.tgz", - "integrity": "sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@pixi/constants": "7.4.3", - "@types/css-font-loading-module": "^0.0.12", - "ismobilejs": "^1.1.0" - } - }, - "node_modules/@pixi/ticker": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.3.tgz", - "integrity": "sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@pixi/extensions": "7.4.3", - "@pixi/settings": "7.4.3", - "@pixi/utils": "7.4.3" - } - }, - "node_modules/@pixi/utils": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.3.tgz", - "integrity": "sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@pixi/color": "7.4.3", - "@pixi/constants": "7.4.3", - "@pixi/settings": "7.4.3", - "@types/earcut": "^2.1.0", - "earcut": "^2.2.4", - "eventemitter3": "^4.0.0", - "url": "^0.11.0" - } - }, - "node_modules/@pixi/utils/node_modules/@types/earcut": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", - "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@pixi/utils/node_modules/earcut": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", - "license": "ISC", - "peer": true - }, - "node_modules/@pixi/utils/node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT", - "peer": true - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", - "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", - "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", - "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", - "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", - "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", - "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", - "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", - "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", - "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", - "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", - "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", - "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", - "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", - "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", - "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", - "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", - "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", - "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", - "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", - "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", - "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", - "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/css-font-loading-module": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", - "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", - "license": "MIT" - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/dom-webcodecs": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.17.tgz", - "integrity": "sha512-IwKW5uKL0Zrv5ccUJpjIlqf7ppk2v29l/ZLQxLlwHxljBfnDD9Gxm+hzMkGM0AOAL/21H0pp7cTUYLiiVUGchA==", - "license": "MIT" - }, - "node_modules/@types/earcut": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", - "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz", - "integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.26", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", - "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/wicg-file-system-access": { - "version": "2020.9.8", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz", - "integrity": "sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==", - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@uiw/color-convert": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.0.tgz", - "integrity": "sha512-tFm6iac8iN3VL9IIAXKAVpBX6ZQH7ucCvb+EgZNM5y8prRWlk53cERnP4RycE3wnYb/HUKW0lrngoygQnEWAHg==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0" - } - }, - "node_modules/@uiw/react-color-alpha": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@uiw/react-color-alpha/-/react-color-alpha-2.9.0.tgz", - "integrity": "sha512-tP9zs2KUPzIGvdtTBTNlDEdwHWcNJ3Ju/nnpPTsKERRJF2vzucb3v18JzihX0ACiOXU9+JVqgpAVgvoZXMeITA==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.0", - "@uiw/react-drag-event-interactive": "2.9.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-colorful": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@uiw/react-color-colorful/-/react-color-colorful-2.9.0.tgz", - "integrity": "sha512-qw7zhgX9J4aydK0hKKLroXMYp0tw3ASNuZM1fQNwXXIpOkEW4zO+PHTkzUBpbII68wGS8tAxsZessTWNpAVi0w==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.0", - "@uiw/react-color-alpha": "2.9.0", - "@uiw/react-color-hue": "2.9.0", - "@uiw/react-color-saturation": "2.9.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-hue": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@uiw/react-color-hue/-/react-color-hue-2.9.0.tgz", - "integrity": "sha512-2Ix027ref3ppiVq+OQkbyKr6O8fLTH+n0wEW4xhHfa3kV8mBa2bEu8jAjrkSwO6uElaE6OJcuqYe2llyKRlIFg==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.0", - "@uiw/react-color-alpha": "2.9.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-saturation": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@uiw/react-color-saturation/-/react-color-saturation-2.9.0.tgz", - "integrity": "sha512-tBARqsW0ap+gwByGKSid1nYaGJd6dw2MTMwCABF9rebPQHtYbN/YgBjl9dNnp6XDGd+VBhH4RbpM38WV7J1F4g==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.0", - "@uiw/react-drag-event-interactive": "2.9.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-drag-event-interactive": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.9.0.tgz", - "integrity": "sha512-oBqrClsSuYA3IjkSHp4uHOBLgPkR98KeKeheqCRltF8w/a1CPaFCl43pyKyxwXaTV6qM6cb3VkT3JRCHMNqTzA==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@webgpu/types": { - "version": "0.1.66", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", - "integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/app-builder-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", - "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", - "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "2.2.1", - "@electron/osx-sign": "1.0.5", - "@electron/universal": "1.5.1", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.9", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", - "chromium-pickle-js": "^0.2.0", - "debug": "^4.3.4", - "ejs": "^3.1.8", - "electron-publish": "24.13.1", - "form-data": "^4.0.0", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "is-ci": "^3.0.0", - "isbinaryfile": "^5.0.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "minimatch": "^5.1.1", - "read-config-file": "6.3.2", - "sanitize-filename": "^1.6.3", - "semver": "^7.3.8", - "tar": "^6.1.12", - "temp-file": "^3.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "24.13.3", - "electron-builder-squirrel-windows": "24.13.3" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "dev": true, - "license": "ISC" - }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz", - "integrity": "sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/bluebird-lst": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", - "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bluebird": "^3.5.5" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "24.13.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", - "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "4.0.0", - "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.2.4", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-ci": "^3.0.0", - "js-yaml": "^4.1.0", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", - "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001749", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", - "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/config-file-ts": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", - "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.3.10", - "typescript": "^5.3.3" - } - }, - "node_modules/config-file-ts/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/dir-compare": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", - "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal": "^1.0.0", - "minimatch": "^3.0.4" - } - }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/dmg-builder": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", - "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "24.13.3", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dnd-timeline": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dnd-timeline/-/dnd-timeline-2.2.0.tgz", - "integrity": "sha512-bQ/2bm70eA7YeztgdxoSpdpTwPzj8VT2/wTlYrnFpqJ71et7EVJZR35XPZIVzBqSyKK9T/QyUzE26gYck9ldxg==", - "license": "MIT", - "dependencies": { - "@dnd-kit/core": "^6.1.0", - "@dnd-kit/utilities": "^3.2.2", - "resize-observer-polyfill": "^1.5.1" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", - "license": "ISC" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-30.5.1.tgz", - "integrity": "sha512-AhL7+mZ8Lg14iaNfoYTkXQ2qee8mmsQyllKdqxlpv/zrKgfxz6jNVtcRRbQtLxtF8yzcImWdfTQROpYiPumdbw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", - "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "24.13.3", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", - "chalk": "^4.1.2", - "dmg-builder": "24.13.3", - "fs-extra": "^10.1.0", - "is-ci": "^3.0.0", - "lazy-val": "^1.0.5", - "read-config-file": "6.3.2", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", - "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "24.13.3", - "archiver": "^5.3.1", - "builder-util": "24.13.1", - "fs-extra": "^10.1.0" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "24.13.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", - "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", - "chalk": "^4.1.2", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-rebuild": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-3.2.9.tgz", - "integrity": "sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw==", - "deprecated": "Please use @electron/rebuild moving forward. There is no API change, just a package name change", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "lzma-native": "^8.0.5", - "node-abi": "^3.0.0", - "node-api-version": "^0.1.4", - "node-gyp": "^9.0.0", - "ora": "^5.1.0", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/src/cli.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/electron-rebuild/node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/electron-rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-rebuild/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-rebuild/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.234", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", - "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", - "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gifuct-js": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", - "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", - "license": "MIT", - "dependencies": { - "js-binary-schema-parser": "^2.0.3" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, - "license": "ISC" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/isbinaryfile": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz", - "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/ismobilejs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", - "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", - "license": "MIT" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-binary-schema-parser": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", - "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "dev": true, - "license": "MPL-2.0", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.545.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", - "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/lzma-native": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", - "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^3.1.0", - "node-gyp-build": "^4.2.1", - "readable-stream": "^3.6.0" - }, - "bin": { - "lzmajs": "bin/lzmajs" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/lzma-native/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dev": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-fetch/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mp4-muxer": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/mp4-muxer/-/mp4-muxer-5.2.2.tgz", - "integrity": "sha512-dhozjTywI0h2qFzeShagt8YYw811fh1XlwiDCE2f6Aeqf6xG2CyuShoSa5E0AZDO8pPF0JOZ3wOmWBNWIGdSpQ==", - "deprecated": "This library is superseded by Mediabunny. Please migrate to it.", - "license": "MIT", - "dependencies": { - "@types/dom-webcodecs": "^0.1.6", - "@types/wicg-file-system-access": "^2020.9.5" - } - }, - "node_modules/mp4box": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.2.0.tgz", - "integrity": "sha512-tE+L7wdhSuwBKZGjUzj03Qzj4lWyOw8pHSPyLnvHTKx92NJGkJls0pcEusUHWEh5gWVBlhdu79STJh4Bubz9mQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=20.8.1" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.78.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", - "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.1.4.tgz", - "integrity": "sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-gyp": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", - "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-svg-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", - "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pixi.js": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz", - "integrity": "sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==", - "license": "MIT", - "dependencies": { - "@pixi/colord": "^2.9.6", - "@types/css-font-loading-module": "^0.0.12", - "@types/earcut": "^3.0.0", - "@webgpu/types": "^0.1.40", - "@xmldom/xmldom": "^0.8.10", - "earcut": "^3.0.2", - "eventemitter3": "^5.0.1", - "gifuct-js": "^2.1.2", - "ismobilejs": "^1.1.1", - "parse-svg-path": "^0.1.2", - "tiny-lru": "^11.4.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/pixijs" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/read-config-file": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", - "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "config-file-ts": "^0.2.4", - "dotenv": "^9.0.2", - "dotenv-expand": "^5.1.0", - "js-yaml": "^4.1.0", - "json5": "^2.2.0", - "lazy-val": "^1.0.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/rollup": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", - "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.4", - "@rollup/rollup-android-arm64": "4.52.4", - "@rollup/rollup-darwin-arm64": "4.52.4", - "@rollup/rollup-darwin-x64": "4.52.4", - "@rollup/rollup-freebsd-arm64": "4.52.4", - "@rollup/rollup-freebsd-x64": "4.52.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", - "@rollup/rollup-linux-arm-musleabihf": "4.52.4", - "@rollup/rollup-linux-arm64-gnu": "4.52.4", - "@rollup/rollup-linux-arm64-musl": "4.52.4", - "@rollup/rollup-linux-loong64-gnu": "4.52.4", - "@rollup/rollup-linux-ppc64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-musl": "4.52.4", - "@rollup/rollup-linux-s390x-gnu": "4.52.4", - "@rollup/rollup-linux-x64-gnu": "4.52.4", - "@rollup/rollup-linux-x64-musl": "4.52.4", - "@rollup/rollup-openharmony-arm64": "4.52.4", - "@rollup/rollup-win32-arm64-msvc": "4.52.4", - "@rollup/rollup-win32-ia32-msvc": "4.52.4", - "@rollup/rollup-win32-x64-gnu": "4.52.4", - "@rollup/rollup-win32-x64-msvc": "4.52.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "license": "ISC" - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "peer": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "peer": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sonner": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", - "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ssri/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ssri/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-lru": { - "version": "11.4.5", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.5.tgz", - "integrity": "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uiohook-napi": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/uiohook-napi/-/uiohook-napi-1.5.4.tgz", - "integrity": "sha512-7vPVDNwgb6MwTgviA/dnF2MrW0X5xm76fAqaOAC3cEKkswqAZOPw1USu14Sr6383s5qhXegcJaR63CpJOPCNAg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "4.x.x" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", - "license": "MIT", - "peer": true, - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.12.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "license": "MIT", - "peer": true - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/vite": { - "version": "5.4.20", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", - "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-plugin-electron": { - "version": "0.28.8", - "resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.28.8.tgz", - "integrity": "sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite-plugin-electron-renderer": "*" - }, - "peerDependenciesMeta": { - "vite-plugin-electron-renderer": { - "optional": true - } - } - }, - "node_modules/vite-plugin-electron-renderer": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", - "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", - "dev": true, - "license": "MIT" - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - } - } + "name": "recordly", + "version": "1.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "recordly", + "version": "1.4.0", + "hasInstallScript": true, + "dependencies": { + "@phosphor-icons/react": "^2.1.10", + "@solar-icons/react": "^2.3.0", + "@supabase/supabase-js": "^2.116.0", + "capturekit": "^1.0.13", + "electron-updater": "^6.8.3", + "ffmpeg-static": "^5.3.0", + "ffprobe-static": "^3.1.0", + "uiohook-napi": "^1.5.4" + }, + "devDependencies": { + "@biomejs/biome": "2.3.13", + "@dnd-kit/core": "^6.3.1", + "@electron/rebuild": "^4.2.0", + "@fix-webm-duration/fix": "^1.0.1", + "@heroui/react": "^3.2.6", + "@heroui/styles": "^3.2.6", + "@playwright/test": "^1.63.0", + "@tailwindcss/postcss": "^4.3.3", + "@types/gif.js": "^0.2.5", + "@types/node": "^25.0.3", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^4.2.1", + "clsx": "^2.1.1", + "dnd-timeline": "^2.2.0", + "electron": "^43.1.0", + "electron-builder": "^26.7.0", + "emoji-picker-react": "^4.16.1", + "fast-check": "^4.5.2", + "gif.js": "^0.2.0", + "mediabunny": "^1.25.1", + "motion": "^12.23.24", + "mp4box": "^2.2.0", + "pixi-filters": "^6.1.5", + "pixi.js": "^8.14.0", + "postcss": "^8.5.6", + "react": "^19.3.0", + "react-aria": "3.52.1", + "react-dom": "^19.3.0", + "react-icons": "^5.5.0", + "react-resizable-panels": "^3.0.6", + "react-rnd": "^10.5.2", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.3.3", + "tailwindcss-animate": "^1.0.7", + "terser": "^5.44.1", + "typescript": "^5.2.2", + "vite": "^5.1.6", + "vite-plugin-electron": "^0.28.6", + "vite-plugin-electron-renderer": "^0.14.5", + "vitest": "^3.2.7", + "web-demuxer": "^4.0.0" + } + }, + "node_modules/@adobe/react-spectrum": { + "version": "3.47.5", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.47.5.tgz", + "integrity": "sha512-iTbAwwMijVnQjwjwbvkWn82AyRAbFNSJzt/VawWp1vXRIxVIO9COZ1pPG5Fj88xXgqlG8maVcv58p74vqqy5HA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@react-types/shared": "^3.36.1", + "@spectrum-icons/ui": "^3.7.2", + "@spectrum-icons/workflow": "^4.3.2", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "clsx": "^2.0.0", + "react-aria": "3.52.1", + "react-aria-components": "1.21.1", + "react-stately": "3.50.0", + "react-transition-group": "^4.4.5", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@adobe/react-spectrum-ui": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-ui/-/react-spectrum-ui-1.2.1.tgz", + "integrity": "sha512-wcrbEE2O/9WnEn6avBnaVRRx88S5PLFsPLr4wffzlbMfXeQsy+RMQwaJd3cbzrn18/j04Isit7f7Emfn0dhrJA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@adobe/react-spectrum-workflow": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-workflow/-/react-spectrum-workflow-2.3.5.tgz", + "integrity": "sha512-b53VIPwPWKb/T5gzE3qs+QlGP5gVrw/LnWV3xMksDU+CRl3rzOKUwxIGiZO8ICyYh1WiyqY4myGlPU/nAynBUg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.13.tgz", + "integrity": "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.13", + "@biomejs/cli-darwin-x64": "2.3.13", + "@biomejs/cli-linux-arm64": "2.3.13", + "@biomejs/cli-linux-arm64-musl": "2.3.13", + "@biomejs/cli-linux-x64": "2.3.13", + "@biomejs/cli-linux-x64-musl": "2.3.13", + "@biomejs/cli-win32-arm64": "2.3.13", + "@biomejs/cli-win32-x64": "2.3.13" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.13.tgz", + "integrity": "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.13.tgz", + "integrity": "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.13.tgz", + "integrity": "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.13.tgz", + "integrity": "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.13.tgz", + "integrity": "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.13.tgz", + "integrity": "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.13.tgz", + "integrity": "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.13.tgz", + "integrity": "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@derhuerst/http-basic/node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/get/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fix-webm-duration/fix": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@fix-webm-duration/fix/-/fix-1.0.1.tgz", + "integrity": "sha512-rRN4CpWQaXRbCXYqKIxnsUq8OSWSGq/SVlnxxkx0HxMZZ1u0qnB24P766o8QS5YaMMqAOFAzmsMmfZ2OWucOLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fix-webm-duration/parser": "1.0.1", + "tslib": "^2.3.0" + } + }, + "node_modules/@fix-webm-duration/parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@fix-webm-duration/parser/-/parser-1.0.1.tgz", + "integrity": "sha512-Z6el7ohBBpym4iOmxDxumN715cHzLcAbtOtYfIbvoyToVtAuxvHt3ELXGff83iAXEU1Yj7g+obQBdiKJYVhbWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@heroui/react": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@heroui/react/-/react-3.2.6.tgz", + "integrity": "sha512-KBhUUUdX4571SSjwpuBPEFZTYAw/tmGezTIsTnjIOlXDZkjWACfhFe6ibtReO+xTkUcfxCV1r3cF00/3OPlhhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@heroui/styles": "3.2.6", + "@radix-ui/react-avatar": "1.2.6", + "@react-stately/utils": "3.12.1", + "@react-types/color": "3.2.0", + "@react-types/shared": "3.36.1", + "input-otp": "1.5.0", + "tailwind-variants": "3.3.1" + }, + "peerDependencies": { + "@internationalized/date": "^3.12.4", + "@react-aria/ssr": "^3.10.1", + "@react-aria/utils": "^3.34.1", + "react": ">=19.0.0", + "react-aria": "^3.52.1", + "react-aria-components": "^1.21.1", + "react-dom": ">=19.0.0", + "tailwindcss": ">=4.0.0" + } + }, + "node_modules/@heroui/styles": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@heroui/styles/-/styles-3.2.6.tgz", + "integrity": "sha512-feKZUphVe5+QCXnUe8kpqT5T+pPeMI3LaQcSztx08PGVTkhz0dU0C6crKvA1G4HHIPFXXon68VeL8lPjvGHsWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tailwind-variants": "3.3.1", + "tw-animate-css": "1.4.0" + }, + "peerDependencies": { + "tailwindcss": ">=4.0.0" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.4.tgz", + "integrity": "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.8.tgz", + "integrity": "sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.10.tgz", + "integrity": "sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-aria/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.2.1.tgz", + "integrity": "sha512-v/pZh1yBNeayQ5Kio0xRIX0gBf6uu+KjCRW4QSydTyRpmhvk6sMzHT9i9/vIMdaDHaYHBXlXF+Tp/mvS9CG4Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-aria": "^3.48.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.10.1.tgz", + "integrity": "sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-aria": "^3.48.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.34.1.tgz", + "integrity": "sha512-H6+rGZL+0f58bBNaUMfctEnT+NogqwAk+nHiB8sR3K+YlQ37GTuCijy2U/pPvQtFMS5mURrjZeBH5JNNXsx14A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-aria": "^3.48.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-spectrum/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@react-spectrum/color/-/color-3.2.1.tgz", + "integrity": "sha512-FbB+GaVufsN+obNJmUAe8XGZTeJjEWvEf/k/7nxDxoDCBdg/o6eEPONAZyID5Dj1f8BZKNym8VURxDZx7IIyuQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum": "^3.47.0", + "@swc/helpers": "^0.5.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-spectrum/provider": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-spectrum/provider/-/provider-3.11.1.tgz", + "integrity": "sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@adobe/react-spectrum": "^3.47.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/color": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz", + "integrity": "sha512-v4YlLa/oRQpPa/M9Yq1e0Bl8Z7xzGZdNDAIbfIMZuiJtacaOKriPW+AOgjjT/Ab0fNX2YDbwIYvkiMOJBnTi5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.12.1.tgz", + "integrity": "sha512-NqKfzrknpfwiewx7R2vk1P+CneClInPDsIhw15+jOcUYSEfej0nta4cJywuKQJ2gsPwqX/ojDNixedCve9FWGw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.2.0.tgz", + "integrity": "sha512-beV3vz80nzZ1EuYUM7296Kyi3AHcMrbQw0qub/9yzHWVTKKc5sy/e4dCMKcWL/ArkeAyc7jDOiui190RQ4l0Fw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@react-aria/color": "^3.2.0", + "@react-spectrum/color": "^3.2.0", + "@react-stately/color": "^3.10.0" + }, + "peerDependencies": { + "@react-spectrum/provider": "^3.0.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.36.1.tgz", + "integrity": "sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@solar-icons/react": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solar-icons/react/-/react-2.3.0.tgz", + "integrity": "sha512-ihrnDrdksuX76wsea9i0jlEtln0Vgkzj0TxhIJ2JRWhDSwvX+ItXT3KR30C6ldMmFrEMcbpRCfGxOIHNlOZr0g==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@spectrum-icons/ui": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.2.tgz", + "integrity": "sha512-7IuSHO+Ck8gJUIGIXqDcDIU4R8RHRUZhlfz8AwN1rSdCEvdgp6DWYN0LDTMrWh7TN/H5hrJ7oDO/gUpR3TnBjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum-ui": "1.2.1", + "@babel/runtime": "^7.24.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "@adobe/react-spectrum": "^3.47.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@spectrum-icons/workflow": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.2.tgz", + "integrity": "sha512-UkVg8Kp9rInjfqtG27in9p7RWfN9JFSvZr7ZOpN2n9jkrQ1y88hZpK8dKce8lgVb7jALXtsi2p0rjyaWzO0QsA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum-workflow": "2.3.5", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "@adobe/react-spectrum": "^3.47.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.116.0.tgz", + "integrity": "sha512-Cmosty12gyKGK9N3bQb+lMmuAFev5nmUzaR1AsmZHqKOAGzqX1VQzmp49CNPwOx/pw0H9Qqk4rs9yhwTlKpfDg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.116.0.tgz", + "integrity": "sha512-E+VOc2QDcni/fySqkBFiZhnoB3SGydEdZgFI6/dEAGAHx6yEhB46TN9qb2wXs+E+RSzOBV0R6dasiSlw4xlZAA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", + "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.116.0.tgz", + "integrity": "sha512-kGpVZTDHxFTJS3tu+rU0iTAZ+4U0bcLVjxwCk8f3gRhjw3qdCZjTBlgYvc4kGH2XccmAzbkKwXL/mrNHMGSc+A==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.116.0.tgz", + "integrity": "sha512-MHAnlXxi2s6yiJsZsQMfs2B3RFxeVfQWxerqYhIMqcCQV/FuY3LIeouPEkXw/ah7wUWMLYwempF9MOCUScyddg==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "0.4.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.116.0.tgz", + "integrity": "sha512-6/3hR6vccBP6oGM5B6RfbwZcTCKmQOodd/ZWQdsw8yJsU5zO/a//oBL6yLnmgxcjnHSrelW8rsO7hL5DPybyUQ==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.116.0.tgz", + "integrity": "sha512-YyWmKXt2NspV9iO8FPnlswUFJIRnrLd3oTCb+3ZyYRuKZtBH0xCUDgnUqoyA0fGUxpM/UhfwDjYf/dht/9bp7g==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.116.0", + "@supabase/functions-js": "2.116.0", + "@supabase/postgrest-js": "2.116.0", + "@supabase/realtime-js": "2.116.0", + "@supabase/storage-js": "2.116.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", + "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.17.tgz", + "integrity": "sha512-IwKW5uKL0Zrv5ccUJpjIlqf7ppk2v29l/ZLQxLlwHxljBfnDD9Gxm+hzMkGM0AOAL/21H0pp7cTUYLiiVUGchA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/gif.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/gif.js/-/gif.js-0.2.5.tgz", + "integrity": "sha512-OdDQYh9v7td9ztjaooBSqjUBAyAuui2xwDDmQcyRLd6c9T0iWgkebAoCBEdEEBoZG3ekJE/6UnH63Dzq0S3bvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/events": "*" + } + }, + "node_modules/@types/gradient-parser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@types/gradient-parser/-/gradient-parser-0.1.5.tgz", + "integrity": "sha512-r7K3NkJz3A95WkVVmjs0NcchhHstC2C/VIYNX4JC6tieviUNo774FFeOHjThr3Vw/WCeMP9kAT77MKbIRlO/4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.66", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", + "integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.7.0.tgz", + "integrity": "sha512-/UgCD8VrO79Wv8aBNpjMfsS1pIUfIPURoRn0Ik6tMe5avdZF+vQgl/juJgipcMmH3YS0BD573lCdCHyoi84USg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.6.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.7.0", + "electron-builder-squirrel-windows": "26.7.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", + "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz", + "integrity": "sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", + "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001749", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", + "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/capturekit": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/capturekit/-/capturekit-1.0.13.tgz", + "integrity": "sha512-bSSXB9JDZEkGa06QnBEc+AdghqMZY7tJBD8K63E06xBptrPzHobRLBKHuc4RbzyqWI/eRAnagfHKkONyqamasA==", + "license": "MIT", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.7.0.tgz", + "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.7.0", + "builder-util": "26.4.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dnd-timeline": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dnd-timeline/-/dnd-timeline-2.2.0.tgz", + "integrity": "sha512-bQ/2bm70eA7YeztgdxoSpdpTwPzj8VT2/wTlYrnFpqJ71et7EVJZR35XPZIVzBqSyKK9T/QyUzE26gYck9ldxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/utilities": "^3.2.2", + "resize-observer-polyfill": "^1.5.1" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "43.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.1.0.tgz", + "integrity": "sha512-DPfxpQLd4NL3BJ8DBxYAfmLUKKesF5Rx9dQx5FyczAP8bhOPScjHE48GArVeXu68LlAainuwkmQTQvdZwpIIAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.7.0.tgz", + "integrity": "sha512-LoXbCvSFxLesPneQ/fM7FB4OheIDA2tjqCdUkKlObV5ZKGhYgi5VHPHO/6UUOUodAlg7SrkPx7BZJPby+Vrtbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.7.0", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.7.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.7.0.tgz", + "integrity": "sha512-3EqkQK+q0kGshdPSKEPb2p5F75TENMKu6Fe5aTdeaPfdzFK4Yjp5L0d6S7K8iyvqIsGQ/ei4bnpyX9wt+kVCKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.7.0", + "builder-util": "26.4.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", + "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-updater": { + "version": "6.8.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", + "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.5.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-picker-react": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.16.1.tgz", + "integrity": "sha512-MrPX0tOCfRL3uYI4of/2GRZ7S6qS7YlacKiF78uFH84/C62vcuHE2DZyv5b4ZJMk0e06es1jjB4e31Bb+YSM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-check": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.2.tgz", + "integrity": "sha512-tOzL01LMrDIWPLfvMiGUMH0AjqnOelHQPmgvYkW/aRO4Yaw+pBQqWmyebNzAEbKOigoCN8HkRWUZXFkjmiaXMQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^7.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ffprobe-static": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ffprobe-static/-/ffprobe-static-3.1.0.tgz", + "integrity": "sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", + "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-dom": "^12.43.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gif.js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/gif.js/-/gif.js-0.2.0.tgz", + "integrity": "sha512-bYxCoT8OZKmbxY8RN4qDiYuj4nrQDTzgLRcFVovyona1PTWNePzI4nzOmotnlOFIzTk/ZxAHtv+TfVLiBWj/hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/input-otp": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.5.0.tgz", + "integrity": "sha512-3AcfdW1sNG0FmSA5hHMBXG7jNW5CdcdKs8ln8JOmn6S2SRkoXoJx+2UwC+ZvjflfnOdHJx0SfQ9/YjlxVYLtGQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mediabunny": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.25.1.tgz", + "integrity": "sha512-0Rrd47PMCVJbTPA7IJaXPCupV5/RZ/icgr+a0qExRJAr0n5vB4fsGSo+fdHIehG0CrddXtVRvNZwFtJz709yfA==", + "dev": true, + "license": "MPL-2.0", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/mediabunny/node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", + "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.24", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", + "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mp4box": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.2.0.tgz", + "integrity": "sha512-tE+L7wdhSuwBKZGjUzj03Qzj4lWyOw8pHSPyLnvHTKx92NJGkJls0pcEusUHWEh5gWVBlhdu79STJh4Bubz9mQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.8.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-abi": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", + "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pixi-filters": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.1.5.tgz", + "integrity": "sha512-Ewb/J+kxAbaNN+0/ATJbglAJG+skGJfh7BIDP3ILIDdD6wWk1p0pGa25pVf1T8hGBOQSUNVAmwwJBwkj+cyLLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/gradient-parser": "^0.1.2" + }, + "peerDependencies": { + "pixi.js": ">=8.0.0-0" + } + }, + "node_modules/pixi.js": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz", + "integrity": "sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/css-font-loading-module": "^0.0.12", + "@types/earcut": "^3.0.0", + "@webgpu/types": "^0.1.40", + "@xmldom/xmldom": "^0.8.10", + "earcut": "^3.0.2", + "eventemitter3": "^5.0.1", + "gifuct-js": "^2.1.2", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.1.2", + "tiny-lru": "^11.4.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/re-resizable": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", + "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-aria": { + "version": "3.52.1", + "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.52.1.tgz", + "integrity": "sha512-fdZZruC9/x/joCg0mhKGs5aHpwrXLCSZ4GOJmhhYyiE0ffyEsk9MLFt9LCOCF9tn8ErTD+UDQP4oDUSlZkmpCg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@internationalized/number": "^3.6.8", + "@internationalized/string": "^3.2.10", + "@react-types/shared": "^3.36.1", + "@swc/helpers": "^0.5.0", + "aria-hidden": "^1.2.3", + "clsx": "^2.0.0", + "react-stately": "3.50.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-aria-components": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.21.1.tgz", + "integrity": "sha512-J88WflYY0z+EhLX0ce+GjFlQ3ag3ubIgfUTjpKSrBQBu92Xtl4Ub9o118HItUddtbk1Ido0jzyPy94/klZy1hg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@internationalized/string": "^3.2.10", + "@react-types/shared": "^3.36.1", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "react-aria": "3.52.1", + "react-stately": "3.50.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-resizable-panels": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz", + "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-rnd": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", + "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "re-resizable": "6.11.2", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, + "license": "0BSD" + }, + "node_modules/react-stately": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.50.0.tgz", + "integrity": "sha512-TnckvpDQGU0672wEaLZqdzvhuSeXWjgW6vijMWxiKOxc8CosQUfPGISdcZyfHqjX3XMjEtRxj4w9HumvX4h4mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@internationalized/number": "^3.6.8", + "@internationalized/string": "^3.2.10", + "@react-types/shared": "^3.36.1", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.3.1.tgz", + "integrity": "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tiny-lru": { + "version": "11.4.5", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.5.tgz", + "integrity": "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uiohook-napi": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/uiohook-napi/-/uiohook-napi-1.5.4.tgz", + "integrity": "sha512-7vPVDNwgb6MwTgviA/dnF2MrW0X5xm76fAqaOAC3cEKkswqAZOPw1USu14Sr6383s5qhXegcJaR63CpJOPCNAg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "4.x.x" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-electron": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.28.8.tgz", + "integrity": "sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite-plugin-electron-renderer": "*" + }, + "peerDependenciesMeta": { + "vite-plugin-electron-renderer": { + "optional": true + } + } + }, + "node_modules/vite-plugin-electron-renderer": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", + "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/web-demuxer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/web-demuxer/-/web-demuxer-4.0.0.tgz", + "integrity": "sha512-QFsKe8SNjP6MDtAw2lWfyVmX2wXIpDUT+9p2KHXJb5OPWdhVbjBHcV06tDMXzuU1T6Y1P9TRm9bkeVXEwy0dVw==", + "dev": true, + "engines": { + "node": ">=18", + "pnpm": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } } diff --git a/package.json b/package.json index 24377b9b6..168b09191 100644 --- a/package.json +++ b/package.json @@ -1,57 +1,106 @@ { - "name": "openscreen", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build && electron-builder", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" - }, - "dependencies": { - "@fix-webm-duration/fix": "^1.0.1", - "@pixi/filter-drop-shadow": "^5.2.0", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@uiw/react-color-colorful": "^2.9.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "dnd-timeline": "^2.2.0", - "lucide-react": "^0.545.0", - "mp4-muxer": "^5.2.2", - "mp4box": "^2.2.0", - "pixi.js": "^8.14.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-icons": "^5.5.0", - "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1", - "tailwindcss-animate": "^1.0.7", - "uiohook-napi": "^1.5.4" - }, - "devDependencies": { - "@types/react": "^18.2.64", - "@types/react-dom": "^18.2.21", - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.21", - "electron": "^30.0.1", - "electron-builder": "^24.13.3", - "electron-rebuild": "^3.2.9", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "typescript": "^5.2.2", - "vite": "^5.1.6", - "vite-plugin-electron": "^0.28.6", - "vite-plugin-electron-renderer": "^0.14.5" - }, - "main": "dist-electron/main.js" + "name": "recordly", + "productName": "Recordly", + "description": "A free, creator-focused screen recorder with auto-zoom, cursor effects, backgrounds, annotations, and more - built for polished videos out of the box.", + "author": "webadderall", + "homepage": "https://github.com/webadderallorg/Recordly", + "repository": { + "type": "git", + "url": "https://github.com/webadderallorg/Recordly.git" + }, + "bugs": { + "url": "https://github.com/webadderallorg/Recordly/issues" + }, + "private": true, + "version": "1.4.0", + "type": "module", + "scripts": { + "dev": "vite --config vite.config.ts", + "postinstall": "node scripts/postinstall.mjs", + "build": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder", + "lint": "biome lint .", + "lint:fix": "biome lint --write .", + "format": "biome format --write .", + "format:check": "biome format .", + "preview": "vite preview --config vite.config.ts", + "rebuild:native": "node ./node_modules/@electron/rebuild/lib/cli.js --force --only uiohook-napi", + "build:native-helpers": "node scripts/build-native-helpers.mjs", + "build:whisper-runtime": "node scripts/build-whisper-runtime.mjs", + "build:platform-native-helpers": "npm run build:whisper-runtime && npm run build:native-helpers && npm run build:windows-capture && npm run build:windows-gpu-export && npm run build:nvidia-cuda-compositor && npm run build:cursor-monitor", + "build:windows-gpu-export": "node scripts/build-windows-gpu-export.mjs", + "build:nvidia-cuda-compositor": "node scripts/build-nvidia-cuda-compositor.mjs", + "build:windows-capture": "node scripts/build-windows-capture.mjs", + "build:cursor-monitor": "node scripts/build-cursor-monitor.mjs", + "build:mac": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder --mac", + "build:win": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder --win", + "build:linux": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder --linux", + "i18n:check": "node scripts/i18n-check.mjs", + "benchmark:export-queues": "node scripts/benchmark-export-queues.mjs", + "normalize:electron-main-cjs": "node scripts/normalize-electron-main-cjs.mjs", + "smoke:electron-main-cjs": "node scripts/smoke-electron-main-cjs.mjs", + "smoke:packaged-binaries": "node scripts/smoke-packaged-binaries.mjs", + "verify:macos-distribution": "node scripts/verify-macos-distribution.mjs", + "checksums:release": "node scripts/write-release-checksums.mjs", + "release:create": "node scripts/create-release.mjs", + "test": "vitest --run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "test:ui": "playwright test", + "dev:ui": "vite --mode ui --host 127.0.0.1" + }, + "dependencies": { + "@phosphor-icons/react": "^2.1.10", + "@solar-icons/react": "^2.3.0", + "@supabase/supabase-js": "^2.116.0", + "capturekit": "^1.0.13", + "electron-updater": "^6.8.3", + "ffmpeg-static": "^5.3.0", + "ffprobe-static": "^3.1.0", + "uiohook-napi": "^1.5.4" + }, + "devDependencies": { + "@biomejs/biome": "2.3.13", + "@dnd-kit/core": "^6.3.1", + "@electron/rebuild": "^4.2.0", + "@fix-webm-duration/fix": "^1.0.1", + "@heroui/react": "^3.2.6", + "@heroui/styles": "^3.2.6", + "@playwright/test": "^1.63.0", + "@tailwindcss/postcss": "^4.3.3", + "@types/gif.js": "^0.2.5", + "@types/node": "^25.0.3", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^4.2.1", + "clsx": "^2.1.1", + "dnd-timeline": "^2.2.0", + "electron": "^43.1.0", + "electron-builder": "^26.7.0", + "emoji-picker-react": "^4.16.1", + "fast-check": "^4.5.2", + "gif.js": "^0.2.0", + "mediabunny": "^1.25.1", + "motion": "^12.23.24", + "mp4box": "^2.2.0", + "pixi-filters": "^6.1.5", + "pixi.js": "^8.14.0", + "postcss": "^8.5.6", + "react": "^19.3.0", + "react-aria": "3.52.1", + "react-dom": "^19.3.0", + "react-icons": "^5.5.0", + "react-resizable-panels": "^3.0.6", + "react-rnd": "^10.5.2", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.3.3", + "tailwindcss-animate": "^1.0.7", + "terser": "^5.44.1", + "typescript": "^5.2.2", + "vite": "^5.1.6", + "vite-plugin-electron": "^0.28.6", + "vite-plugin-electron-renderer": "^0.14.5", + "vitest": "^3.2.7", + "web-demuxer": "^4.0.0" + }, + "main": "dist-electron/main.cjs" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..34932abbe --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "@playwright/test"; +export default defineConfig({ + testDir: "./tests/ui", + timeout: 30000, + use: { + baseURL: "http://127.0.0.1:5178", + viewport: { width: 1440, height: 1000 }, + trace: "retain-on-failure", + }, + webServer: { + env: { + RECORDLY_RENDERER_ONLY: "1", + VITE_SUPABASE_URL: "https://auth.recordly.test", + VITE_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_ui_test", + }, + command: "npx vite --host 127.0.0.1 --port 5178 --strictPort", + url: "http://127.0.0.1:5178", + reuseExistingServer: false, + }, +}); diff --git a/postcss.config.cjs b/postcss.config.cjs index 96bb01e7d..2769da7a1 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,6 +1 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file +module.exports = { plugins: { "@tailwindcss/postcss": {} } }; diff --git a/preview.png b/preview.png deleted file mode 100644 index b65ad87c2..000000000 Binary files a/preview.png and /dev/null differ diff --git a/preview2.png b/preview2.png deleted file mode 100644 index a3fd12091..000000000 Binary files a/preview2.png and /dev/null differ diff --git a/preview3.png b/preview3.png deleted file mode 100644 index edc736929..000000000 Binary files a/preview3.png and /dev/null differ diff --git a/public/announcements/placeholder-1.svg b/public/announcements/placeholder-1.svg new file mode 100644 index 000000000..8aa9f2ada --- /dev/null +++ b/public/announcements/placeholder-1.svg @@ -0,0 +1 @@ +ANNOUNCEMENTSSpace for what’s next. diff --git a/public/announcements/placeholder-2.svg b/public/announcements/placeholder-2.svg new file mode 100644 index 000000000..782937857 --- /dev/null +++ b/public/announcements/placeholder-2.svg @@ -0,0 +1 @@ +COMING SOONMore to explore. diff --git a/public/app-icons/recordly-1024.png b/public/app-icons/recordly-1024.png new file mode 100644 index 000000000..c194c3fff Binary files /dev/null and b/public/app-icons/recordly-1024.png differ diff --git a/public/app-icons/recordly-128.png b/public/app-icons/recordly-128.png new file mode 100644 index 000000000..8dc2b50fd Binary files /dev/null and b/public/app-icons/recordly-128.png differ diff --git a/public/app-icons/recordly-16.png b/public/app-icons/recordly-16.png new file mode 100644 index 000000000..66ea3f137 Binary files /dev/null and b/public/app-icons/recordly-16.png differ diff --git a/public/app-icons/recordly-256.png b/public/app-icons/recordly-256.png new file mode 100644 index 000000000..6d6eb25e7 Binary files /dev/null and b/public/app-icons/recordly-256.png differ diff --git a/public/app-icons/recordly-32.png b/public/app-icons/recordly-32.png new file mode 100644 index 000000000..dee1d807a Binary files /dev/null and b/public/app-icons/recordly-32.png differ diff --git a/public/app-icons/recordly-512.png b/public/app-icons/recordly-512.png new file mode 100644 index 000000000..1d1a8fa60 Binary files /dev/null and b/public/app-icons/recordly-512.png differ diff --git a/public/app-icons/recordly-64.png b/public/app-icons/recordly-64.png new file mode 100644 index 000000000..fc3c7a673 Binary files /dev/null and b/public/app-icons/recordly-64.png differ diff --git a/public/app-icons/recordlymac-1024.png b/public/app-icons/recordlymac-1024.png new file mode 100644 index 000000000..c8b9fe951 Binary files /dev/null and b/public/app-icons/recordlymac-1024.png differ diff --git a/public/app-icons/recordlymac-128.png b/public/app-icons/recordlymac-128.png new file mode 100644 index 000000000..31d83516a Binary files /dev/null and b/public/app-icons/recordlymac-128.png differ diff --git a/public/app-icons/recordlymac-16.png b/public/app-icons/recordlymac-16.png new file mode 100644 index 000000000..ea827a787 Binary files /dev/null and b/public/app-icons/recordlymac-16.png differ diff --git a/public/app-icons/recordlymac-256.png b/public/app-icons/recordlymac-256.png new file mode 100644 index 000000000..0d472c863 Binary files /dev/null and b/public/app-icons/recordlymac-256.png differ diff --git a/public/app-icons/recordlymac-32.png b/public/app-icons/recordlymac-32.png new file mode 100644 index 000000000..e4107c884 Binary files /dev/null and b/public/app-icons/recordlymac-32.png differ diff --git a/public/app-icons/recordlymac-512.png b/public/app-icons/recordlymac-512.png new file mode 100644 index 000000000..132874bf8 Binary files /dev/null and b/public/app-icons/recordlymac-512.png differ diff --git a/public/app-icons/recordlymac-64.png b/public/app-icons/recordlymac-64.png new file mode 100644 index 000000000..58945af1c Binary files /dev/null and b/public/app-icons/recordlymac-64.png differ diff --git a/public/auth/README.md b/public/auth/README.md new file mode 100644 index 000000000..dcabd0d65 --- /dev/null +++ b/public/auth/README.md @@ -0,0 +1,8 @@ +# Official provider logos + +Unmodified standalone color logos displayed inside HeroUI buttons; labels and controls belong to Recordly. + +- `google-logo.png`: https://developers.google.com/static/identity/images/g-logo.png — linked by https://developers.google.com/identity/branding-guidelines +- `microsoft-logo.svg`: https://learn.microsoft.com/en-us/entra/identity-platform/media/howto-add-branding-in-apps/ms-symbollockup_mssymbol_19.svg — linked by https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps + +Downloaded September 23, 2026. Trademarks belong to the respective providers. Preserve original colors and aspect ratio. diff --git a/public/auth/google-logo.png b/public/auth/google-logo.png new file mode 100644 index 000000000..cd6d16acd Binary files /dev/null and b/public/auth/google-logo.png differ diff --git a/public/auth/login-banner.png b/public/auth/login-banner.png new file mode 100644 index 000000000..5fc12766f Binary files /dev/null and b/public/auth/login-banner.png differ diff --git a/public/auth/login-side.png b/public/auth/login-side.png new file mode 100644 index 000000000..4f0da37b8 Binary files /dev/null and b/public/auth/login-side.png differ diff --git a/public/auth/microsoft-logo.svg b/public/auth/microsoft-logo.svg new file mode 100644 index 000000000..1f7397648 --- /dev/null +++ b/public/auth/microsoft-logo.svg @@ -0,0 +1 @@ +MS-SymbolLockup \ No newline at end of file diff --git a/public/electron-vite.animate.svg b/public/electron-vite.animate.svg deleted file mode 100644 index ea3e77707..000000000 --- a/public/electron-vite.animate.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/electron-vite.svg b/public/electron-vite.svg deleted file mode 100644 index 8a6aefe65..000000000 --- a/public/electron-vite.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/vite.svg b/public/vite.svg index e7b8dfb1b..ee9fadaf9 100644 --- a/public/vite.svg +++ b/public/vite.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/wallpaper.png b/public/wallpaper.png deleted file mode 100644 index 0e51d83d3..000000000 Binary files a/public/wallpaper.png and /dev/null differ diff --git a/public/wallpapers/bluerays.jpeg b/public/wallpapers/bluerays.jpeg new file mode 100644 index 000000000..a73d3b6bd Binary files /dev/null and b/public/wallpapers/bluerays.jpeg differ diff --git a/public/wallpapers/cherrypop.jpg b/public/wallpapers/cherrypop.jpg new file mode 100644 index 000000000..13a1695ad Binary files /dev/null and b/public/wallpapers/cherrypop.jpg differ diff --git a/public/wallpapers/cityscape.jpg b/public/wallpapers/cityscape.jpg new file mode 100644 index 000000000..2bde6982e Binary files /dev/null and b/public/wallpapers/cityscape.jpg differ diff --git a/public/wallpapers/energy-17.jpg b/public/wallpapers/energy-17.jpg new file mode 100644 index 000000000..713695fbb Binary files /dev/null and b/public/wallpapers/energy-17.jpg differ diff --git a/public/wallpapers/energy-19.jpg b/public/wallpapers/energy-19.jpg new file mode 100644 index 000000000..dfabececa Binary files /dev/null and b/public/wallpapers/energy-19.jpg differ diff --git a/public/wallpapers/farmvalley.jpg b/public/wallpapers/farmvalley.jpg new file mode 100644 index 000000000..69dd795ee Binary files /dev/null and b/public/wallpapers/farmvalley.jpg differ diff --git a/public/wallpapers/glassmorphism-3.jpg b/public/wallpapers/glassmorphism-3.jpg new file mode 100644 index 000000000..986c69a34 Binary files /dev/null and b/public/wallpapers/glassmorphism-3.jpg differ diff --git a/public/wallpapers/glassmorphism-4.jpg b/public/wallpapers/glassmorphism-4.jpg new file mode 100644 index 000000000..1f9aaffde Binary files /dev/null and b/public/wallpapers/glassmorphism-4.jpg differ diff --git a/public/wallpapers/ipad-17-dark.jpg b/public/wallpapers/ipad-17-dark.jpg new file mode 100644 index 000000000..f1782e17f Binary files /dev/null and b/public/wallpapers/ipad-17-dark.jpg differ diff --git a/public/wallpapers/ipad-17-light.jpg b/public/wallpapers/ipad-17-light.jpg new file mode 100644 index 000000000..738ecf990 Binary files /dev/null and b/public/wallpapers/ipad-17-light.jpg differ diff --git a/public/wallpapers/iridescent-9.jpg b/public/wallpapers/iridescent-9.jpg new file mode 100644 index 000000000..b33c7f83c Binary files /dev/null and b/public/wallpapers/iridescent-9.jpg differ diff --git a/public/wallpapers/lemonade.jpeg b/public/wallpapers/lemonade.jpeg new file mode 100644 index 000000000..fc67e2a23 Binary files /dev/null and b/public/wallpapers/lemonade.jpeg differ diff --git a/public/wallpapers/levels.jpg b/public/wallpapers/levels.jpg new file mode 100644 index 000000000..c181df856 Binary files /dev/null and b/public/wallpapers/levels.jpg differ diff --git a/public/wallpapers/luisdelrio.jpg b/public/wallpapers/luisdelrio.jpg new file mode 100644 index 000000000..7b6365b9b Binary files /dev/null and b/public/wallpapers/luisdelrio.jpg differ diff --git a/public/wallpapers/midnight-8.jpg b/public/wallpapers/midnight-8.jpg new file mode 100644 index 000000000..21932e706 Binary files /dev/null and b/public/wallpapers/midnight-8.jpg differ diff --git a/public/wallpapers/mountaintrees.jpg b/public/wallpapers/mountaintrees.jpg new file mode 100644 index 000000000..ae1098fd9 Binary files /dev/null and b/public/wallpapers/mountaintrees.jpg differ diff --git a/public/wallpapers/sequoia-blue-orange.jpg b/public/wallpapers/sequoia-blue-orange.jpg new file mode 100644 index 000000000..904fab1bf Binary files /dev/null and b/public/wallpapers/sequoia-blue-orange.jpg differ diff --git a/public/wallpapers/sequoia-blue.jpg b/public/wallpapers/sequoia-blue.jpg new file mode 100644 index 000000000..a0dfb46d0 Binary files /dev/null and b/public/wallpapers/sequoia-blue.jpg differ diff --git a/public/wallpapers/sonoma-clouds.jpg b/public/wallpapers/sonoma-clouds.jpg new file mode 100644 index 000000000..e60330f13 Binary files /dev/null and b/public/wallpapers/sonoma-clouds.jpg differ diff --git a/public/wallpapers/sonoma-dark.jpg b/public/wallpapers/sonoma-dark.jpg new file mode 100644 index 000000000..e11d78a21 Binary files /dev/null and b/public/wallpapers/sonoma-dark.jpg differ diff --git a/public/wallpapers/sonoma-evening.jpg b/public/wallpapers/sonoma-evening.jpg new file mode 100644 index 000000000..d61e60d6b Binary files /dev/null and b/public/wallpapers/sonoma-evening.jpg differ diff --git a/public/wallpapers/sonoma-horizon.jpg b/public/wallpapers/sonoma-horizon.jpg new file mode 100644 index 000000000..1e237181c Binary files /dev/null and b/public/wallpapers/sonoma-horizon.jpg differ diff --git a/public/wallpapers/sonoma-light.jpg b/public/wallpapers/sonoma-light.jpg new file mode 100644 index 000000000..44ef9efd4 Binary files /dev/null and b/public/wallpapers/sonoma-light.jpg differ diff --git a/public/wallpapers/tahoe-dark.jpg b/public/wallpapers/tahoe-dark.jpg new file mode 100644 index 000000000..e83fce96c Binary files /dev/null and b/public/wallpapers/tahoe-dark.jpg differ diff --git a/public/wallpapers/tahoe-light.jpg b/public/wallpapers/tahoe-light.jpg new file mode 100644 index 000000000..93001c56e Binary files /dev/null and b/public/wallpapers/tahoe-light.jpg differ diff --git a/public/wallpapers/ventura-dark.jpg b/public/wallpapers/ventura-dark.jpg new file mode 100644 index 000000000..09267bccf Binary files /dev/null and b/public/wallpapers/ventura-dark.jpg differ diff --git a/public/wallpapers/ventura.jpg b/public/wallpapers/ventura.jpg new file mode 100644 index 000000000..77e3c5c9e Binary files /dev/null and b/public/wallpapers/ventura.jpg differ diff --git a/public/wallpapers/wallpaper1.jpg b/public/wallpapers/wallpaper1.jpg index d91b53073..98c93cb14 100644 Binary files a/public/wallpapers/wallpaper1.jpg and b/public/wallpapers/wallpaper1.jpg differ diff --git a/public/wallpapers/wallpaper1.png b/public/wallpapers/wallpaper1.png deleted file mode 100644 index 0e51d83d3..000000000 Binary files a/public/wallpapers/wallpaper1.png and /dev/null differ diff --git a/public/wallpapers/wallpaper13.jpg b/public/wallpapers/wallpaper13.jpg new file mode 100644 index 000000000..052f0ff7f Binary files /dev/null and b/public/wallpapers/wallpaper13.jpg differ diff --git a/public/wallpapers/wallpaper15.jpg b/public/wallpapers/wallpaper15.jpg new file mode 100644 index 000000000..5742aab45 Binary files /dev/null and b/public/wallpapers/wallpaper15.jpg differ diff --git a/public/wallpapers/wallpaper4.jpg b/public/wallpapers/wallpaper4.jpg index 3620da9a5..49f281643 100644 Binary files a/public/wallpapers/wallpaper4.jpg and b/public/wallpapers/wallpaper4.jpg differ diff --git a/public/wallpapers/wallpaper5.jpg b/public/wallpapers/wallpaper5.jpg deleted file mode 100644 index dec36ce04..000000000 Binary files a/public/wallpapers/wallpaper5.jpg and /dev/null differ diff --git a/public/wallpapers/wallpaper6.jpg b/public/wallpapers/wallpaper6.jpg deleted file mode 100644 index 23314b220..000000000 Binary files a/public/wallpapers/wallpaper6.jpg and /dev/null differ diff --git a/public/wallpapers/wallpaper7.jpg b/public/wallpapers/wallpaper7.jpg index 6c23515f3..aa9d818d8 100644 Binary files a/public/wallpapers/wallpaper7.jpg and b/public/wallpapers/wallpaper7.jpg differ diff --git a/public/wallpapers/wispysky.mp4 b/public/wallpapers/wispysky.mp4 new file mode 100644 index 000000000..3454d9160 Binary files /dev/null and b/public/wallpapers/wispysky.mp4 differ diff --git a/public/wasm/web-demuxer.wasm b/public/wasm/web-demuxer.wasm new file mode 100755 index 000000000..41d4f7355 Binary files /dev/null and b/public/wasm/web-demuxer.wasm differ diff --git a/recordly.rb b/recordly.rb new file mode 100644 index 000000000..f037c928b --- /dev/null +++ b/recordly.rb @@ -0,0 +1,25 @@ +cask "recordly" do + arch arm: "arm64", intel: "x64" + + version "1.1.14" + sha256 arm: "e669ab7c8bdd4596211937183ee2374545da5482702cab0dfe477c1466422b0f", + intel: "85f5183219de0b656400625797ff9299893bd8fbda8455b0f745878b2c729526" + + url "https://github.com/webadderallorg/Recordly/releases/download/v#{version}/Recordly-#{arch}.dmg" + name "Recordly" + desc "Creator-focused screen recorder with auto-zoom, cursor effects, and more" + homepage "https://github.com/webadderallorg/Recordly" + + livecheck do + url :url + strategy :github_latest + end + + app "Recordly.app" + + zap trash: [ + "~/Library/Application Support/Recordly", + "~/Library/Preferences/dev.recordly.app.plist", + "~/Library/Saved Application State/dev.recordly.app.savedState", + ] +end diff --git a/scripts/benchmark-export-queues.mjs b/scripts/benchmark-export-queues.mjs new file mode 100644 index 000000000..57b030c69 --- /dev/null +++ b/scripts/benchmark-export-queues.mjs @@ -0,0 +1,1051 @@ +import { execFile, spawn } from "node:child_process"; +import { once } from "node:events"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import electron from "electron"; +import ffmpegStatic from "ffmpeg-static"; + +const execFileAsync = promisify(execFile); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, ".."); +const mainEntry = path.join(repoRoot, "dist-electron", "main.cjs"); +const rendererEntry = path.join(repoRoot, "dist", "index.html"); + +const width = parseEvenInteger(process.env.RECORDLY_BENCH_EXPORT_WIDTH ?? "1280", "Width"); +const height = parseEvenInteger(process.env.RECORDLY_BENCH_EXPORT_HEIGHT ?? "720", "Height"); +const frameRate = parsePositiveInteger(process.env.RECORDLY_BENCH_EXPORT_FPS ?? "60", "Frame rate"); +const durationSeconds = parsePositiveInteger( + process.env.RECORDLY_BENCH_EXPORT_DURATION ?? "15", + "Duration", +); +const timeoutMs = parsePositiveInteger( + process.env.RECORDLY_BENCH_EXPORT_TIMEOUT_MS ?? "180000", + "Timeout", +); +const runsPerVariant = parsePositiveInteger(process.env.RECORDLY_BENCH_EXPORT_RUNS ?? "2", "Runs"); +const useNativeExport = process.env.RECORDLY_BENCH_EXPORT_USE_NATIVE === "1"; +const useWebcamOverlay = process.env.RECORDLY_BENCH_EXPORT_ENABLE_WEBCAM === "1"; +const providedInputPath = process.env.RECORDLY_BENCH_EXPORT_INPUT ?? null; +const providedWebcamInputPath = process.env.RECORDLY_BENCH_EXPORT_WEBCAM_INPUT ?? null; +const keepTempArtifacts = process.env.RECORDLY_BENCH_EXPORT_KEEP_TEMP === "1"; +const exportEncodingMode = parseExportEncodingMode( + process.env.RECORDLY_BENCH_EXPORT_ENCODING_MODE ?? null, +); +const exportQuality = parseExportQuality(process.env.RECORDLY_BENCH_EXPORT_QUALITY ?? null); +const exportShadowIntensity = parseExportShadowIntensity( + process.env.RECORDLY_BENCH_EXPORT_SHADOW_INTENSITY ?? null, +); +const webcamWidth = parseEvenInteger( + process.env.RECORDLY_BENCH_EXPORT_WEBCAM_WIDTH ?? "640", + "Webcam width", +); +const webcamHeight = parseEvenInteger( + process.env.RECORDLY_BENCH_EXPORT_WEBCAM_HEIGHT ?? "360", + "Webcam height", +); +const webcamShadowIntensity = parseExportShadowIntensity( + process.env.RECORDLY_BENCH_EXPORT_WEBCAM_SHADOW ?? null, +); +const webcamSize = parseExportWebcamSize(process.env.RECORDLY_BENCH_EXPORT_WEBCAM_SIZE ?? null); +const MODERN_BACKEND_SWEEP = ["auto", "webcodecs", "breeze"]; +const exportPipeline = parseExportPipeline(process.env.RECORDLY_BENCH_EXPORT_PIPELINE ?? null); +const exportBackend = parseExportBackend(process.env.RECORDLY_BENCH_EXPORT_BACKEND ?? null); +const exportRenderBackend = parseRenderBackend( + process.env.RECORDLY_BENCH_EXPORT_RENDER_BACKEND ?? null, +); +const exportBackendList = parseExportBackendList( + process.env.RECORDLY_BENCH_EXPORT_BACKENDS ?? null, +); + +const VARIANT_PRESETS = { + adaptive: { name: "adaptive" }, + baseline: { name: "baseline", maxEncodeQueue: 120, maxDecodeQueue: 10, maxPendingFrames: 24 }, + tuned: { name: "tuned", maxEncodeQueue: 240, maxDecodeQueue: 12, maxPendingFrames: 32 }, +}; + +const variantNameList = parseBenchmarkVariantList( + process.env.RECORDLY_BENCH_EXPORT_VARIANTS ?? null, +); + +const variants = variantNameList + ? variantNameList.map((variantName) => VARIANT_PRESETS[variantName]) + : [VARIANT_PRESETS.baseline, VARIANT_PRESETS.tuned]; + +function collectUniqueStrings(values) { + return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))]; +} + +function parsePositiveInteger(rawValue, label) { + const parsed = Number.parseInt(rawValue, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`${label} must be a positive integer`); + } + + return parsed; +} + +function parseEvenInteger(rawValue, label) { + const parsed = parsePositiveInteger(rawValue, label); + if (parsed % 2 !== 0) { + throw new Error(`${label} must be even`); + } + + return parsed; +} + +function parseExportPipeline(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + if (rawValue === "legacy" || rawValue === "modern") { + return rawValue; + } + + throw new Error("RECORDLY_BENCH_EXPORT_PIPELINE must be 'legacy' or 'modern'"); +} + +function parseExportBackend(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + if (rawValue === "auto" || rawValue === "webcodecs" || rawValue === "breeze") { + return rawValue; + } + + throw new Error("RECORDLY_BENCH_EXPORT_BACKEND must be 'auto', 'webcodecs', or 'breeze'"); +} + +function parseRenderBackend(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + if (rawValue === "webgl" || rawValue === "webgpu") { + return rawValue; + } + + throw new Error("RECORDLY_BENCH_EXPORT_RENDER_BACKEND must be 'webgl' or 'webgpu'"); +} + +function parseExportBackendList(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + if (rawValue === "all") { + return [...MODERN_BACKEND_SWEEP]; + } + + const values = rawValue + .split(",") + .map((value) => value.trim()) + .filter((value) => value.length > 0) + .map((value) => parseExportBackend(value)) + .filter((value) => value !== null); + + if (values.length === 0) { + throw new Error( + "RECORDLY_BENCH_EXPORT_BACKENDS must include at least one of: auto, webcodecs, breeze", + ); + } + + return [...new Set(values)]; +} + +function parseBenchmarkVariantList(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + const values = rawValue + .split(",") + .map((value) => value.trim()) + .filter((value) => value.length > 0); + + if (values.length === 0) { + throw new Error( + "RECORDLY_BENCH_EXPORT_VARIANTS must include at least one of: adaptive, baseline, tuned", + ); + } + + for (const value of values) { + if (!(value in VARIANT_PRESETS)) { + throw new Error( + "RECORDLY_BENCH_EXPORT_VARIANTS must include only: adaptive, baseline, tuned", + ); + } + } + + return [...new Set(values)]; +} + +function parseExportEncodingMode(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + if (rawValue === "fast" || rawValue === "balanced" || rawValue === "quality") { + return rawValue; + } + + throw new Error("RECORDLY_BENCH_EXPORT_ENCODING_MODE must be 'fast', 'balanced', or 'quality'"); +} + +function parseExportQuality(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + if ( + rawValue === "medium" || + rawValue === "good" || + rawValue === "high" || + rawValue === "source" + ) { + return rawValue; + } + + throw new Error("RECORDLY_BENCH_EXPORT_QUALITY must be 'medium', 'good', 'high', or 'source'"); +} + +function parseExportShadowIntensity(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + const parsed = Number.parseFloat(rawValue); + if (!Number.isFinite(parsed) || parsed < 0) { + throw new Error("RECORDLY_BENCH_EXPORT_SHADOW_INTENSITY must be a non-negative number"); + } + + return parsed; +} + +function parseExportWebcamSize(rawValue) { + if (rawValue === null || rawValue === "") { + return null; + } + + const parsed = Number.parseFloat(rawValue); + if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) { + throw new Error("RECORDLY_BENCH_EXPORT_WEBCAM_SIZE must be a number between 0 and 100"); + } + + return parsed; +} + +function summarizeSmokeProgress(progressSamples) { + if (!Array.isArray(progressSamples) || progressSamples.length === 0) { + return null; + } + + const extractingSamples = progressSamples.filter( + (sample) => + sample?.phase === "extracting" && + typeof sample?.currentFrame === "number" && + sample.currentFrame > 1, + ); + const fpsSource = extractingSamples.length > 0 ? extractingSamples : progressSamples; + const renderFpsSamples = fpsSource + .map((sample) => sample?.renderFps) + .filter((value) => typeof value === "number" && Number.isFinite(value)); + const firstSample = progressSamples[0] ?? null; + const lastSample = progressSamples.at(-1) ?? null; + const firstExtractingSample = extractingSamples[0] ?? null; + const lastExtractingSample = extractingSamples.at(-1) ?? null; + + return { + samples: progressSamples.length, + extractingSamples: extractingSamples.length, + firstElapsedMs: typeof firstSample?.elapsedMs === "number" ? firstSample.elapsedMs : null, + lastElapsedMs: typeof lastSample?.elapsedMs === "number" ? lastSample.elapsedMs : null, + firstExtractingElapsedMs: + typeof firstExtractingSample?.elapsedMs === "number" + ? firstExtractingSample.elapsedMs + : null, + lastExtractingElapsedMs: + typeof lastExtractingSample?.elapsedMs === "number" + ? lastExtractingSample.elapsedMs + : null, + firstRenderFps: renderFpsSamples[0] ?? null, + lastRenderFps: renderFpsSamples.at(-1) ?? null, + minRenderFps: renderFpsSamples.length > 0 ? Math.min(...renderFpsSamples) : null, + maxRenderFps: renderFpsSamples.length > 0 ? Math.max(...renderFpsSamples) : null, + }; +} + +async function ensureBuildArtifacts() { + await fs.access(mainEntry); + await fs.access(rendererEntry); +} + +async function createFixtureVideo( + ffmpegPath, + targetPath, + { + fixtureWidth = width, + fixtureHeight = height, + includeAudio = true, + videoFilter = `testsrc2=size=${fixtureWidth}x${fixtureHeight}:rate=${frameRate}`, + } = {}, +) { + const args = ["-y", "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", videoFilter]; + + if (includeAudio) { + args.push( + "-f", + "lavfi", + "-i", + "sine=frequency=880:sample_rate=48000", + "-c:a", + "aac", + "-b:a", + "128k", + ); + } else { + args.push("-an"); + } + + args.push( + "-t", + String(durationSeconds), + "-c:v", + "libx264", + "-preset", + "veryfast", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + targetPath, + ); + + await execFileAsync(ffmpegPath, args, { + timeout: 60_000, + maxBuffer: 20 * 1024 * 1024, + }); +} + +function parseDurationSeconds(ffmpegOutput) { + const match = ffmpegOutput.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/i); + if (!match) { + return null; + } + + return ( + Number.parseInt(match[1], 10) * 3600 + + Number.parseInt(match[2], 10) * 60 + + Number.parseFloat(match[3]) + ); +} + +async function inspectOutput(ffmpegPath, targetPath) { + try { + const { stderr } = await execFileAsync( + ffmpegPath, + ["-hide_banner", "-i", targetPath, "-f", "null", "-"], + { + timeout: 30_000, + maxBuffer: 20 * 1024 * 1024, + }, + ); + return parseDurationSeconds(stderr); + } catch (error) { + return parseDurationSeconds(String(error?.stderr ?? "")); + } +} + +async function readSmokeExportReport(outputPath) { + const reportPath = `${outputPath}.report.json`; + + try { + const reportContent = await fs.readFile(reportPath, "utf8"); + return { + reportPath, + report: JSON.parse(reportContent), + }; + } catch { + return null; + } +} + +function buildBenchmarkRequests() { + if (exportBackendList) { + return exportBackendList.map((backend) => ({ + pipeline: exportPipeline, + backend, + label: backend, + slug: backend, + })); + } + + if (exportBackend) { + return [ + { + pipeline: exportPipeline, + backend: exportBackend, + label: exportBackend, + slug: exportBackend, + }, + ]; + } + + if (exportPipeline === "modern") { + return MODERN_BACKEND_SWEEP.map((backend) => ({ + pipeline: exportPipeline, + backend, + label: backend, + slug: backend, + })); + } + + return [ + { + pipeline: exportPipeline, + backend: null, + label: "default", + slug: "default", + }, + ]; +} + +function formatTableCell(value) { + if (Array.isArray(value)) { + return value.length > 0 ? value.join(", ") : "-"; + } + + if (value === null || value === undefined || value === "") { + return "-"; + } + + return String(value).replace(/\s+/g, " ").trim(); +} + +function printTable(title, columns, rows) { + if (!Array.isArray(rows) || rows.length === 0) { + return; + } + + const formattedRows = rows.map((row) => + columns.map((column) => formatTableCell(column.getValue(row))), + ); + const widths = columns.map((column, columnIndex) => { + const headerWidth = column.header.length; + const rowWidth = Math.max(...formattedRows.map((row) => row[columnIndex].length)); + return Math.max(headerWidth, rowWidth); + }); + const divider = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`; + + console.log(`[benchmark-export-queues] ${title}`); + console.log( + `| ${columns + .map((column, columnIndex) => column.header.padEnd(widths[columnIndex])) + .join(" | ")} |`, + ); + console.log(divider); + for (const row of formattedRows) { + console.log( + `| ${row.map((value, columnIndex) => value.padEnd(widths[columnIndex])).join(" | ")} |`, + ); + } +} + +function formatMs(value) { + return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)} ms` : "-"; +} + +function formatDeltaMs(value) { + if (typeof value !== "number" || !Number.isFinite(value)) { + return "-"; + } + + const roundedValue = Math.round(value); + return `${roundedValue > 0 ? "+" : ""}${roundedValue} ms`; +} + +function formatPercent(value) { + return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(1)}%` : "-"; +} + +function formatSeconds(value) { + return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(2)} s` : "-"; +} + +function formatMegabytes(value) { + return typeof value === "number" && Number.isFinite(value) + ? `${(value / (1024 * 1024)).toFixed(2)} MB` + : "-"; +} + +function formatBoolean(value) { + return value ? "Yes" : "No"; +} + +function calculateDelta(referenceValue, nextValue) { + if ( + typeof referenceValue !== "number" || + !Number.isFinite(referenceValue) || + typeof nextValue !== "number" || + !Number.isFinite(nextValue) + ) { + return { deltaMs: null, deltaPercent: null }; + } + + return { + deltaMs: nextValue - referenceValue, + deltaPercent: + referenceValue > 0 ? ((nextValue - referenceValue) / referenceValue) * 100 : null, + }; +} + +function buildRequestedConfigRows(benchmarkRequests) { + const rows = [ + { key: "Width", value: width }, + { key: "Height", value: height }, + { key: "Frame rate", value: `${frameRate} FPS` }, + { key: "Duration", value: `${durationSeconds} s` }, + { key: "Timeout", value: formatMs(timeoutMs) }, + { key: "Runs per variant", value: runsPerVariant }, + { key: "Pipeline", value: exportPipeline ?? "default" }, + { key: "Requested backends", value: benchmarkRequests.map((request) => request.label) }, + { key: "Render backend", value: exportRenderBackend ?? "default" }, + { key: "Backend sweep", value: formatBoolean(benchmarkRequests.length > 1) }, + { key: "Encoding mode", value: exportEncodingMode ?? "default" }, + { key: "Quality", value: exportQuality ?? "default" }, + { key: "Shadow intensity", value: exportShadowIntensity ?? "default" }, + { key: "Webcam enabled", value: formatBoolean(useWebcamOverlay) }, + { key: "Experimental native override", value: formatBoolean(useNativeExport) }, + ]; + + if (useWebcamOverlay) { + rows.push( + { key: "Webcam width", value: webcamWidth }, + { key: "Webcam height", value: webcamHeight }, + { key: "Webcam shadow", value: webcamShadowIntensity ?? "default" }, + { key: "Webcam size", value: webcamSize ?? "default" }, + ); + } + + return rows; +} + +function printRequestedConfigTable(benchmarkRequests) { + printTable( + "Requested config", + [ + { header: "Setting", getValue: (row) => row.key }, + { header: "Value", getValue: (row) => row.value }, + ], + buildRequestedConfigRows(benchmarkRequests), + ); +} + +function buildTimingTableRows(benchmarkResults) { + return benchmarkResults.flatMap((result) => + result.summaries.map((summary) => ({ + backend: result.request.backend ?? "default", + pipeline: result.request.pipeline ?? "default", + variant: summary.variant.name, + averageElapsedMs: summary.averageElapsedMs, + medianElapsedMs: summary.medianElapsedMs, + averageSmokeElapsedMs: summary.averageSmokeElapsedMs, + minElapsedMs: summary.minElapsedMs, + maxElapsedMs: summary.maxElapsedMs, + averageOutputDurationSeconds: summary.averageOutputDurationSeconds, + averageSizeBytes: summary.averageSizeBytes, + webcamEnabled: summary.webcamEnabled, + })), + ); +} + +function printTimingSummaryTable(benchmarkResults) { + printTable( + "Timing summary", + [ + { header: "Pipeline", getValue: (row) => row.pipeline }, + { header: "Backend", getValue: (row) => row.backend }, + { header: "Variant", getValue: (row) => row.variant }, + { header: "Avg total", getValue: (row) => formatMs(row.averageElapsedMs) }, + { header: "Median total", getValue: (row) => formatMs(row.medianElapsedMs) }, + { header: "Avg export", getValue: (row) => formatMs(row.averageSmokeElapsedMs) }, + { header: "Min", getValue: (row) => formatMs(row.minElapsedMs) }, + { header: "Max", getValue: (row) => formatMs(row.maxElapsedMs) }, + { + header: "Avg output", + getValue: (row) => formatSeconds(row.averageOutputDurationSeconds), + }, + { header: "Avg size", getValue: (row) => formatMegabytes(row.averageSizeBytes) }, + { header: "Webcam", getValue: (row) => formatBoolean(row.webcamEnabled) }, + ], + buildTimingTableRows(benchmarkResults), + ); +} + +function buildBackendDetailTableRows(benchmarkResults) { + return benchmarkResults.flatMap((result) => + result.summaries.map((summary) => ({ + backend: result.request.backend ?? "default", + pipeline: result.request.pipeline ?? "default", + variant: summary.variant.name, + encodeQueue: summary.variant.maxEncodeQueue, + decodeQueue: summary.variant.maxDecodeQueue, + pendingFrames: summary.variant.maxPendingFrames, + observedRenderBackends: summary.observedRenderBackends, + observedEncodeBackends: summary.observedEncodeBackends, + observedEncoders: summary.observedEncoders, + })), + ); +} + +function printBackendDetailTable(benchmarkResults) { + printTable( + "Observed backends", + [ + { header: "Pipeline", getValue: (row) => row.pipeline }, + { header: "Backend", getValue: (row) => row.backend }, + { header: "Variant", getValue: (row) => row.variant }, + { header: "Encode Q", getValue: (row) => row.encodeQueue }, + { header: "Decode Q", getValue: (row) => row.decodeQueue }, + { header: "Pending", getValue: (row) => row.pendingFrames }, + { header: "Render", getValue: (row) => row.observedRenderBackends }, + { header: "Encode", getValue: (row) => row.observedEncodeBackends }, + { header: "Encoder", getValue: (row) => row.observedEncoders }, + ], + buildBackendDetailTableRows(benchmarkResults), + ); +} + +function buildDeltaTableRows(benchmarkResults) { + return benchmarkResults + .map((result) => { + const baseline = result.summaries.find( + (summary) => summary.variant.name === "baseline", + ); + const tuned = result.summaries.find((summary) => summary.variant.name === "tuned"); + if (!baseline || !tuned) { + return null; + } + + const averageDelta = calculateDelta(baseline.averageElapsedMs, tuned.averageElapsedMs); + const medianDelta = calculateDelta(baseline.medianElapsedMs, tuned.medianElapsedMs); + const exportDelta = calculateDelta( + baseline.averageSmokeElapsedMs, + tuned.averageSmokeElapsedMs, + ); + + return { + pipeline: result.request.pipeline ?? "default", + backend: result.request.backend ?? "default", + averageDeltaMs: averageDelta.deltaMs, + averageDeltaPercent: averageDelta.deltaPercent, + medianDeltaMs: medianDelta.deltaMs, + medianDeltaPercent: medianDelta.deltaPercent, + exportDeltaMs: exportDelta.deltaMs, + exportDeltaPercent: exportDelta.deltaPercent, + }; + }) + .filter(Boolean); +} + +function printDeltaTable(benchmarkResults) { + printTable( + "Tuned vs baseline", + [ + { header: "Pipeline", getValue: (row) => row.pipeline }, + { header: "Backend", getValue: (row) => row.backend }, + { + header: "Avg delta", + getValue: (row) => + `${formatDeltaMs(row.averageDeltaMs)} (${formatPercent(row.averageDeltaPercent)})`, + }, + { + header: "Median delta", + getValue: (row) => + `${formatDeltaMs(row.medianDeltaMs)} (${formatPercent(row.medianDeltaPercent)})`, + }, + { + header: "Export delta", + getValue: (row) => + `${formatDeltaMs(row.exportDeltaMs)} (${formatPercent(row.exportDeltaPercent)})`, + }, + ], + buildDeltaTableRows(benchmarkResults), + ); +} + +async function runVariant( + ffmpegPath, + inputPath, + webcamInputPath, + benchmarkRequest, + variant, + runIndex, +) { + const outputPath = path.join( + path.dirname(inputPath), + `${benchmarkRequest.slug}-${variant.name}-${runIndex + 1}-${Date.now()}.mp4`, + ); + const startedAt = performance.now(); + const runLabel = `${benchmarkRequest.label}/${variant.name}#${runIndex + 1}`; + const child = spawn(electron, [repoRoot], { + cwd: repoRoot, + env: { + ...process.env, + RECORDLY_SMOKE_EXPORT: "1", + RECORDLY_SMOKE_EXPORT_INPUT: inputPath, + RECORDLY_SMOKE_EXPORT_OUTPUT: outputPath, + ...(useNativeExport ? { RECORDLY_SMOKE_EXPORT_USE_NATIVE: "1" } : {}), + ...(exportEncodingMode + ? { RECORDLY_SMOKE_EXPORT_ENCODING_MODE: exportEncodingMode } + : {}), + ...(exportQuality ? { RECORDLY_SMOKE_EXPORT_QUALITY: exportQuality } : {}), + ...(exportShadowIntensity !== null + ? { RECORDLY_SMOKE_EXPORT_SHADOW_INTENSITY: String(exportShadowIntensity) } + : {}), + ...(webcamInputPath ? { RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT: webcamInputPath } : {}), + ...(webcamShadowIntensity !== null + ? { RECORDLY_SMOKE_EXPORT_WEBCAM_SHADOW: String(webcamShadowIntensity) } + : {}), + ...(webcamSize !== null + ? { RECORDLY_SMOKE_EXPORT_WEBCAM_SIZE: String(webcamSize) } + : {}), + ...(benchmarkRequest.pipeline + ? { RECORDLY_SMOKE_EXPORT_PIPELINE: benchmarkRequest.pipeline } + : {}), + ...(benchmarkRequest.backend + ? { RECORDLY_SMOKE_EXPORT_BACKEND: benchmarkRequest.backend } + : {}), + ...(exportRenderBackend + ? { RECORDLY_SMOKE_EXPORT_RENDER_BACKEND: exportRenderBackend } + : {}), + ...(typeof variant.maxEncodeQueue === "number" + ? { RECORDLY_SMOKE_EXPORT_MAX_ENCODE_QUEUE: String(variant.maxEncodeQueue) } + : {}), + ...(typeof variant.maxDecodeQueue === "number" + ? { RECORDLY_SMOKE_EXPORT_MAX_DECODE_QUEUE: String(variant.maxDecodeQueue) } + : {}), + ...(typeof variant.maxPendingFrames === "number" + ? { RECORDLY_SMOKE_EXPORT_MAX_PENDING_FRAMES: String(variant.maxPendingFrames) } + : {}), + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + let combinedOutput = ""; + child.stdout.on("data", (chunk) => { + const text = chunk.toString(); + combinedOutput += text; + process.stdout.write(`[${runLabel}] ${text}`); + }); + child.stderr.on("data", (chunk) => { + const text = chunk.toString(); + combinedOutput += text; + process.stderr.write(`[${runLabel}] ${text}`); + }); + + const timeout = setTimeout(() => { + child.kill("SIGKILL"); + }, timeoutMs); + + const [exitCode, signal] = await once(child, "close"); + clearTimeout(timeout); + + if (exitCode !== 0) { + const signalText = signal ? ` (signal ${signal})` : ""; + throw new Error( + `${variant.name} run ${runIndex + 1} failed with code ${exitCode ?? "unknown"}${signalText}\n${combinedOutput.trim()}`, + ); + } + + const smokeExportReport = await readSmokeExportReport(outputPath); + let outputStats; + try { + outputStats = await fs.stat(outputPath); + } catch (error) { + const reportSuffix = smokeExportReport + ? `\n${JSON.stringify(smokeExportReport.report)}` + : ""; + throw new Error( + `${variant.name} run ${runIndex + 1} did not produce an output file: ${error instanceof Error ? error.message : String(error)}${reportSuffix}`, + ); + } + if (outputStats.size <= 0) { + const reportSuffix = smokeExportReport + ? `\n${JSON.stringify(smokeExportReport.report)}` + : ""; + throw new Error( + `${variant.name} run ${runIndex + 1} produced an empty output file${reportSuffix}`, + ); + } + + const elapsedMs = Math.round(performance.now() - startedAt); + const outputDuration = await inspectOutput(ffmpegPath, outputPath); + + return { + elapsedMs, + outputPath, + sizeBytes: outputStats.size, + outputDuration, + webcamEnabled: !!webcamInputPath, + smokeExportReport: smokeExportReport?.report ?? null, + smokeProgressSummary: summarizeSmokeProgress(smokeExportReport?.report?.progressSamples), + }; +} + +async function runBenchmarkRequest(ffmpegPath, inputPath, webcamInputPath, benchmarkRequest) { + const summaries = []; + for (const variant of variants) { + const runs = []; + for (let index = 0; index < runsPerVariant; index += 1) { + console.log( + `[benchmark-export-queues] Running ${benchmarkRequest.label}/${variant.name} (${index + 1}/${runsPerVariant}) with encode=${variant.maxEncodeQueue ?? "auto"} decode=${variant.maxDecodeQueue ?? "auto"} pending=${variant.maxPendingFrames ?? "auto"}`, + ); + runs.push( + await runVariant( + ffmpegPath, + inputPath, + webcamInputPath, + benchmarkRequest, + variant, + index, + ), + ); + } + + const runSummary = summarizeVariantRuns(runs); + summaries.push({ + variant, + runs, + ...runSummary, + webcamEnabled: useWebcamOverlay, + }); + } + + return { + request: benchmarkRequest, + summaries, + }; +} + +function average(values) { + return values.reduce((sum, value) => sum + value, 0) / values.length; +} + +function median(values) { + if (values.length === 0) { + return 0; + } + + const sorted = [...values].sort((left, right) => left - right); + const middleIndex = Math.floor(sorted.length / 2); + if (sorted.length % 2 === 0) { + return (sorted[middleIndex - 1] + sorted[middleIndex]) / 2; + } + + return sorted[middleIndex]; +} + +function summarizeVariantRuns(runs) { + const elapsedValues = runs.map((run) => run.elapsedMs); + const sizeValues = runs.map((run) => run.sizeBytes); + const outputDurationValues = runs + .map((run) => run.outputDuration) + .filter((value) => typeof value === "number" && Number.isFinite(value)); + const smokeElapsedValues = runs + .map((run) => run.smokeExportReport?.elapsedMs) + .filter((value) => typeof value === "number" && Number.isFinite(value)); + + return { + averageElapsedMs: Math.round(average(elapsedValues)), + medianElapsedMs: Math.round(median(elapsedValues)), + minElapsedMs: Math.min(...elapsedValues), + maxElapsedMs: Math.max(...elapsedValues), + averageSizeBytes: Math.round(average(sizeValues)), + averageOutputDurationSeconds: + outputDurationValues.length > 0 ? average(outputDurationValues) : null, + averageSmokeElapsedMs: + smokeElapsedValues.length > 0 ? Math.round(average(smokeElapsedValues)) : null, + observedRenderBackends: collectUniqueStrings( + runs.map((run) => run.smokeExportReport?.metrics?.renderBackend), + ), + observedEncodeBackends: collectUniqueStrings( + runs.map((run) => run.smokeExportReport?.metrics?.encodeBackend), + ), + observedEncoders: collectUniqueStrings( + runs.map((run) => run.smokeExportReport?.metrics?.encoderName), + ), + }; +} + +async function main() { + if (typeof ffmpegStatic !== "string" || ffmpegStatic.length === 0) { + throw new Error("ffmpeg-static is unavailable for this platform"); + } + + if (typeof electron !== "string" || electron.length === 0) { + throw new Error("The Electron binary is unavailable in this workspace"); + } + + await ensureBuildArtifacts(); + const benchmarkRequests = buildBenchmarkRequests(); + + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-export-queue-bench-")); + const inputExtension = providedInputPath ? path.extname(providedInputPath) || ".mp4" : ".mp4"; + const inputPath = path.join(tempDir, `input${inputExtension}`); + const webcamExtension = providedWebcamInputPath + ? path.extname(providedWebcamInputPath) || ".mp4" + : ".mp4"; + const webcamInputPath = useWebcamOverlay + ? path.join(tempDir, `webcam${webcamExtension}`) + : null; + + try { + console.log("[benchmark-export-queues] Config"); + console.log( + JSON.stringify({ + width, + height, + frameRate, + durationSeconds, + timeoutMs, + runsPerVariant, + requestedPipeline: exportPipeline, + requestedBackend: exportBackend, + requestedBackends: benchmarkRequests.map((request) => request.label), + requestedRenderBackend: exportRenderBackend, + backendSweepEnabled: benchmarkRequests.length > 1, + requestedEncodingMode: exportEncodingMode, + requestedQuality: exportQuality, + requestedShadowIntensity: exportShadowIntensity, + webcamEnabled: useWebcamOverlay, + providedInput: providedInputPath, + providedWebcamInput: providedWebcamInputPath, + keepTempArtifacts, + requestedWebcamShadowIntensity: webcamShadowIntensity, + requestedWebcamSize: webcamSize, + }), + ); + printRequestedConfigTable(benchmarkRequests); + + if (providedInputPath) { + console.log( + `[benchmark-export-queues] Using provided input video: ${providedInputPath}`, + ); + await fs.copyFile(providedInputPath, inputPath); + } else { + console.log(`[benchmark-export-queues] Generating fixture video: ${inputPath}`); + await createFixtureVideo(ffmpegStatic, inputPath); + } + if (webcamInputPath) { + if (providedWebcamInputPath) { + console.log( + `[benchmark-export-queues] Using provided webcam video: ${providedWebcamInputPath}`, + ); + await fs.copyFile(providedWebcamInputPath, webcamInputPath); + } else { + console.log( + `[benchmark-export-queues] Generating webcam fixture video: ${webcamInputPath}`, + ); + await createFixtureVideo(ffmpegStatic, webcamInputPath, { + fixtureWidth: webcamWidth, + fixtureHeight: webcamHeight, + includeAudio: false, + videoFilter: `testsrc=size=${webcamWidth}x${webcamHeight}:rate=${frameRate}`, + }); + } + } + + const benchmarkResults = []; + for (const benchmarkRequest of benchmarkRequests) { + benchmarkResults.push( + await runBenchmarkRequest( + ffmpegStatic, + inputPath, + webcamInputPath, + benchmarkRequest, + ), + ); + } + + console.log("[benchmark-export-queues] Summary"); + for (const result of benchmarkResults) { + for (const summary of result.summaries) { + console.log( + JSON.stringify({ + requestedPipeline: result.request.pipeline, + requestedBackend: result.request.backend, + name: summary.variant.name, + webcamEnabled: useWebcamOverlay, + webcamShadowIntensity, + webcamSize, + maxEncodeQueue: summary.variant.maxEncodeQueue, + maxDecodeQueue: summary.variant.maxDecodeQueue, + maxPendingFrames: summary.variant.maxPendingFrames, + averageElapsedMs: summary.averageElapsedMs, + medianElapsedMs: summary.medianElapsedMs, + minElapsedMs: summary.minElapsedMs, + maxElapsedMs: summary.maxElapsedMs, + averageSizeBytes: summary.averageSizeBytes, + averageOutputDurationSeconds: summary.averageOutputDurationSeconds, + averageSmokeElapsedMs: summary.averageSmokeElapsedMs, + observedRenderBackends: summary.observedRenderBackends, + observedEncodeBackends: summary.observedEncodeBackends, + observedEncoders: summary.observedEncoders, + runs: summary.runs.map((run) => ({ + elapsedMs: run.elapsedMs, + sizeBytes: run.sizeBytes, + outputDuration: run.outputDuration, + smokeExportReport: run.smokeExportReport, + smokeProgressSummary: run.smokeProgressSummary, + })), + }), + ); + } + } + printTimingSummaryTable(benchmarkResults); + printBackendDetailTable(benchmarkResults); + printDeltaTable(benchmarkResults); + + for (const result of benchmarkResults) { + if (result.summaries.length < 2) { + continue; + } + + const baseline = result.summaries[0]; + const tuned = result.summaries[1]; + const { deltaMs, deltaPercent: percent } = calculateDelta( + baseline.averageElapsedMs, + tuned.averageElapsedMs, + ); + const { deltaMs: medianDeltaMs, deltaPercent: medianPercent } = calculateDelta( + baseline.medianElapsedMs, + tuned.medianElapsedMs, + ); + const backendLabel = result.request.backend ?? "default"; + console.log( + `[benchmark-export-queues] ${backendLabel} tuned vs baseline: ${deltaMs}ms (${typeof percent === "number" ? percent.toFixed(1) : "-"}%)`, + ); + console.log( + `[benchmark-export-queues] ${backendLabel} tuned vs baseline (median): ${medianDeltaMs}ms (${typeof medianPercent === "number" ? medianPercent.toFixed(1) : "-"}%)`, + ); + } + } finally { + if (keepTempArtifacts) { + console.log(`[benchmark-export-queues] Preserved temp artifacts: ${tempDir}`); + } else { + await fs.rm(tempDir, { recursive: true, force: true }); + } + } +} + +main().catch((error) => { + console.error( + `[benchmark-export-queues] ${error instanceof Error ? error.message : String(error)}`, + ); + process.exitCode = 1; +}); diff --git a/scripts/build-cursor-monitor.mjs b/scripts/build-cursor-monitor.mjs new file mode 100644 index 000000000..37fea5a4e --- /dev/null +++ b/scripts/build-cursor-monitor.mjs @@ -0,0 +1,168 @@ +import { execSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; + +import { + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, +} from "./native-helper-manifest.mjs"; +import { + configureWithWindowsCmakeGenerator, + WINDOWS_VISUAL_STUDIO_INSTALL_DIRS, +} from "./windows-cmake-generators.mjs"; + +const projectRoot = process.cwd(); +const sourceDir = path.join(projectRoot, "electron", "native", "cursor-monitor"); +const buildDir = path.join(sourceDir, "build"); +const bundledDir = path.join( + projectRoot, + "electron", + "native", + "bin", + process.arch === "arm64" ? "win32-arm64" : "win32-x64", +); +const bundledExePath = path.join(bundledDir, "cursor-monitor.exe"); +const helperId = "cursor-monitor"; + +if (process.platform !== "win32") { + console.log("[build-cursor-monitor] Skipping: host platform is not Windows."); + process.exit(0); +} + +if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) { + console.error("[build-cursor-monitor] CMakeLists.txt not found at", sourceDir); + process.exit(1); +} + +function findCmake() { + // Check PATH first + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // not on PATH + } + + const standaloneCmakePaths = [ + path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"), + path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"), + ]; + for (const cmakePath of standaloneCmakePaths) { + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + + // VS 2022 bundled CMake + const vsRoots = [ + path.join("C:", "Program Files", "Microsoft Visual Studio"), + path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"), + ]; + const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"]; + const vsVersions = WINDOWS_VISUAL_STUDIO_INSTALL_DIRS; + for (const root of vsRoots) { + for (const version of vsVersions) { + for (const edition of vsEditions) { + const cmakePath = path.join( + root, + version, + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + } + } + + return null; +} + +const cmake = findCmake(); +if (!cmake) { + if (existsSync(bundledExePath)) { + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "cursor-monitor.exe", + }); + if (!verification.ok) { + console.warn(formatNativeHelperManifestWarning("build-cursor-monitor", verification)); + } + console.log(`[build-cursor-monitor] Using bundled helper: ${bundledExePath}`); + process.exit(0); + } + + console.error( + "[build-cursor-monitor] CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.", + ); + process.exit(1); +} + +mkdirSync(buildDir, { recursive: true }); +const cacheFile = path.join(buildDir, "CMakeCache.txt"); +const cacheDir = path.join(buildDir, "CMakeFiles"); + +function clearCmakeCache() { + rmSync(cacheFile, { force: true }); + rmSync(cacheDir, { recursive: true, force: true }); +} + +console.log("[build-cursor-monitor] Configuring CMake..."); +try { + configureWithWindowsCmakeGenerator({ + prefix: "build-cursor-monitor", + clearCache: clearCmakeCache, + configure: (generator, toolset) => + execSync(`${cmake} .. -G "${generator}" -A x64${toolset ? ` -T ${toolset}` : ""}`, { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }), + }); +} catch (error) { + console.error("[build-cursor-monitor] CMake configure failed:", error.message); + process.exit(1); +} + +console.log("[build-cursor-monitor] Building..."); +try { + execSync(`${cmake} --build . --config Release`, { + cwd: buildDir, + stdio: "inherit", + timeout: 300000, + }); +} catch (error) { + console.error("[build-cursor-monitor] Build failed:", error.message); + process.exit(1); +} + +const exePath = path.join(buildDir, "Release", "cursor-monitor.exe"); +if (existsSync(exePath)) { + console.log(`[build-cursor-monitor] Built successfully: ${exePath}`); + mkdirSync(bundledDir, { recursive: true }); + copyFileSync(exePath, bundledExePath); + console.log(`[build-cursor-monitor] Staged bundled helper: ${bundledExePath}`); + const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "cursor-monitor.exe", + }); + console.log(`[build-cursor-monitor] Updated helper manifest: ${manifestPath}`); +} else { + console.error("[build-cursor-monitor] Expected exe not found at", exePath); + process.exit(1); +} diff --git a/scripts/build-native-helpers.mjs b/scripts/build-native-helpers.mjs new file mode 100644 index 000000000..24d0bf953 --- /dev/null +++ b/scripts/build-native-helpers.mjs @@ -0,0 +1,85 @@ +import { spawnSync } from "node:child_process"; +import { chmod, mkdir } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +const projectRoot = process.cwd(); +const nativeRoot = path.join(projectRoot, "electron", "native"); +const moduleCacheRoot = path.join(os.tmpdir(), "recordly-swift-module-cache"); + +if (process.platform !== "darwin") { + console.log("[build-native-helpers] Skipping: host platform is not macOS."); + process.exit(0); +} + +function getTargetConfigs() { + return [ + { + archTag: "darwin-arm64", + swiftTarget: "arm64-apple-macos14.0", + }, + { + archTag: "darwin-x64", + swiftTarget: "x86_64-apple-macos14.0", + }, + ]; +} + +const helpers = [ + { + source: "ScreenCaptureKitRecorder.swift", + output: "recordly-screencapturekit-helper", + }, + { + source: "ScreenCaptureKitWindowList.swift", + output: "recordly-window-list", + }, + { + source: "SystemCursorAssets.swift", + output: "recordly-system-cursors", + }, + { + source: "NativeCursorMonitor.swift", + output: "recordly-native-cursor-monitor", + }, +]; + +const swiftcCheck = spawnSync("swiftc", ["--version"], { encoding: "utf8" }); +if (swiftcCheck.status !== 0) { + const details = [swiftcCheck.stderr, swiftcCheck.stdout].filter(Boolean).join("\n").trim(); + throw new Error(details || "swiftc is unavailable; install Xcode Command Line Tools."); +} + +for (const target of getTargetConfigs()) { + const outputDir = path.join(nativeRoot, "bin", target.archTag); + await mkdir(outputDir, { recursive: true }); + + for (const helper of helpers) { + const sourcePath = path.join(nativeRoot, helper.source); + const outputPath = path.join(outputDir, helper.output); + + const result = spawnSync( + "swiftc", + ["-O", "-target", target.swiftTarget, sourcePath, "-o", outputPath], + { + encoding: "utf8", + env: { + ...process.env, + CLANG_MODULE_CACHE_PATH: path.join(moduleCacheRoot, "clang"), + SWIFT_MODULECACHE_PATH: path.join(moduleCacheRoot, "swift"), + }, + timeout: 120000, + }, + ); + + if (result.status !== 0) { + const details = [result.stderr, result.stdout].filter(Boolean).join("\n").trim(); + throw new Error(details || `Failed to compile ${helper.source} for ${target.archTag}`); + } + + await chmod(outputPath, 0o755); + console.log( + `[build-native-helpers] Built ${helper.output} (${target.archTag}) -> ${outputPath}`, + ); + } +} diff --git a/scripts/build-nvidia-cuda-compositor.mjs b/scripts/build-nvidia-cuda-compositor.mjs new file mode 100644 index 000000000..f1447c4b7 --- /dev/null +++ b/scripts/build-nvidia-cuda-compositor.mjs @@ -0,0 +1,292 @@ +import { execSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +import { + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, +} from "./native-helper-manifest.mjs"; +import { + configureWithWindowsCmakeGenerator, + WINDOWS_VISUAL_STUDIO_INSTALL_DIRS, +} from "./windows-cmake-generators.mjs"; + +const projectRoot = process.cwd(); +const sourceDir = path.join(projectRoot, "electron", "native", "nvidia-cuda-compositor"); +const buildDir = path.join(sourceDir, "build"); +const bundledDir = path.join( + projectRoot, + "electron", + "native", + "bin", + process.arch === "arm64" ? "win32-arm64" : "win32-x64", +); +const bundledExePath = path.join(bundledDir, "recordly-nvidia-cuda-compositor.exe"); +const helperId = "recordly-nvidia-cuda-compositor"; +const generatorArch = process.arch === "arm64" ? "ARM64" : "x64"; +const videoCodecSdkRoot = + process.env.RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT?.trim() || + path.join(projectRoot, ".tmp", "video-sdk-samples"); + +if (process.platform !== "win32") { + console.log("[build-nvidia-cuda-compositor] Skipping NVIDIA CUDA compositor build."); + process.exit(0); +} + +if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) { + console.error("[build-nvidia-cuda-compositor] CMakeLists.txt not found at", sourceDir); + process.exit(1); +} + +function fallbackToBundledHelperOrExit(reason) { + if (existsSync(bundledExePath)) { + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "recordly-nvidia-cuda-compositor.exe", + }); + if (!verification.ok) { + console.warn( + formatNativeHelperManifestWarning("build-nvidia-cuda-compositor", verification), + ); + } + console.log(`[build-nvidia-cuda-compositor] ${reason}`); + console.log(`[build-nvidia-cuda-compositor] Using bundled helper: ${bundledExePath}`); + process.exit(0); + } + + console.error(`[build-nvidia-cuda-compositor] ${reason}`); + console.error( + "[build-nvidia-cuda-compositor] No bundled helper is available; install CUDA Toolkit + NVIDIA Video Codec SDK or provide a staged helper.", + ); + process.exit(1); +} + +function findCmake() { + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // Continue probing common Windows install locations. + } + + const standaloneCmakePaths = [ + path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"), + path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"), + ]; + for (const cmakePath of standaloneCmakePaths) { + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + + const vsRoots = [ + path.join("C:", "Program Files", "Microsoft Visual Studio"), + path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"), + ]; + const vsEditions = ["Preview", "Community", "Professional", "Enterprise", "BuildTools"]; + const vsVersions = WINDOWS_VISUAL_STUDIO_INSTALL_DIRS; + for (const root of vsRoots) { + for (const version of vsVersions) { + for (const edition of vsEditions) { + const cmakePath = path.join( + root, + version, + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + } + } + + return null; +} + +if (!existsSync(path.join(videoCodecSdkRoot, "Samples", "NvCodec"))) { + fallbackToBundledHelperOrExit( + `NVIDIA Video Codec SDK samples not found at ${videoCodecSdkRoot}. Set RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT to build from source.`, + ); +} + +function replaceOrThrow(filePath, content, pattern, replacement, label) { + const updated = content.replace(pattern, replacement); + if (updated === content) { + throw new Error(`Unable to patch ${label} in ${filePath}`); + } + return updated; +} + +function patchNvDecoderForRecordlyCallbacks() { + const nvDecoderDir = path.join(videoCodecSdkRoot, "Samples", "NvCodec", "NvDecoder"); + const headerPath = path.join(nvDecoderDir, "NvDecoder.h"); + const sourcePath = path.join(nvDecoderDir, "NvDecoder.cpp"); + + let header = readFileSync(headerPath, "utf8"); + if (!header.includes("RecordlyMappedFrameHandler")) { + header = replaceOrThrow( + headerPath, + header, + /#include "nvcuvid\.h"\r?\n/, + `#include "nvcuvid.h" + +using RecordlyMappedFrameHandler = void (*)(CUdeviceptr, unsigned int, int, int, int, int64_t, void*); +using RecordlyDisplayFramePolicy = bool (*)(int, void*); +`, + "NvDecoder callback aliases", + ); + header = replaceOrThrow( + headerPath, + header, + / {4}int setReconfigParams\(const Rect \* pCropRect, const Dim \* pResizeDim\);\r?\n/, + ` int setReconfigParams(const Rect * pCropRect, const Dim * pResizeDim); + void SetMappedFrameHandler(RecordlyMappedFrameHandler handler, void* userData) { m_recordlyMappedFrameHandler = handler; m_recordlyMappedFrameUserData = userData; } + void SetDisplayFramePolicy(RecordlyDisplayFramePolicy policy, void* userData) { m_recordlyDisplayFramePolicy = policy; m_recordlyDisplayFramePolicyUserData = userData; } + int GetDisplayFrameCount() const { return m_nDisplayFrameCount; } +`, + "NvDecoder public callback methods", + ); + header = replaceOrThrow( + headerPath, + header, + / {4}int m_nDecodedFrame = 0, m_nDecodedFrameReturned = 0;\r?\n/, + ` int m_nDecodedFrame = 0, m_nDecodedFrameReturned = 0; + int m_nDisplayFrameCount = 0; + RecordlyMappedFrameHandler m_recordlyMappedFrameHandler = nullptr; + void* m_recordlyMappedFrameUserData = nullptr; + RecordlyDisplayFramePolicy m_recordlyDisplayFramePolicy = nullptr; + void* m_recordlyDisplayFramePolicyUserData = nullptr; +`, + "NvDecoder callback state", + ); + writeFileSync(headerPath, header); + } + + let source = readFileSync(sourcePath, "utf8"); + if (!source.includes("Recordly mapped frame callback")) { + source = replaceOrThrow( + sourcePath, + source, + / {4}if \(result == CUDA_SUCCESS && \(DecodeStatus\.decodeStatus == cuvidDecodeStatus_Error \|\| DecodeStatus\.decodeStatus == cuvidDecodeStatus_Error_Concealed\)\)\r?\n {4}\{\r?\n {8}printf\("Decode Error occurred for picture %d\\n", m_nPicNumInDecodeOrder\[pDispInfo->picture_index\]\);\r?\n {4}\}\r?\n {4}uint8_t \*pDecodedFrame = nullptr;\r?\n/, + ` if (result == CUDA_SUCCESS && (DecodeStatus.decodeStatus == cuvidDecodeStatus_Error || DecodeStatus.decodeStatus == cuvidDecodeStatus_Error_Concealed)) + { + printf("Decode Error occurred for picture %d\\n", m_nPicNumInDecodeOrder[pDispInfo->picture_index]); + } + + const int displayFrameIndex = m_nDisplayFrameCount++; + if (m_recordlyDisplayFramePolicy && + !m_recordlyDisplayFramePolicy(displayFrameIndex, m_recordlyDisplayFramePolicyUserData)) + { + NVDEC_API_CALL(cuvidUnmapVideoFrame(m_hDecoder, dpSrcFrame)); + return 1; + } + + // Recordly mapped frame callback keeps the CUDA helper from making an + // extra device-to-device copy when the caller can consume mapped NV12. + if (m_recordlyMappedFrameHandler) + { + m_recordlyMappedFrameHandler( + dpSrcFrame, + nSrcPitch, + m_nWidth, + m_nHeight, + m_nSurfaceHeight, + pDispInfo->timestamp, + m_recordlyMappedFrameUserData); + NVDEC_API_CALL(cuvidUnmapVideoFrame(m_hDecoder, dpSrcFrame)); + return 1; + } + + uint8_t *pDecodedFrame = nullptr; +`, + "NvDecoder mapped frame callback hook", + ); + writeFileSync(sourcePath, source); + } +} + +try { + patchNvDecoderForRecordlyCallbacks(); +} catch (error) { + fallbackToBundledHelperOrExit( + `Failed to patch NVIDIA Video Codec SDK samples: ${error instanceof Error ? error.message : String(error)}`, + ); +} + +const cmake = findCmake(); +if (!cmake) { + fallbackToBundledHelperOrExit( + "CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.", + ); +} + +mkdirSync(buildDir, { recursive: true }); + +function clearCmakeCache() { + rmSync(path.join(buildDir, "CMakeCache.txt"), { force: true }); + rmSync(path.join(buildDir, "CMakeFiles"), { recursive: true, force: true }); +} + +console.log("[build-nvidia-cuda-compositor] Configuring CMake..."); +try { + configureWithWindowsCmakeGenerator({ + prefix: "build-nvidia-cuda-compositor", + clearCache: clearCmakeCache, + configure: (generator, toolset) => + execSync( + `${cmake} .. -G "${generator}" -A ${generatorArch}${toolset ? ` -T ${toolset}` : ""} -DRECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT="${videoCodecSdkRoot}"`, + { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }, + ), + }); +} catch (error) { + fallbackToBundledHelperOrExit( + `CMake configure failed: ${error instanceof Error ? error.message : String(error)}`, + ); +} + +console.log("[build-nvidia-cuda-compositor] Building NVIDIA CUDA compositor..."); +try { + execSync(`${cmake} --build . --config Release`, { + cwd: buildDir, + stdio: "inherit", + timeout: 300000, + }); +} catch (error) { + fallbackToBundledHelperOrExit( + `Build failed: ${error instanceof Error ? error.message : String(error)}`, + ); +} + +const exePath = path.join(buildDir, "Release", "recordly-nvidia-cuda-compositor.exe"); +if (!existsSync(exePath)) { + console.error("[build-nvidia-cuda-compositor] Expected exe not found at", exePath); + process.exit(1); +} + +mkdirSync(bundledDir, { recursive: true }); +copyFileSync(exePath, bundledExePath); +console.log(`[build-nvidia-cuda-compositor] Staged bundled helper: ${bundledExePath}`); +const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "recordly-nvidia-cuda-compositor.exe", +}); +console.log(`[build-nvidia-cuda-compositor] Updated helper manifest: ${manifestPath}`); diff --git a/scripts/build-whisper-runtime.mjs b/scripts/build-whisper-runtime.mjs new file mode 100644 index 000000000..f1f76f7ab --- /dev/null +++ b/scripts/build-whisper-runtime.mjs @@ -0,0 +1,562 @@ +import { execFileSync, execSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { createWriteStream, existsSync, rmSync } from "node:fs"; +import { chmod, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { get as httpsGet } from "node:https"; +import path from "node:path"; + +import { + configureWithWindowsCmakeGenerator, + WINDOWS_VISUAL_STUDIO_INSTALL_DIRS, +} from "./windows-cmake-generators.mjs"; + +const projectRoot = process.cwd(); +const whisperVersion = "v1.8.4"; +const nativeRoot = path.join(projectRoot, "electron", "native"); +const cacheRoot = path.join(projectRoot, ".tmp", "whisper-runtime"); +const archivePath = path.join(cacheRoot, `${whisperVersion}.tar.gz`); +const extractRoot = path.join(cacheRoot, `src-${whisperVersion}`); +const windowsX64ArchivePath = path.join(cacheRoot, `${whisperVersion}-windows-x64.zip`); +const windowsX64ArchiveSha256 = "74f973345cb52ef5ba3ec9e7e7af8e48cc8c71722d1528603b80588a11f82e3e"; + +function getHostArch() { + return process.arch === "arm64" ? "arm64" : "x64"; +} + +function getNativeArchTag(platform, arch) { + if (platform === "darwin") { + return arch === "arm64" ? "darwin-arm64" : "darwin-x64"; + } + + if (platform === "win32") { + return arch === "arm64" ? "win32-arm64" : "win32-x64"; + } + + if (platform === "linux") { + return arch === "arm64" ? "linux-arm64" : "linux-x64"; + } + + throw new Error(`[build-whisper-runtime] Unsupported platform: ${platform}/${arch}`); +} + +function getRequestedArchitectures(platform) { + const hostArch = getHostArch(); + const configured = process.env.WHISPER_RUNTIME_ARCHS?.trim(); + + if (!configured) { + return [hostArch]; + } + + if (configured === "all") { + return ["arm64", "x64"]; + } + + const supported = new Set(["arm64", "x64"]); + const requested = configured + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); + + if (requested.length === 0) { + return [hostArch]; + } + + const invalid = requested.filter((arch) => !supported.has(arch)); + if (invalid.length > 0) { + throw new Error( + `[build-whisper-runtime] Unsupported ${platform} target architecture request: ${invalid.join(", ")}`, + ); + } + + return [...new Set(requested)]; +} + +function createDarwinTarget(arch) { + const targetArch = arch === "arm64" ? "arm64" : "x64"; + const isCrossCompile = targetArch !== getHostArch(); + const configureArgs = [ + "-DCMAKE_BUILD_TYPE=Release", + `-DCMAKE_OSX_ARCHITECTURES=${targetArch === "arm64" ? "arm64" : "x86_64"}`, + ]; + + if (isCrossCompile) { + configureArgs.push("-DGGML_NATIVE=OFF"); + } + + return { + platform: "darwin", + arch: targetArch, + archTag: getNativeArchTag("darwin", targetArch), + buildRoot: path.join(cacheRoot, `build-darwin-${targetArch}`), + outputDir: path.join(nativeRoot, "bin", getNativeArchTag("darwin", targetArch)), + configureArgs, + }; +} + +function getTargetConfigs() { + if (process.platform === "darwin") { + return getRequestedArchitectures("darwin").map((arch) => createDarwinTarget(arch)); + } + + const arch = getHostArch(); + const archTag = getNativeArchTag(process.platform, arch); + + if (process.platform === "win32") { + return [ + { + platform: "win32", + arch, + archTag, + buildRoot: path.join(cacheRoot, `build-${archTag}`), + outputDir: path.join(nativeRoot, "bin", archTag), + configureArgs: ["-A", arch === "arm64" ? "ARM64" : "x64"], + }, + ]; + } + + if (process.platform === "linux") { + return [ + { + platform: "linux", + arch, + archTag, + buildRoot: path.join(cacheRoot, `build-${archTag}`), + outputDir: path.join(nativeRoot, "bin", archTag), + configureArgs: ["-DCMAKE_BUILD_TYPE=Release"], + }, + ]; + } + + throw new Error( + `[build-whisper-runtime] Unsupported platform: ${process.platform}/${process.arch}`, + ); +} + +function getSourceArchiveUrl() { + return `https://github.com/ggml-org/whisper.cpp/archive/refs/tags/${whisperVersion}.tar.gz`; +} + +function findCmake() { + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // not on PATH + } + + if (process.platform === "win32") { + const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"]; + for (const version of WINDOWS_VISUAL_STUDIO_INSTALL_DIRS) { + for (const edition of vsEditions) { + const cmakePath = path.join( + "C:", + "Program Files", + "Microsoft Visual Studio", + version, + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return cmakePath; + } + } + } + } + + return null; +} + +function ensureTarAvailable() { + try { + execSync("tar --version", { stdio: "pipe" }); + } catch { + throw new Error("[build-whisper-runtime] tar is required to unpack whisper.cpp sources."); + } +} + +async function downloadFile(url, destinationPath) { + await mkdir(path.dirname(destinationPath), { recursive: true }); + + await new Promise((resolve, reject) => { + const request = (currentUrl, redirectCount = 0) => { + const req = httpsGet(currentUrl, (response) => { + const statusCode = response.statusCode ?? 0; + const location = response.headers.location; + + if (statusCode >= 300 && statusCode < 400 && location) { + response.resume(); + if (redirectCount >= 5) { + reject( + new Error( + "[build-whisper-runtime] Too many redirects while downloading whisper.cpp source.", + ), + ); + return; + } + request(new URL(location, currentUrl).toString(), redirectCount + 1); + return; + } + + if (statusCode < 200 || statusCode >= 300) { + response.resume(); + reject( + new Error( + `[build-whisper-runtime] Failed to download whisper.cpp source: HTTP ${statusCode}`, + ), + ); + return; + } + + const fileStream = createWriteStream(destinationPath); + fileStream.on("finish", resolve); + fileStream.on("error", reject); + response.on("error", reject); + response.pipe(fileStream); + }); + + req.on("error", reject); + }; + + request(url); + }); +} + +async function getFileSha256(filePath) { + return createHash("sha256") + .update(await readFile(filePath)) + .digest("hex"); +} + +async function findDirectoryContaining(rootPath, fileName) { + const pending = [rootPath]; + while (pending.length > 0) { + const currentPath = pending.shift(); + const entries = await readdir(currentPath, { withFileTypes: true }); + if (entries.some((entry) => entry.isFile() && entry.name === fileName)) { + return currentPath; + } + for (const entry of entries) { + if (entry.isDirectory()) { + pending.push(path.join(currentPath, entry.name)); + } + } + } + return null; +} + +async function stageWindowsX64PrebuiltRuntime(target) { + if (target.platform !== "win32" || target.arch !== "x64") { + return false; + } + + await mkdir(cacheRoot, { recursive: true }); + const archiveUrl = `https://github.com/ggml-org/whisper.cpp/releases/download/${whisperVersion}/whisper-bin-x64.zip`; + let archiveIsValid = + existsSync(windowsX64ArchivePath) && + (await getFileSha256(windowsX64ArchivePath)) === windowsX64ArchiveSha256; + if (!archiveIsValid) { + await rm(windowsX64ArchivePath, { force: true }); + console.log( + `[build-whisper-runtime] Downloading official whisper.cpp ${whisperVersion} Windows x64 runtime...`, + ); + await downloadFile(archiveUrl, windowsX64ArchivePath); + archiveIsValid = (await getFileSha256(windowsX64ArchivePath)) === windowsX64ArchiveSha256; + } + + if (!archiveIsValid) { + throw new Error( + "[build-whisper-runtime] Windows x64 runtime archive failed its SHA-256 integrity check.", + ); + } + + const prebuiltExtractRoot = path.join(cacheRoot, `prebuilt-${target.archTag}`); + await rm(prebuiltExtractRoot, { recursive: true, force: true }); + await mkdir(prebuiltExtractRoot, { recursive: true }); + execFileSync("tar", ["-xf", windowsX64ArchivePath, "-C", prebuiltExtractRoot], { + stdio: "inherit", + }); + + const runtimeDir = await findDirectoryContaining(prebuiltExtractRoot, "whisper-cli.exe"); + if (!runtimeDir) { + throw new Error( + "[build-whisper-runtime] Official Windows archive did not contain whisper-cli.exe.", + ); + } + + const runtimeEntries = (await readdir(runtimeDir)).filter( + (entry) => /^(whisper|ggml)/i.test(entry) || entry.toLowerCase().endsWith(".dll"), + ); + await stageRuntimeArtifacts(target, runtimeDir, runtimeEntries); + return true; +} + +async function ensureSourceTree() { + const extractedSourceDir = path.join( + extractRoot, + `whisper.cpp-${whisperVersion.replace(/^v/, "")}`, + ); + if (existsSync(path.join(extractedSourceDir, "CMakeLists.txt"))) { + return extractedSourceDir; + } + + await rm(extractRoot, { recursive: true, force: true }); + await mkdir(extractRoot, { recursive: true }); + + if (!existsSync(archivePath)) { + console.log(`[build-whisper-runtime] Downloading whisper.cpp ${whisperVersion} source...`); + await downloadFile(getSourceArchiveUrl(), archivePath); + } + + ensureTarAvailable(); + execFileSync("tar", ["-xzf", archivePath, "-C", extractRoot], { stdio: "inherit" }); + + if (!existsSync(path.join(extractedSourceDir, "CMakeLists.txt"))) { + throw new Error( + `[build-whisper-runtime] Extracted whisper.cpp source not found at ${extractedSourceDir}`, + ); + } + + return extractedSourceDir; +} + +async function shouldSkipBuild(target) { + const manifestPath = path.join(target.outputDir, "whisper-runtime.json"); + if (!existsSync(manifestPath)) { + return false; + } + + try { + const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + const binaryName = target.platform === "win32" ? "whisper-cli.exe" : "whisper-cli"; + const binaryPath = path.join(target.outputDir, binaryName); + return ( + manifest.version === whisperVersion && + manifest.arch === target.arch && + existsSync(binaryPath) + ); + } catch { + return false; + } +} + +function getConfigureArgs(sourceDir, target, generator, toolset) { + const args = [ + "-S", + sourceDir, + "-B", + target.buildRoot, + "-DWHISPER_BUILD_TESTS=OFF", + "-DWHISPER_BUILD_SERVER=OFF", + "-DBUILD_SHARED_LIBS=OFF", + ...(generator ? ["-G", generator] : []), + ...(toolset ? ["-T", toolset] : []), + ...target.configureArgs, + ]; + + return args; +} + +function getBuildArgs(target) { + const args = ["--build", target.buildRoot, "--config", "Release"]; + + if (target.platform !== "win32") { + args.push("--parallel"); + } + + return args; +} + +async function findRuntimeArtifacts(target) { + const candidateDirs = + target.platform === "win32" + ? [path.join(target.buildRoot, "bin", "Release"), path.join(target.buildRoot, "bin")] + : [path.join(target.buildRoot, "bin")]; + + for (const candidateDir of candidateDirs) { + if (!existsSync(candidateDir)) { + continue; + } + + const entries = await readdir(candidateDir); + const runtimeEntries = entries.filter((entry) => + /^(whisper|ggml|libwhisper|libggml)/i.test(entry), + ); + if (runtimeEntries.length > 0) { + return { + candidateDir, + runtimeEntries, + }; + } + } + + throw new Error("[build-whisper-runtime] Built whisper runtime artifacts were not found."); +} + +async function stageRuntimeArtifacts(target, candidateDir, runtimeEntries) { + await mkdir(target.outputDir, { recursive: true }); + + for (const entry of runtimeEntries) { + const sourcePath = path.join(candidateDir, entry); + const destinationPath = path.join(target.outputDir, entry); + const entryStats = await stat(sourcePath); + + if (entryStats.isDirectory()) { + await rm(destinationPath, { recursive: true, force: true }); + await cp(sourcePath, destinationPath, { recursive: true }); + continue; + } + + await cp(sourcePath, destinationPath, { force: true }); + if (target.platform !== "win32") { + await chmod(destinationPath, 0o755).catch(() => undefined); + } + } + + const manifestPath = path.join(target.outputDir, "whisper-runtime.json"); + await writeFile( + manifestPath, + JSON.stringify( + { + version: whisperVersion, + platform: target.platform, + arch: target.arch, + binary: target.platform === "win32" ? "whisper-cli.exe" : "whisper-cli", + }, + null, + 2, + ), + "utf8", + ); +} + +async function main() { + const targets = getTargetConfigs(); + + // Official whisper.cpp releases include a signed, portable Windows x64 + // runtime. Prefer it so developers and packaged builds do not require a full + // Visual Studio C++ installation just to enable captions. + for (const target of targets) { + if (!(await shouldSkipBuild(target))) { + try { + await stageWindowsX64PrebuiltRuntime(target); + } catch (error) { + console.warn( + "[build-whisper-runtime] Failed to stage the official Windows runtime; falling back to a source build:", + error, + ); + } + } + } + + const stagedChecks = await Promise.all(targets.map((target) => shouldSkipBuild(target))); + if (stagedChecks.every(Boolean)) { + console.log( + `[build-whisper-runtime] Whisper runtime ${whisperVersion} is staged for ${targets.map((target) => target.archTag).join(", ")}.`, + ); + return; + } + + const cmake = findCmake(); + + if (!cmake) { + // Soft-fail only when this script runs as part of `npm install`/`npm ci`, + // in CI, or when the developer explicitly opted in. Direct invocations + // (e.g. via `npm run build`, `build:win`, `build:mac`, `build:linux`) + // must still fail loudly so we never ship a release build that is + // missing the whisper runtime and silently ships broken auto-captions. + const isPostinstall = process.env.npm_lifecycle_event === "postinstall"; + const isCI = process.env.CI === "true"; + const allowMissing = process.env.WHISPER_RUNTIME_ALLOW_MISSING === "1"; + const softFailAllowed = isPostinstall || isCI || allowMissing; + + const skipChecks = await Promise.all(targets.map((target) => shouldSkipBuild(target))); + const allTargetsStaged = skipChecks.every(Boolean); + + if (allTargetsStaged) { + console.log( + "[build-whisper-runtime] CMake not found; using bundled whisper runtime artifacts.", + ); + return; + } + + const missing = targets + .filter((_target, index) => !skipChecks[index]) + .map((target) => target.archTag) + .join(", "); + + if (softFailAllowed) { + console.warn( + `[build-whisper-runtime] CMake not found and no bundled runtime is staged for: ${missing}. ` + + "Auto-caption features that rely on whisper.cpp will be unavailable until you install CMake " + + "and rerun `npm run build:whisper-runtime`.", + ); + return; + } + + throw new Error( + `[build-whisper-runtime] CMake is required to stage the whisper runtime for: ${missing}. ` + + "Install CMake and retry, or set WHISPER_RUNTIME_ALLOW_MISSING=1 to build without auto-caption support.", + ); + } + + const sourceDir = await ensureSourceTree(); + + console.log( + `[build-whisper-runtime] Target architectures for ${process.platform}: ${targets.map((target) => target.archTag).join(", ")}`, + ); + + for (const target of targets) { + if (await shouldSkipBuild(target)) { + console.log( + `[build-whisper-runtime] Whisper runtime ${whisperVersion} already staged for ${target.archTag}.`, + ); + continue; + } + + await mkdir(target.buildRoot, { recursive: true }); + + console.log( + `[build-whisper-runtime] Configuring whisper.cpp ${whisperVersion} for ${target.archTag}...`, + ); + if (target.platform === "win32") { + configureWithWindowsCmakeGenerator({ + prefix: "build-whisper-runtime", + clearCache: () => { + rmSync(path.join(target.buildRoot, "CMakeCache.txt"), { force: true }); + rmSync(path.join(target.buildRoot, "CMakeFiles"), { + recursive: true, + force: true, + }); + }, + configure: (generator, toolset) => + execFileSync(cmake, getConfigureArgs(sourceDir, target, generator, toolset), { + stdio: "inherit", + timeout: 300000, + }), + }); + } else { + execFileSync(cmake, getConfigureArgs(sourceDir, target), { + stdio: "inherit", + timeout: 300000, + }); + } + + console.log( + `[build-whisper-runtime] Building bundled whisper runtime for ${target.archTag}...`, + ); + execFileSync(cmake, getBuildArgs(target), { stdio: "inherit", timeout: 900000 }); + + const { candidateDir, runtimeEntries } = await findRuntimeArtifacts(target); + await stageRuntimeArtifacts(target, candidateDir, runtimeEntries); + console.log(`[build-whisper-runtime] Staged whisper runtime -> ${target.outputDir}`); + } +} + +await main(); diff --git a/scripts/build-windows-capture.mjs b/scripts/build-windows-capture.mjs new file mode 100644 index 000000000..14cc83461 --- /dev/null +++ b/scripts/build-windows-capture.mjs @@ -0,0 +1,174 @@ +import { execSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; + +import { + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, +} from "./native-helper-manifest.mjs"; +import { + configureWithWindowsCmakeGenerator, + WINDOWS_VISUAL_STUDIO_INSTALL_DIRS, +} from "./windows-cmake-generators.mjs"; + +const projectRoot = process.cwd(); +const sourceDir = path.join(projectRoot, "electron", "native", "wgc-capture"); +const buildDir = path.join(sourceDir, "build"); +const bundledDir = path.join( + projectRoot, + "electron", + "native", + "bin", + process.arch === "arm64" ? "win32-arm64" : "win32-x64", +); +const bundledExePath = path.join(bundledDir, "wgc-capture.exe"); +const helperId = "wgc-capture"; +const generatorArch = process.arch === "arm64" ? "ARM64" : "x64"; + +if (process.platform !== "win32") { + console.log( + "[build-windows-capture] Skipping native Windows capture build: host platform is not Windows.", + ); + process.exit(0); +} + +if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) { + console.error("[build-windows-capture] CMakeLists.txt not found at", sourceDir); + process.exit(1); +} + +function findCmake() { + // Check PATH first + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // not on PATH + } + + const standaloneCmakePaths = [ + path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"), + path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"), + ]; + for (const cmakePath of standaloneCmakePaths) { + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + + // VS 2022 bundled CMake + const vsRoots = [ + path.join("C:", "Program Files", "Microsoft Visual Studio"), + path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"), + ]; + const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"]; + const vsVersions = WINDOWS_VISUAL_STUDIO_INSTALL_DIRS; + for (const root of vsRoots) { + for (const version of vsVersions) { + for (const edition of vsEditions) { + const cmakePath = path.join( + root, + version, + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + } + } + + return null; +} + +const cmake = findCmake(); +if (!cmake) { + if (existsSync(bundledExePath)) { + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "wgc-capture.exe", + }); + if (!verification.ok) { + console.warn(formatNativeHelperManifestWarning("build-windows-capture", verification)); + } + console.log(`[build-windows-capture] Using bundled helper: ${bundledExePath}`); + process.exit(0); + } + + console.error( + "[build-windows-capture] CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.", + ); + process.exit(1); +} + +mkdirSync(buildDir, { recursive: true }); +const cacheFile = path.join(buildDir, "CMakeCache.txt"); +const cacheDir = path.join(buildDir, "CMakeFiles"); + +function clearCmakeCache() { + rmSync(cacheFile, { force: true }); + rmSync(cacheDir, { recursive: true, force: true }); +} + +console.log("[build-windows-capture] Configuring CMake..."); +try { + configureWithWindowsCmakeGenerator({ + prefix: "build-windows-capture", + clearCache: clearCmakeCache, + configure: (generator, toolset) => + execSync( + `${cmake} .. -G "${generator}" -A ${generatorArch}${toolset ? ` -T ${toolset}` : ""}`, + { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }, + ), + }); +} catch (error) { + console.error("[build-windows-capture] CMake configure failed:", error.message); + process.exit(1); +} + +console.log("[build-windows-capture] Building native Windows capture helper..."); +try { + execSync(`${cmake} --build . --config Release`, { + cwd: buildDir, + stdio: "inherit", + timeout: 300000, + }); +} catch (error) { + console.error("[build-windows-capture] Build failed:", error.message); + process.exit(1); +} + +const exePath = path.join(buildDir, "Release", "wgc-capture.exe"); +if (existsSync(exePath)) { + console.log(`[build-windows-capture] Built successfully: ${exePath}`); + mkdirSync(bundledDir, { recursive: true }); + copyFileSync(exePath, bundledExePath); + console.log(`[build-windows-capture] Staged bundled helper: ${bundledExePath}`); + const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "wgc-capture.exe", + }); + console.log(`[build-windows-capture] Updated helper manifest: ${manifestPath}`); +} else { + console.error("[build-windows-capture] Expected exe not found at", exePath); + process.exit(1); +} diff --git a/scripts/build-windows-gpu-export.mjs b/scripts/build-windows-gpu-export.mjs new file mode 100644 index 000000000..63e756be3 --- /dev/null +++ b/scripts/build-windows-gpu-export.mjs @@ -0,0 +1,170 @@ +import { execSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; + +import { + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, +} from "./native-helper-manifest.mjs"; +import { + configureWithWindowsCmakeGenerator, + WINDOWS_VISUAL_STUDIO_INSTALL_DIRS, +} from "./windows-cmake-generators.mjs"; + +const projectRoot = process.cwd(); +const sourceDir = path.join(projectRoot, "electron", "native", "gpu-export-probe"); +const buildDir = path.join(sourceDir, "build"); +const bundledDir = path.join( + projectRoot, + "electron", + "native", + "bin", + process.arch === "arm64" ? "win32-arm64" : "win32-x64", +); +const bundledExePath = path.join(bundledDir, "recordly-gpu-export.exe"); +const helperId = "recordly-gpu-export"; +const generatorArch = process.arch === "arm64" ? "ARM64" : "x64"; + +if (process.platform !== "win32") { + console.log("[build-windows-gpu-export] Skipping Windows GPU export helper build."); + process.exit(0); +} + +if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) { + console.error("[build-windows-gpu-export] CMakeLists.txt not found at", sourceDir); + process.exit(1); +} + +function findCmake() { + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // Continue probing common Windows install locations. + } + + const standaloneCmakePaths = [ + path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"), + path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"), + ]; + for (const cmakePath of standaloneCmakePaths) { + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + + const vsRoots = [ + path.join("C:", "Program Files", "Microsoft Visual Studio"), + path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"), + ]; + const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"]; + const vsVersions = WINDOWS_VISUAL_STUDIO_INSTALL_DIRS; + for (const root of vsRoots) { + for (const version of vsVersions) { + for (const edition of vsEditions) { + const cmakePath = path.join( + root, + version, + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + } + } + + return null; +} + +const cmake = findCmake(); +if (!cmake) { + if (existsSync(bundledExePath)) { + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "recordly-gpu-export.exe", + }); + if (!verification.ok) { + console.error( + formatNativeHelperManifestWarning("build-windows-gpu-export", verification), + ); + process.exit(1); + } + console.log(`[build-windows-gpu-export] Using bundled helper: ${bundledExePath}`); + process.exit(0); + } + + console.error( + "[build-windows-gpu-export] CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.", + ); + process.exit(1); +} + +mkdirSync(buildDir, { recursive: true }); + +function clearCmakeCache() { + rmSync(path.join(buildDir, "CMakeCache.txt"), { force: true }); + rmSync(path.join(buildDir, "CMakeFiles"), { recursive: true, force: true }); +} + +console.log("[build-windows-gpu-export] Configuring CMake..."); +try { + configureWithWindowsCmakeGenerator({ + prefix: "build-windows-gpu-export", + clearCache: clearCmakeCache, + configure: (generator, toolset) => + execSync( + `${cmake} .. -G "${generator}" -A ${generatorArch}${toolset ? ` -T ${toolset}` : ""}`, + { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }, + ), + }); +} catch (error) { + console.error("[build-windows-gpu-export] CMake configure failed:", error.message); + process.exit(1); +} + +console.log("[build-windows-gpu-export] Building Windows GPU export helper..."); +try { + execSync(`${cmake} --build . --config Release`, { + cwd: buildDir, + stdio: "inherit", + timeout: 300000, + }); +} catch (error) { + console.error("[build-windows-gpu-export] Build failed:", error.message); + process.exit(1); +} + +const exePath = path.join(buildDir, "Release", "gpu-export-probe.exe"); +if (!existsSync(exePath)) { + console.error("[build-windows-gpu-export] Expected exe not found at", exePath); + process.exit(1); +} + +mkdirSync(bundledDir, { recursive: true }); +copyFileSync(exePath, bundledExePath); +console.log(`[build-windows-gpu-export] Staged bundled helper: ${bundledExePath}`); +const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "recordly-gpu-export.exe", +}); +console.log(`[build-windows-gpu-export] Updated helper manifest: ${manifestPath}`); diff --git a/scripts/create-release.mjs b/scripts/create-release.mjs new file mode 100644 index 000000000..25c36c6d2 --- /dev/null +++ b/scripts/create-release.mjs @@ -0,0 +1,123 @@ +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; + +function printUsage() { + console.error(`Usage: + node scripts/create-release.mjs --tag [--title ] [--notes <text> | --notes-file <path>] [--prerelease] [--draft] + +Examples: + node scripts/create-release.mjs --tag v1.2.0-beta.2 --title "v1.2.0 beta-2" --prerelease --notes-file ./release-notes.md + node scripts/create-release.mjs --tag v1.2.0 --notes "Stable release summary" +`); +} + +function parseArgs(argv) { + const parsed = { + tag: "", + title: "", + notes: "", + notesFile: "", + prerelease: false, + draft: false, + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + switch (arg) { + case "--tag": + parsed.tag = argv[++i] ?? ""; + break; + case "--title": + parsed.title = argv[++i] ?? ""; + break; + case "--notes": + parsed.notes = argv[++i] ?? ""; + break; + case "--notes-file": + parsed.notesFile = argv[++i] ?? ""; + break; + case "--prerelease": + parsed.prerelease = true; + break; + case "--draft": + parsed.draft = true; + break; + case "--help": + case "-h": + printUsage(); + process.exit(0); + break; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (!parsed.tag) { + throw new Error("Missing required --tag argument"); + } + + if (parsed.notes && parsed.notesFile) { + throw new Error("Use either --notes or --notes-file, not both"); + } + + return parsed; +} + +function loadNotes({ notes, notesFile }) { + if (notesFile) { + return fs.readFileSync(notesFile, "utf8"); + } + + return notes; +} + +function resolveGhBinary() { + const candidates = [ + process.env.GH_BIN, + "gh", + "/opt/homebrew/bin/gh", + "/usr/local/bin/gh", + ].filter(Boolean); + + for (const candidate of candidates) { + try { + execFileSync(candidate, ["--version"], { stdio: "ignore" }); + return candidate; + } catch { + continue; + } + } + + throw new Error( + "Could not find the GitHub CLI. Install `gh`, add it to PATH, or set GH_BIN to its full path.", + ); +} + +try { + const options = parseArgs(process.argv.slice(2)); + const notes = loadNotes(options).trim(); + const ghBinary = resolveGhBinary(); + const commandArgs = ["release", "create", options.tag, "--verify-tag", "--generate-notes"]; + + if (options.title) { + commandArgs.push("--title", options.title); + } + + if (notes) { + commandArgs.push("--notes", notes); + } + + if (options.prerelease) { + commandArgs.push("--prerelease"); + } + + if (options.draft) { + commandArgs.push("--draft"); + } + + execFileSync(ghBinary, commandArgs, { stdio: "inherit" }); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + printUsage(); + process.exit(1); +} diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs new file mode 100644 index 000000000..bb93a4c67 --- /dev/null +++ b/scripts/i18n-check.mjs @@ -0,0 +1,97 @@ +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const localesDir = path.join(root, "src", "i18n", "locales"); + +const locales = fs + .readdirSync(localesDir) + .filter((entry) => { + const fullPath = path.join(localesDir, entry); + return fs.statSync(fullPath).isDirectory(); + }) + .sort((left, right) => left.localeCompare(right)); + +if (!locales.includes("en")) { + console.error('i18n-check: expected base locale directory "en"'); + process.exit(1); +} + +function loadJson(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`i18n-check: failed to load ${path.relative(root, filePath)}: ${message}`); + } +} + +function collectKeyPaths(obj, prefix = "") { + if (!obj || typeof obj !== "object" || Array.isArray(obj)) { + return prefix ? [prefix] : []; + } + + const keys = Object.keys(obj); + if (keys.length === 0) { + return prefix ? [prefix] : []; + } + + const paths = []; + for (const key of keys) { + const nextPrefix = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + if (value && typeof value === "object" && !Array.isArray(value)) { + paths.push(...collectKeyPaths(value, nextPrefix)); + } else { + paths.push(nextPrefix); + } + } + return paths; +} + +const baseLocaleDir = path.join(localesDir, "en"); +const namespaceFiles = fs + .readdirSync(baseLocaleDir) + .filter((file) => file.endsWith(".json")) + .sort((left, right) => left.localeCompare(right)); + +let hasErrors = false; + +for (const namespaceFile of namespaceFiles) { + const baseData = loadJson(path.join(baseLocaleDir, namespaceFile)); + const baseKeys = new Set(collectKeyPaths(baseData)); + + for (const locale of locales) { + if (locale === "en") continue; + + const localeFile = path.join(localesDir, locale, namespaceFile); + if (!fs.existsSync(localeFile)) { + console.error(`i18n-check: missing namespace file ${locale}/${namespaceFile}`); + hasErrors = true; + continue; + } + + const localeData = loadJson(localeFile); + const localeKeys = new Set(collectKeyPaths(localeData)); + + for (const key of baseKeys) { + if (!localeKeys.has(key)) { + console.error(`i18n-check: missing key ${locale}/${namespaceFile}:${key}`); + hasErrors = true; + } + } + + for (const key of localeKeys) { + if (!baseKeys.has(key)) { + console.error(`i18n-check: extra key ${locale}/${namespaceFile}:${key}`); + hasErrors = true; + } + } + } +} + +if (hasErrors) { + process.exit(1); +} + +console.log("i18n-check: locale files are structurally consistent"); diff --git a/scripts/install-ffmpeg-static.mjs b/scripts/install-ffmpeg-static.mjs new file mode 100644 index 000000000..80fa37731 --- /dev/null +++ b/scripts/install-ffmpeg-static.mjs @@ -0,0 +1,32 @@ +import { spawnSync } from "node:child_process"; + +const maxAttempts = Number.parseInt(process.env.FFMPEG_STATIC_INSTALL_ATTEMPTS ?? "3", 10); +const baseDelayMs = Number.parseInt(process.env.FFMPEG_STATIC_INSTALL_DELAY_MS ?? "2000", 10); + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function main() { + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const result = spawnSync(process.execPath, ["node_modules/ffmpeg-static/install.js"], { + stdio: "inherit", + }); + + if (result.status === 0) { + return; + } + + if (attempt === maxAttempts) { + process.exit(result.status ?? 1); + } + + const delayMs = baseDelayMs * attempt; + console.warn( + `[ffmpeg-static] Install attempt ${attempt}/${maxAttempts} failed; retrying in ${delayMs}ms...`, + ); + await sleep(delayMs); + } +} + +await main(); diff --git a/scripts/launch-recordly-cuda-auto.ps1 b/scripts/launch-recordly-cuda-auto.ps1 new file mode 100644 index 000000000..5ce72ae34 --- /dev/null +++ b/scripts/launch-recordly-cuda-auto.ps1 @@ -0,0 +1,83 @@ +param( + [string]$AppPath, + [string]$CudaScriptPath, + [switch]$CloseExisting, + [switch]$NoDiagnostics, + [switch]$AllowCudaAudio +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") + +if (-not $AppPath) { + $packagedAppPath = Join-Path $repoRoot "release\win-unpacked\Recordly.exe" + $installedAppPath = Join-Path $env:LOCALAPPDATA "Programs\recordly\Recordly.exe" + if (Test-Path $packagedAppPath) { + $AppPath = $packagedAppPath + } elseif (Test-Path $installedAppPath) { + $AppPath = $installedAppPath + } else { + throw "Recordly.exe was not found. Build the Windows package first or pass -AppPath." + } +} + +if (-not $CudaScriptPath) { + $CudaScriptPath = Join-Path $repoRoot "electron\native\nvidia-cuda-compositor\run-mp4-pipeline.mjs" +} + +if (-not (Test-Path $AppPath)) { + throw "Recordly app not found: $AppPath" +} + +if (-not (Test-Path $CudaScriptPath)) { + throw "NVIDIA CUDA/NVENC wrapper script not found: $CudaScriptPath" +} + +$existingRecordly = @(Get-Process -Name "Recordly" -ErrorAction SilentlyContinue) +if ($existingRecordly.Count -gt 0) { + if (-not $CloseExisting) { + Write-Host "Recordly is already running. Close it first, or rerun with -CloseExisting so the CUDA env is inherited by the new app process." + $existingRecordly | Select-Object Id, ProcessName, Path | Format-Table -AutoSize + exit 2 + } + + $existingRecordly | Stop-Process -Force + Start-Sleep -Milliseconds 500 +} + +$env:RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT = "1" +$env:RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT = (Resolve-Path $CudaScriptPath).Path +$env:RECORDLY_NVIDIA_CUDA_EXPORT_HIGH_PRIORITY = "1" +$env:RECORDLY_NVIDIA_CUDA_SAMPLE_GPU = "1" + +if ($AllowCudaAudio) { + $env:RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT = "1" + Remove-Item Env:\RECORDLY_NVIDIA_CUDA_FORCE_VIDEO_ONLY -ErrorAction SilentlyContinue +} else { + Remove-Item Env:\RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT -ErrorAction SilentlyContinue + $env:RECORDLY_NVIDIA_CUDA_FORCE_VIDEO_ONLY = "1" +} + +if ($NoDiagnostics) { + Remove-Item Env:\RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS -ErrorAction SilentlyContinue +} else { + $env:RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS = "1" +} + +$resolvedAppPath = (Resolve-Path $AppPath).Path +$appDirectory = Split-Path $resolvedAppPath -Parent +if ($AllowCudaAudio) { + $cudaAudioMode = "inline CUDA audio candidate; app still requires timestamp-aligned CUDA output" +} else { + $cudaAudioMode = "CUDA video-only, then shared app audio mux" +} + +Write-Host "Launching Recordly with guarded NVIDIA CUDA/NVENC auto export enabled:" +Write-Host " App: $resolvedAppPath" +Write-Host " CUDA wrapper: $env:RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT" +Write-Host " Force CUDA video-only: $($env:RECORDLY_NVIDIA_CUDA_FORCE_VIDEO_ONLY -eq '1')" +Write-Host " CUDA audio exports: $cudaAudioMode" +Write-Host " Diagnostics: $($env:RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS -eq '1')" + +Start-Process -FilePath $resolvedAppPath -WorkingDirectory $appDirectory diff --git a/scripts/macos-distribution-policy.mjs b/scripts/macos-distribution-policy.mjs new file mode 100644 index 000000000..b281288f9 --- /dev/null +++ b/scripts/macos-distribution-policy.mjs @@ -0,0 +1,138 @@ +const TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/; + +const MACH_O_MAGIC_NUMBERS = new Set([ + 0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca, +]); + +export const REQUIRED_MACOS_ENTITLEMENTS = Object.freeze([ + "com.apple.security.cs.allow-jit", + "com.apple.security.cs.allow-unsigned-executable-memory", + "com.apple.security.cs.disable-library-validation", + "com.apple.security.device.audio-input", + "com.apple.security.device.camera", +]); + +const ALLOWED_MACOS_ENTITLEMENTS = new Set(REQUIRED_MACOS_ENTITLEMENTS); + +function readCodeSignValue(details, key) { + const prefix = `${key}=`; + return details + .split(/\r?\n/) + .find((line) => line.startsWith(prefix)) + ?.slice(prefix.length) + .trim(); +} + +export function assertValidAppleTeamId(teamId) { + if (!TEAM_ID_PATTERN.test(teamId)) { + throw new Error("APPLE_TEAM_ID must be exactly 10 uppercase letters or digits"); + } +} + +export function hasMachOMagic(header) { + if (!(header instanceof Uint8Array) || header.byteLength < 4) { + return false; + } + + const view = new DataView(header.buffer, header.byteOffset, header.byteLength); + return MACH_O_MAGIC_NUMBERS.has(view.getUint32(0, false)); +} + +export function collectCodeSigningMetadataErrors(details, expectedTeamId) { + const errors = []; + const authorities = details + .split(/\r?\n/) + .filter((line) => line.startsWith("Authority=")) + .map((line) => line.slice("Authority=".length).trim()); + const identifier = readCodeSignValue(details, "Identifier"); + const teamIdentifier = readCodeSignValue(details, "TeamIdentifier"); + const timestamp = readCodeSignValue(details, "Timestamp"); + const codeDirectory = details.split(/\r?\n/).find((line) => line.startsWith("CodeDirectory ")); + + if (identifier !== "dev.recordly.app") { + errors.push(`unexpected bundle identifier: ${identifier ?? "missing"}`); + } + + if (!authorities[0]?.startsWith("Developer ID Application:")) { + errors.push("the leaf signing authority is not Developer ID Application"); + } + + if (teamIdentifier !== expectedTeamId) { + errors.push(`unexpected TeamIdentifier: ${teamIdentifier ?? "missing"}`); + } + + if (!timestamp || timestamp.toLowerCase() === "none") { + errors.push("secure signing timestamp is missing"); + } + + if (!codeDirectory?.includes("runtime")) { + errors.push("hardened runtime flag is missing"); + } + + return errors; +} + +export function collectEntitlementErrors(entitlements) { + const errors = []; + + for (const entitlement of REQUIRED_MACOS_ENTITLEMENTS) { + if (entitlements[entitlement] !== true) { + errors.push(`required entitlement is missing or disabled: ${entitlement}`); + } + } + + for (const entitlement of Object.keys(entitlements).sort()) { + if (ALLOWED_MACOS_ENTITLEMENTS.has(entitlement)) { + continue; + } + + if ( + entitlement === "com.apple.security.get-task-allow" && + entitlements[entitlement] === true + ) { + errors.push("distribution build must not enable com.apple.security.get-task-allow"); + continue; + } + + errors.push(`unexpected root application entitlement: ${entitlement}`); + } + + return errors; +} + +export function expectedMachOArchitecture(filePath, buildArch) { + const normalizedPath = filePath.replaceAll("\\", "/"); + if (normalizedPath.includes("/darwin-arm64/")) { + return "arm64"; + } + + if (normalizedPath.includes("/darwin-x64/")) { + return "x86_64"; + } + + return buildArch === "arm64" ? "arm64" : "x86_64"; +} + +export function parseLipoArchitectures(output) { + const trimmed = output.trim(); + const architectureList = trimmed.match(/are:\s+(.+)$/i)?.[1]; + if (architectureList) { + return architectureList.trim().split(/\s+/); + } + + const singleArchitecture = trimmed.match(/architecture:\s+([^\s]+)$/i)?.[1]; + return singleArchitecture ? [singleArchitecture] : trimmed.split(/\s+/).filter(Boolean); +} + +export function collectArchitectureErrors(filePath, lipoOutput, buildArch) { + const expectedArchitecture = expectedMachOArchitecture(filePath, buildArch); + const architectures = parseLipoArchitectures(lipoOutput); + + if (architectures.includes(expectedArchitecture)) { + return []; + } + + return [ + `${filePath} does not contain ${expectedArchitecture} (found: ${architectures.join(", ") || "none"})`, + ]; +} diff --git a/scripts/native-helper-manifest.mjs b/scripts/native-helper-manifest.mjs new file mode 100644 index 000000000..0cf96e0f3 --- /dev/null +++ b/scripts/native-helper-manifest.mjs @@ -0,0 +1,188 @@ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const MANIFEST_FILE_NAME = "helpers-manifest.json"; + +function getPlatformArchDir(platform, arch) { + if (platform !== "win32") { + throw new Error(`Unsupported native helper manifest platform: ${platform}`); + } + + return arch === "arm64" ? "win32-arm64" : "win32-x64"; +} + +function hashBuffer(buffer) { + return createHash("sha256").update(buffer).digest("hex"); +} + +function collectSourceFiles(sourceDir, rootDir = sourceDir) { + const entries = readdirSync(sourceDir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + if (entry.name === "build" || entry.name === "bin") { + continue; + } + + const absolutePath = path.join(sourceDir, entry.name); + if (entry.isDirectory()) { + files.push(...collectSourceFiles(absolutePath, rootDir)); + continue; + } + + if (!entry.isFile()) { + continue; + } + + files.push(path.relative(rootDir, absolutePath).replaceAll("\\", "/")); + } + + files.sort((left, right) => left.localeCompare(right)); + return files; +} + +function hashSourceTree(sourceDir) { + const hash = createHash("sha256"); + for (const relativePath of collectSourceFiles(sourceDir)) { + const absolutePath = path.join(sourceDir, relativePath); + hash.update(relativePath); + hash.update("\n"); + hash.update(readFileSync(absolutePath)); + hash.update("\n"); + } + + return hash.digest("hex"); +} + +function hashFile(filePath) { + return hashBuffer(readFileSync(filePath)); +} + +export function getNativeHelperManifestPath({ + projectRoot, + platform = process.platform, + arch = process.arch, +}) { + return path.join( + projectRoot, + "electron", + "native", + "bin", + getPlatformArchDir(platform, arch), + MANIFEST_FILE_NAME, + ); +} + +function readManifest(manifestPath, platform, arch) { + if (!existsSync(manifestPath)) { + return { + version: 1, + platform, + arch, + helpers: {}, + }; + } + + const manifestContent = JSON.parse(readFileSync(manifestPath, "utf8")); + return { + version: 1, + platform, + arch, + helpers: {}, + ...manifestContent, + }; +} + +export function updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath, + binaryName, + platform = process.platform, + arch = process.arch, +}) { + const manifestPath = getNativeHelperManifestPath({ projectRoot, platform, arch }); + const manifestDir = path.dirname(manifestPath); + mkdirSync(manifestDir, { recursive: true }); + + const manifest = readManifest(manifestPath, platform, arch); + manifest.helpers[helperId] = { + binaryName, + binarySha256: hashFile(binaryPath), + sourceDir: path.relative(projectRoot, sourceDir).replaceAll("\\", "/"), + sourceFingerprint: hashSourceTree(sourceDir), + updatedAt: new Date().toISOString(), + }; + + writeFileSync(`${manifestPath}`, `${JSON.stringify(manifest, null, 2)}\n`); + return manifestPath; +} + +export function verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath, + binaryName, + platform = process.platform, + arch = process.arch, +}) { + const manifestPath = getNativeHelperManifestPath({ projectRoot, platform, arch }); + if (!existsSync(manifestPath)) { + return { + ok: false, + manifestPath, + reasons: ["manifest missing"], + }; + } + + if (!existsSync(binaryPath)) { + return { + ok: false, + manifestPath, + reasons: ["bundled helper missing"], + }; + } + + const manifest = readManifest(manifestPath, platform, arch); + const helperManifest = manifest.helpers?.[helperId]; + if (!helperManifest) { + return { + ok: false, + manifestPath, + reasons: [`${helperId} entry missing`], + }; + } + + const reasons = []; + if (helperManifest.binaryName !== binaryName) { + reasons.push( + `expected binary ${binaryName}, found ${helperManifest.binaryName ?? "unknown"}`, + ); + } + + const expectedBinaryHash = helperManifest.binarySha256; + const actualBinaryHash = hashFile(binaryPath); + if (expectedBinaryHash !== actualBinaryHash) { + reasons.push("binary hash mismatch"); + } + + const expectedSourceFingerprint = helperManifest.sourceFingerprint; + const actualSourceFingerprint = hashSourceTree(sourceDir); + if (expectedSourceFingerprint !== actualSourceFingerprint) { + reasons.push("source fingerprint mismatch"); + } + + return { + ok: reasons.length === 0, + manifestPath, + reasons, + }; +} + +export function formatNativeHelperManifestWarning(helperLabel, verificationResult) { + const reasonText = verificationResult.reasons.join(", "); + return `[${helperLabel}] Bundled helper provenance check failed (${reasonText}). Rebuild the helper to refresh ${path.basename(verificationResult.manifestPath)}.`; +} diff --git a/scripts/normalize-electron-main-cjs.mjs b/scripts/normalize-electron-main-cjs.mjs new file mode 100644 index 000000000..082ab4262 --- /dev/null +++ b/scripts/normalize-electron-main-cjs.mjs @@ -0,0 +1,467 @@ +import fs from "node:fs/promises"; +import { pathToFileURL } from "node:url"; + +const mainBundleUrl = new URL("../dist-electron/main.cjs", import.meta.url); +const IMPORT_META_URL_CJS_REPLACEMENT = 'require("node:url").pathToFileURL(__filename).href'; + +function convertNamedImports(namedSpec) { + return namedSpec.replace(/\s+as\s+/g, ": "); +} + +function convertImportLine(line) { + const importFromMatch = line.match( + /^([ \t]*)import\s+([^;\n]+?)\s+from\s+(["'][^"']+["'])\s*;?[ \t]*$/, + ); + if (importFromMatch) { + const [, indent, rawSpec, moduleLiteral] = importFromMatch; + const spec = rawSpec.trim(); + if (spec.startsWith("* as ")) { + return `${indent}const ${spec.slice(5).trim()} = require(${moduleLiteral});`; + } + + if (spec.startsWith("{")) { + return `${indent}const ${convertNamedImports(spec)} = require(${moduleLiteral});`; + } + + const commaIndex = spec.indexOf(","); + if (commaIndex >= 0) { + const defaultName = spec.slice(0, commaIndex).trim(); + const namedSpec = spec.slice(commaIndex + 1).trim(); + return [ + `${indent}const ${defaultName} = require(${moduleLiteral});`, + `${indent}const ${convertNamedImports(namedSpec)} = ${defaultName};`, + ].join("\n"); + } + + return `${indent}const ${spec} = require(${moduleLiteral});`; + } + + const sideEffectImportMatch = line.match(/^([ \t]*)import\s+(["'][^"']+["'])\s*;?[ \t]*$/); + if (sideEffectImportMatch) { + const [, indent, moduleLiteral] = sideEffectImportMatch; + return `${indent}require(${moduleLiteral});`; + } + + if (/^[ \t]*export\s*\{\s*\}\s*;?[ \t]*$/.test(line)) { + return ""; + } + + return null; +} + +function convertNamedExports(namedSpec, indent = "") { + const statements = []; + for (const rawSpecifier of namedSpec.split(",")) { + const specifier = rawSpecifier.trim(); + if (!specifier) { + continue; + } + + const aliasMatch = specifier.match( + /^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/, + ); + const localName = aliasMatch ? aliasMatch[1] : specifier; + const exportName = aliasMatch ? aliasMatch[2] : specifier; + if ( + !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(localName) || + !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(exportName) + ) { + return null; + } + + statements.push(`${indent}exports.${exportName} = ${localName};`); + } + + return statements; +} + +function convertExportLine(line) { + const singleLineMatch = line.match(/^([ \t]*)export\s*\{\s*([^}]*)\s*\}\s*;?[ \t]*$/); + if (singleLineMatch) { + const [, indent, namedSpec] = singleLineMatch; + return convertNamedExports(namedSpec, indent); + } + + const blockStartMatch = line.match(/^([ \t]*)export\s*\{\s*$/); + if (blockStartMatch) { + return { indent: blockStartMatch[1], specifiers: [], rawLines: [line] }; + } + + return null; +} + +function updateLexicalState(line, state) { + let mode = state.mode; + let escaped = false; + + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + const next = line[index + 1]; + + if (mode === "block-comment") { + if (char === "*" && next === "/") { + mode = null; + index += 1; + } + continue; + } + + if (mode === "single-quote" || mode === "double-quote" || mode === "template") { + if (escaped) { + escaped = false; + continue; + } + + if (char === "\\") { + escaped = true; + continue; + } + + if ( + (mode === "single-quote" && char === "'") || + (mode === "double-quote" && char === '"') || + (mode === "template" && char === "`") + ) { + mode = null; + } + continue; + } + + if (char === "/" && next === "/") { + break; + } + + if (char === "/" && next === "*") { + mode = "block-comment"; + index += 1; + continue; + } + + if (char === "'") { + mode = "single-quote"; + } else if (char === '"') { + mode = "double-quote"; + } else if (char === "`") { + mode = "template"; + } + } + + if (mode === "single-quote" || mode === "double-quote") { + mode = null; + } + + return { mode }; +} + +function hasTokenBoundary(line, startIndex, endIndex) { + const before = line[startIndex - 1]; + const after = line[endIndex]; + const isIdentifier = (char) => Boolean(char && /[A-Za-z0-9_$]/.test(char)); + return !isIdentifier(before) && !isIdentifier(after); +} + +function replaceImportMetaUrlInCode(line, state) { + const token = "import.meta.url"; + let mode = state.mode; + let escaped = false; + let changed = false; + let normalizedLine = ""; + + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + const next = line[index + 1]; + + if (mode === "block-comment") { + normalizedLine += char; + if (char === "*" && next === "/") { + normalizedLine += next; + mode = null; + index += 1; + } + continue; + } + + if (mode === "single-quote" || mode === "double-quote" || mode === "template") { + normalizedLine += char; + if (escaped) { + escaped = false; + continue; + } + + if (char === "\\") { + escaped = true; + continue; + } + + if ( + (mode === "single-quote" && char === "'") || + (mode === "double-quote" && char === '"') || + (mode === "template" && char === "`") + ) { + mode = null; + } + continue; + } + + if (char === "/" && next === "/") { + normalizedLine += line.slice(index); + break; + } + + if (char === "/" && next === "*") { + normalizedLine += char + next; + mode = "block-comment"; + index += 1; + continue; + } + + if (char === "'") { + normalizedLine += char; + mode = "single-quote"; + continue; + } + + if (char === '"') { + normalizedLine += char; + mode = "double-quote"; + continue; + } + + if (char === "`") { + normalizedLine += char; + mode = "template"; + continue; + } + + if (line.startsWith(token, index) && hasTokenBoundary(line, index, index + token.length)) { + normalizedLine += IMPORT_META_URL_CJS_REPLACEMENT; + changed = true; + index += token.length - 1; + continue; + } + + normalizedLine += char; + } + + if (mode === "single-quote" || mode === "double-quote") { + mode = null; + } + + return { + line: normalizedLine, + state: { mode }, + changed, + }; +} + +function containsImportMetaInCode(line, state) { + const token = "import.meta"; + let mode = state.mode; + let escaped = false; + + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + const next = line[index + 1]; + + if (mode === "block-comment") { + if (char === "*" && next === "/") { + mode = null; + index += 1; + } + continue; + } + + if (mode === "single-quote" || mode === "double-quote" || mode === "template") { + if (escaped) { + escaped = false; + continue; + } + + if (char === "\\") { + escaped = true; + continue; + } + + if ( + (mode === "single-quote" && char === "'") || + (mode === "double-quote" && char === '"') || + (mode === "template" && char === "`") + ) { + mode = null; + } + continue; + } + + if (char === "/" && next === "/") { + break; + } + + if (char === "/" && next === "*") { + mode = "block-comment"; + index += 1; + continue; + } + + if (char === "'") { + mode = "single-quote"; + continue; + } + + if (char === '"') { + mode = "double-quote"; + continue; + } + + if (char === "`") { + mode = "template"; + continue; + } + + if (line.startsWith(token, index) && hasTokenBoundary(line, index, index + token.length)) { + return true; + } + } + + return false; +} + +export function normalizeElectronMainCjsSource(source) { + let changed = false; + const lineBreak = source.includes("\r\n") ? "\r\n" : "\n"; + const lines = source.split(/\r?\n/); + const normalizedLines = []; + let state = { mode: null }; + let exportBlock = null; + + for (const line of lines) { + if (exportBlock) { + if (/^[ \t]*\}\s*;?[ \t]*$/.test(line)) { + const statements = convertNamedExports( + exportBlock.specifiers.join(","), + exportBlock.indent, + ); + if (statements === null) { + normalizedLines.push(...exportBlock.rawLines, line); + } else { + normalizedLines.push(...statements); + changed = true; + } + exportBlock = null; + continue; + } + + exportBlock.specifiers.push(line.trim().replace(/,$/, "")); + exportBlock.rawLines.push(line); + continue; + } + + if (state.mode === null) { + const converted = convertImportLine(line); + if (converted !== null) { + normalizedLines.push(converted); + changed = true; + continue; + } + + const convertedExport = convertExportLine(line); + if (convertedExport !== null) { + if (Array.isArray(convertedExport)) { + normalizedLines.push(...convertedExport); + changed = true; + } else { + exportBlock = convertedExport; + } + continue; + } + } + + const normalized = replaceImportMetaUrlInCode(line, state); + if (normalized.changed) { + changed = true; + } + + normalizedLines.push(normalized.line); + state = normalized.state; + } + + if (!changed) { + return { source, changed }; + } + + const normalized = normalizedLines.join(lineBreak); + + return { source: normalized, changed }; +} + +export function findElectronMainCjsEsmSyntax(source) { + const lines = source.split(/\r?\n/); + const matches = []; + let state = { mode: null }; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (state.mode === null) { + const converted = convertImportLine(line); + if (converted !== null) { + matches.push({ + line: index + 1, + text: line.trim(), + }); + continue; + } + if (containsImportMetaInCode(line, state)) { + matches.push({ + line: index + 1, + text: line.trim(), + }); + continue; + } + if (/^[ \t]*export\b/.test(line)) { + matches.push({ + line: index + 1, + text: line.trim(), + }); + continue; + } + } + + state = updateLexicalState(line, state); + } + + return matches; +} + +export async function normalizeElectronMainCjs(bundleUrl = mainBundleUrl) { + let source; + try { + source = await fs.readFile(bundleUrl, "utf8"); + } catch (error) { + throw new Error(`Unable to read dist-electron/main.cjs: ${error}`); + } + + const normalized = normalizeElectronMainCjsSource(source); + if (normalized.changed) { + await fs.writeFile(bundleUrl, normalized.source, "utf8"); + } + + return normalized; +} + +const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; + +if (isDirectRun) { + const result = await normalizeElectronMainCjs(); + const remainingImports = findElectronMainCjsEsmSyntax(result.source); + if (remainingImports.length > 0) { + const details = remainingImports + .map((match) => `line ${match.line}: ${match.text}`) + .join("\n"); + throw new Error(`dist-electron/main.cjs still contains ESM import syntax:\n${details}`); + } + + console.log( + result.changed + ? "Electron main CJS normalized: dist-electron/main.cjs" + : "Electron main CJS already normalized: dist-electron/main.cjs", + ); +} diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs new file mode 100644 index 000000000..b0715a831 --- /dev/null +++ b/scripts/postinstall.mjs @@ -0,0 +1,49 @@ +import { spawnSync } from "node:child_process"; + +const npmExecPath = process.env.npm_execpath; +const hasNpmExecPath = typeof npmExecPath === "string" && npmExecPath.length > 0; +const npmInvoker = hasNpmExecPath + ? { + command: process.execPath, + argsPrefix: [npmExecPath], + shell: false, + } + : { + command: process.platform === "win32" ? "npm.cmd" : "npm", + argsPrefix: [], + shell: process.platform === "win32", + }; + +function runScript(scriptName) { + console.log(`[postinstall] Running npm script: ${scriptName}`); + const result = spawnSync(npmInvoker.command, [...npmInvoker.argsPrefix, "run", scriptName], { + stdio: "inherit", + env: process.env, + shell: npmInvoker.shell, + }); + + if (result.error) { + console.error(`[postinstall] Failed to start "${scriptName}" (${result.error.message}).`); + return false; + } + + if (result.signal) { + console.error(`[postinstall] "${scriptName}" was terminated by signal ${result.signal}.`); + return false; + } + + if (result.status !== 0) { + console.error(`[postinstall] "${scriptName}" exited with code ${result.status}.`); + return false; + } + + return true; +} + +if (!runScript("rebuild:native")) { + process.exit(1); +} + +if (!runScript("build:platform-native-helpers")) { + process.exit(1); +} diff --git a/scripts/smoke-electron-main-cjs.mjs b/scripts/smoke-electron-main-cjs.mjs new file mode 100644 index 000000000..f0e3457ff --- /dev/null +++ b/scripts/smoke-electron-main-cjs.mjs @@ -0,0 +1,30 @@ +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { + findElectronMainCjsEsmSyntax, + normalizeElectronMainCjs, +} from "./normalize-electron-main-cjs.mjs"; + +const mainBundleUrl = new URL("../dist-electron/main.cjs", import.meta.url); +const mainBundlePath = fileURLToPath(mainBundleUrl); + +const { source } = await normalizeElectronMainCjs(mainBundleUrl); +const matches = findElectronMainCjsEsmSyntax(source); + +if (matches.length > 0) { + const details = matches.map((match) => `line ${match.line}: ${match.text}`).join("\n"); + throw new Error(`dist-electron/main.cjs contains ESM import syntax:\n${details}`); +} + +const checkResult = spawnSync(process.execPath, ["--check", mainBundlePath], { + encoding: "utf8", +}); + +if (checkResult.status !== 0) { + const details = [checkResult.stdout, checkResult.stderr].filter(Boolean).join("\n"); + throw new Error( + `dist-electron/main.cjs does not parse as CommonJS:${details ? `\n${details}` : ""}`, + ); +} + +console.log(`Electron main CJS smoke passed: ${mainBundlePath}`); diff --git a/scripts/smoke-packaged-binaries.mjs b/scripts/smoke-packaged-binaries.mjs new file mode 100644 index 000000000..eeb01c17f --- /dev/null +++ b/scripts/smoke-packaged-binaries.mjs @@ -0,0 +1,282 @@ +import { execFileSync } from "node:child_process"; +import { accessSync, constants, existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; + +const projectRoot = process.cwd(); +const releaseRoot = path.join(projectRoot, "release"); +const packageJson = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8")); +const productName = packageJson.productName ?? packageJson.name ?? "Recordly"; +const packageName = packageJson.name ?? "recordly"; + +function relativePath(filePath) { + return path.relative(projectRoot, filePath).replaceAll("\\", "/"); +} + +function fail(message) { + throw new Error(`[packaged-smoke] ${message}`); +} + +function assertFile(filePath, label, { executable = false } = {}) { + if (!existsSync(filePath)) { + fail(`${label} is missing at ${relativePath(filePath)}`); + } + + if (!statSync(filePath).isFile()) { + fail(`${label} is not a file at ${relativePath(filePath)}`); + } + + if (executable && process.platform !== "win32") { + try { + accessSync(filePath, constants.X_OK); + } catch { + fail(`${label} is not executable at ${relativePath(filePath)}`); + } + } + + console.log(`[packaged-smoke] ${label}: ${relativePath(filePath)}`); +} + +function findDirectoriesByName(rootDir, directoryName, maxDepth = 8) { + if (!existsSync(rootDir)) { + return []; + } + + const matches = []; + const queue = [{ dir: rootDir, depth: 0 }]; + + while (queue.length > 0) { + const current = queue.shift(); + if (!current || current.depth > maxDepth) { + continue; + } + + for (const entry of readdirSync(current.dir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + + const childDir = path.join(current.dir, entry.name); + if (entry.name === directoryName) { + matches.push(childDir); + continue; + } + + queue.push({ dir: childDir, depth: current.depth + 1 }); + } + } + + return matches; +} + +function findAppBundleDir(startDir) { + let current = startDir; + while (current !== path.dirname(current)) { + if (current.endsWith(".app")) { + return current; + } + current = path.dirname(current); + } + + return null; +} + +function findFirstExistingFile(candidates) { + return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile()); +} + +function assertPackagedAppExecutable(unpackedRoot) { + const resourcesDir = path.dirname(unpackedRoot); + + if (process.platform === "darwin") { + const appBundleDir = findAppBundleDir(resourcesDir); + if (!appBundleDir) { + fail(`macOS app bundle not found for ${relativePath(unpackedRoot)}`); + } + + const executablePath = findFirstExistingFile([ + path.join(appBundleDir, "Contents", "MacOS", productName), + path.join(appBundleDir, "Contents", "MacOS", packageName), + ]); + assertFile( + executablePath ?? path.join(appBundleDir, "Contents", "MacOS", productName), + "packaged app executable", + { executable: true }, + ); + return; + } + + const appDir = path.dirname(resourcesDir); + const executableCandidates = + process.platform === "win32" + ? [ + path.join(appDir, `${productName}.exe`), + path.join(appDir, `${packageName}.exe`), + path.join(appDir, "Recordly.exe"), + ] + : [ + path.join(appDir, packageName), + path.join(appDir, productName), + path.join(appDir, productName.toLowerCase()), + ]; + + const executablePath = findFirstExistingFile(executableCandidates); + assertFile(executablePath ?? executableCandidates[0], "packaged app executable", { + executable: true, + }); +} + +function getNativeArchTag(platform = process.platform, arch = process.arch) { + if (platform === "darwin") { + return arch === "arm64" ? "darwin-arm64" : "darwin-x64"; + } + + if (platform === "win32") { + return arch === "arm64" ? "win32-arm64" : "win32-x64"; + } + + if (platform === "linux") { + return arch === "arm64" ? "linux-arm64" : "linux-x64"; + } + + return `${platform}-${arch}`; +} + +function getRequiredArchTags() { + const configured = process.env.PACKAGED_SMOKE_ARCH_TAGS?.trim(); + if (!configured) { + return [getNativeArchTag()]; + } + + return [ + ...new Set( + configured + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean), + ), + ]; +} + +function getExpectedNativeHelperFiles(archTag) { + if (archTag.startsWith("win32-")) { + const helpers = [ + { name: "wgc-capture.exe", label: "Windows capture helper", executable: true }, + { + name: "cursor-monitor.exe", + label: "Windows cursor monitor helper", + executable: true, + }, + { + name: "recordly-gpu-export.exe", + label: "Windows GPU export helper", + executable: true, + }, + { name: "helpers-manifest.json", label: "Windows helper manifest" }, + { name: "whisper-cli.exe", label: "Whisper CLI runtime", executable: true }, + { name: "whisper-runtime.json", label: "Whisper runtime manifest" }, + ]; + if (archTag === "win32-x64") { + helpers.push({ + name: "recordly-nvidia-cuda-compositor.exe", + label: "NVIDIA CUDA compositor helper", + executable: true, + }); + } + return helpers; + } + + if (archTag.startsWith("darwin-")) { + return [ + { + name: "recordly-screencapturekit-helper", + label: "ScreenCaptureKit helper", + executable: true, + }, + { name: "recordly-window-list", label: "Window list helper", executable: true }, + { name: "recordly-system-cursors", label: "System cursor helper", executable: true }, + { + name: "recordly-native-cursor-monitor", + label: "Native cursor monitor helper", + executable: true, + }, + { name: "whisper-cli", label: "Whisper CLI runtime", executable: true }, + { name: "whisper-runtime.json", label: "Whisper runtime manifest" }, + ]; + } + + if (archTag.startsWith("linux-")) { + return [ + { name: "whisper-cli", label: "Whisper CLI runtime", executable: true }, + { name: "whisper-runtime.json", label: "Whisper runtime manifest" }, + ]; + } + + return []; +} + +function verifyFfmpeg(unpackedRoot) { + const binaryName = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg"; + const ffmpegPath = path.join(unpackedRoot, "node_modules", "ffmpeg-static", binaryName); + + assertFile(ffmpegPath, "packaged FFmpeg binary", { executable: true }); + + const output = execFileSync(ffmpegPath, ["-version"], { + encoding: "utf8", + timeout: 15000, + windowsHide: true, + }); + + if (!output.startsWith("ffmpeg version")) { + fail(`FFmpeg version smoke returned unexpected output from ${relativePath(ffmpegPath)}`); + } + + console.log(output.split(/\r?\n/, 1)[0]); +} + +function verifyNativeHelpers(unpackedRoot) { + const nativeBinRoot = path.join(unpackedRoot, "electron", "native", "bin"); + if (!existsSync(nativeBinRoot)) { + fail(`native helper bin directory is missing at ${relativePath(nativeBinRoot)}`); + } + + for (const archTag of getRequiredArchTags()) { + const archDir = path.join(nativeBinRoot, archTag); + if (!existsSync(archDir)) { + fail(`native helper arch directory is missing at ${relativePath(archDir)}`); + } + + const expectedFiles = getExpectedNativeHelperFiles(archTag); + if (expectedFiles.length === 0) { + fail(`no packaged helper expectations are defined for ${archTag}`); + } + + for (const expectedFile of expectedFiles) { + assertFile( + path.join(archDir, expectedFile.name), + `${expectedFile.label} (${archTag})`, + { + executable: expectedFile.executable, + }, + ); + } + } +} + +const unpackedRoots = findDirectoriesByName(releaseRoot, "app.asar.unpacked"); + +if (unpackedRoots.length === 0) { + fail("no packaged app.asar.unpacked directory found under release/"); +} + +console.log( + `[packaged-smoke] verifying ${unpackedRoots.length} packaged app root(s) for ${process.platform}/${process.arch}`, +); + +for (const unpackedRoot of unpackedRoots) { + console.log(`[packaged-smoke] root: ${relativePath(unpackedRoot)}`); + assertPackagedAppExecutable(unpackedRoot); + verifyFfmpeg(unpackedRoot); + verifyNativeHelpers(unpackedRoot); +} + +console.log("[packaged-smoke] packaged binary path smoke passed"); diff --git a/scripts/verify-macos-distribution.mjs b/scripts/verify-macos-distribution.mjs new file mode 100644 index 000000000..bd8e4bbcf --- /dev/null +++ b/scripts/verify-macos-distribution.mjs @@ -0,0 +1,491 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import { + closeSync, + existsSync, + lstatSync, + mkdtempSync, + openSync, + readdirSync, + readFileSync, + readSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + assertValidAppleTeamId, + collectArchitectureErrors, + collectCodeSigningMetadataErrors, + collectEntitlementErrors, + hasMachOMagic, +} from "./macos-distribution-policy.mjs"; + +const projectRoot = process.cwd(); +const packageJson = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8")); +const productName = packageJson.productName ?? packageJson.name ?? "Recordly"; +const expectedBundleId = "dev.recordly.app"; +const commandTimeoutMs = 5 * 60 * 1000; +const maxReportDetailLength = 4_000; + +function parseArguments(argv) { + const options = {}; + for (let index = 0; index < argv.length; index += 2) { + const key = argv[index]; + const value = argv[index + 1]; + if (!key?.startsWith("--") || value === undefined) { + throw new Error(`Invalid argument near ${key ?? "end of input"}`); + } + options[key.slice(2)] = value; + } + + const arch = options.arch; + if (arch !== "x64" && arch !== "arm64") { + throw new Error("--arch must be x64 or arm64"); + } + + if (!options["team-id"]) { + throw new Error("--team-id is required"); + } + assertValidAppleTeamId(options["team-id"]); + + return { + arch, + releaseDir: path.resolve(options["release-dir"] ?? "release"), + reportPath: path.resolve( + options.report ?? `release/macos-distribution-report-${arch}.json`, + ), + summaryPath: options.summary ? path.resolve(options.summary) : null, + teamId: options["team-id"], + }; +} + +function formatCommand(command, args) { + return [command, ...args] + .map((part) => (/^[\w./:=@+-]+$/.test(part) ? part : JSON.stringify(part))) + .join(" "); +} + +function runProcess(command, args, { timeout = commandTimeoutMs } = {}) { + const result = spawnSync(command, args, { + cwd: projectRoot, + encoding: "utf8", + timeout, + }); + const stdout = result.stdout?.trim() ?? ""; + const stderr = result.stderr?.trim() ?? ""; + const output = [stdout, stderr].filter(Boolean).join("\n"); + + if (result.error) { + throw new Error(`${formatCommand(command, args)} failed: ${result.error.message}`); + } + + if (result.status !== 0) { + throw new Error( + `${formatCommand(command, args)} exited with ${result.status}${output ? `\n${output}` : ""}`, + ); + } + + return { output, stderr, stdout }; +} + +function assertPolicy(errors, label) { + if (errors.length > 0) { + throw new Error(`${label}:\n- ${errors.join("\n- ")}`); + } +} + +function assertFile(filePath, label) { + if (!existsSync(filePath) || !statSync(filePath).isFile()) { + throw new Error(`${label} is missing: ${filePath}`); + } + + if (statSync(filePath).size === 0) { + throw new Error(`${label} is empty: ${filePath}`); + } +} + +function findAppBundles(rootPath) { + if (!existsSync(rootPath)) { + return []; + } + + const matches = []; + const queue = [rootPath]; + while (queue.length > 0) { + const currentPath = queue.shift(); + if (!currentPath) { + continue; + } + + for (const entry of readdirSync(currentPath, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.isSymbolicLink()) { + continue; + } + + const childPath = path.join(currentPath, entry.name); + if (entry.name === `${productName}.app`) { + matches.push(childPath); + continue; + } + + queue.push(childPath); + } + } + + return matches; +} + +function findSingleAppBundle(rootPath, label) { + const appBundles = findAppBundles(rootPath); + if (appBundles.length !== 1) { + throw new Error( + `${label} must contain exactly one ${productName}.app; found ${appBundles.length}`, + ); + } + return appBundles[0]; +} + +function walkRegularFiles(rootPath) { + const files = []; + const queue = [rootPath]; + while (queue.length > 0) { + const currentPath = queue.shift(); + if (!currentPath) { + continue; + } + + for (const entry of readdirSync(currentPath, { withFileTypes: true })) { + const childPath = path.join(currentPath, entry.name); + const childStats = lstatSync(childPath); + if (childStats.isSymbolicLink()) { + continue; + } + + if (childStats.isDirectory()) { + queue.push(childPath); + } else if (childStats.isFile()) { + files.push(childPath); + } + } + } + + return files; +} + +function isMachOBinary(filePath) { + const header = new Uint8Array(4); + const descriptor = openSync(filePath, "r"); + try { + const bytesRead = readSync(descriptor, header, 0, header.byteLength, 0); + return bytesRead === header.byteLength && hasMachOMagic(header); + } finally { + closeSync(descriptor); + } +} + +function extractPlist(commandResult) { + const output = [commandResult.stdout, commandResult.stderr].filter(Boolean).join("\n"); + const xmlStart = output.indexOf("<?xml"); + const plistStart = output.indexOf("<plist"); + const start = xmlStart >= 0 ? xmlStart : plistStart; + const end = output.lastIndexOf("</plist>"); + if (start < 0 || end < start) { + throw new Error("codesign did not return an XML entitlement plist"); + } + return output.slice(start, end + "</plist>".length); +} + +function shortenDetail(value) { + const detail = String(value ?? "").trim(); + if (detail.length <= maxReportDetailLength) { + return detail; + } + return `${detail.slice(0, maxReportDetailLength)}\n[truncated]`; +} + +function createRecorder(report) { + return function check(name, operation) { + const startedAt = Date.now(); + try { + const detail = operation(); + report.checks.push({ + durationMs: Date.now() - startedAt, + name, + status: "passed", + ...(detail ? { detail: shortenDetail(detail) } : {}), + }); + console.log(`[macos-distribution] PASS ${name}`); + return detail; + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + report.checks.push({ + detail: shortenDetail(detail), + durationMs: Date.now() - startedAt, + name, + status: "failed", + }); + console.error(`[macos-distribution] FAIL ${name}: ${detail}`); + throw error; + } + }; +} + +function verifyInfoPlist(appPath, label, check) { + const infoPlistPath = path.join(appPath, "Contents", "Info.plist"); + check(`${label}: Info.plist policy`, () => { + assertFile(infoPlistPath, "Info.plist"); + const plistJson = runProcess("plutil", [ + "-convert", + "json", + "-o", + "-", + infoPlistPath, + ]).stdout; + const info = JSON.parse(plistJson); + const errors = []; + if (info.CFBundleIdentifier !== expectedBundleId) { + errors.push(`unexpected CFBundleIdentifier: ${info.CFBundleIdentifier ?? "missing"}`); + } + if (info.CFBundleShortVersionString !== packageJson.version) { + errors.push( + `unexpected CFBundleShortVersionString: ${info.CFBundleShortVersionString ?? "missing"}`, + ); + } + for (const usageKey of [ + "NSAudioCaptureUsageDescription", + "NSCameraUsageDescription", + "NSMicrophoneUsageDescription", + ]) { + if (typeof info[usageKey] !== "string" || info[usageKey].trim().length === 0) { + errors.push(`${usageKey} is missing or empty`); + } + } + assertPolicy(errors, `${label} Info.plist policy failed`); + return `${info.CFBundleIdentifier} ${info.CFBundleShortVersionString}`; + }); +} + +function verifyEntitlements(appPath, label, tempRoot, check) { + check(`${label}: signed entitlements`, () => { + const result = runProcess("codesign", [ + "--display", + "--entitlements", + "-", + "--xml", + appPath, + ]); + const entitlementsPath = path.join( + tempRoot, + `${label.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-")}-entitlements.plist`, + ); + writeFileSync(entitlementsPath, extractPlist(result)); + runProcess("plutil", ["-lint", entitlementsPath]); + const entitlements = JSON.parse( + runProcess("plutil", ["-convert", "json", "-o", "-", entitlementsPath]).stdout, + ); + assertPolicy(collectEntitlementErrors(entitlements), `${label} entitlement policy failed`); + return "required runtime, camera, and audio entitlements are present; get-task-allow is absent"; + }); +} + +function verifyMachOBinaries(appPath, arch, check) { + check("packaged app: nested Mach-O signatures and architectures", () => { + const machOBinaries = walkRegularFiles(appPath).filter(isMachOBinary); + + if (machOBinaries.length === 0) { + throw new Error("no Mach-O binaries were found in the app bundle"); + } + + for (const binaryPath of machOBinaries) { + runProcess("codesign", ["--verify", "--strict", "--verbose=2", binaryPath]); + const lipoOutput = runProcess("lipo", ["-info", binaryPath]).output; + assertPolicy( + collectArchitectureErrors(path.relative(appPath, binaryPath), lipoOutput, arch), + "Mach-O architecture policy failed", + ); + } + + return `${machOBinaries.length} Mach-O binaries verified`; + }); +} + +function verifyAppBundle(appPath, label, { arch, check, full, teamId, tempRoot }) { + check(`${label}: strict deep code signature`, () => { + runProcess("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath]); + }); + + check(`${label}: Developer ID metadata`, () => { + const details = runProcess("codesign", ["--display", "--verbose=4", appPath]).output; + assertPolicy( + collectCodeSigningMetadataErrors(details, teamId), + `${label} code-signing policy failed`, + ); + return `Developer ID Application; TeamIdentifier=${teamId}; hardened runtime; secure timestamp`; + }); + + verifyInfoPlist(appPath, label, check); + verifyEntitlements(appPath, label, tempRoot, check); + + if (full) { + verifyMachOBinaries(appPath, arch, check); + } + + check(`${label}: stapled notarization ticket`, () => { + runProcess("xcrun", ["stapler", "validate", appPath]); + }); + + check(`${label}: Gatekeeper execution assessment`, () => { + return runProcess("spctl", ["--assess", "--type", "execute", "--verbose=4", appPath]) + .output; + }); + + check(`${label}: distribution policy assessment`, () => { + return runProcess("syspolicy_check", ["distribution", appPath], { + timeout: 10 * 60 * 1000, + }).output; + }); +} + +function writeReport(report, reportPath, summaryPath) { + report.completedAt = new Date().toISOString(); + report.result = report.checks.some((item) => item.status === "failed") ? "failed" : "passed"; + writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); + + if (!summaryPath) { + return; + } + + const lines = [ + `## macOS ${report.arch} distribution verification`, + "", + `Result: **${report.result.toUpperCase()}**`, + "", + "| Check | Result |", + "| --- | --- |", + ...report.checks.map( + (item) => + `| ${item.name.replaceAll("|", "\\|")} | ${item.status === "passed" ? "PASS" : "FAIL"} |`, + ), + "", + ]; + writeFileSync(summaryPath, `${lines.join("\n")}\n`, { flag: "a" }); +} + +export function verifyMacOSDistribution(argv = process.argv.slice(2)) { + if (process.platform !== "darwin") { + throw new Error("macOS distribution verification must run on macOS"); + } + + const options = parseArguments(argv); + const report = { + arch: options.arch, + checks: [], + packageVersion: packageJson.version, + schemaVersion: 1, + sourceCommit: process.env.GITHUB_SHA ?? null, + startedAt: new Date().toISOString(), + }; + const check = createRecorder(report); + const tempRoot = mkdtempSync(path.join(os.tmpdir(), "recordly-macos-distribution-")); + let mountedDmgPath = null; + + try { + const artifactSuffix = options.arch === "arm64" ? "arm64" : "x64"; + const dmgPath = path.join(options.releaseDir, `${productName}-${artifactSuffix}.dmg`); + const zipPath = path.join(options.releaseDir, `${productName}-${artifactSuffix}.zip`); + + check("release artifacts exist", () => { + assertFile(dmgPath, "DMG artifact"); + assertFile(zipPath, "ZIP artifact"); + return `${path.basename(dmgPath)}, ${path.basename(zipPath)}`; + }); + + const packagedAppPath = check("packaged app bundle exists", () => + findSingleAppBundle(options.releaseDir, "release directory"), + ); + verifyAppBundle(packagedAppPath, "packaged app", { + ...options, + check, + full: true, + tempRoot, + }); + + check("DMG filesystem integrity", () => runProcess("hdiutil", ["verify", dmgPath]).output); + const dmgMountPath = path.join(tempRoot, "dmg"); + check("DMG attaches read-only for inspection", () => { + runProcess("mkdir", ["-p", dmgMountPath]); + runProcess("hdiutil", [ + "attach", + "-nobrowse", + "-readonly", + "-mountpoint", + dmgMountPath, + dmgPath, + ]); + mountedDmgPath = dmgMountPath; + }); + const dmgAppPath = check("DMG contains one app bundle", () => + findSingleAppBundle(dmgMountPath, "DMG"), + ); + verifyAppBundle(dmgAppPath, "DMG app", { + ...options, + check, + full: false, + tempRoot, + }); + check("DMG detaches cleanly", () => { + runProcess("hdiutil", ["detach", dmgMountPath]); + mountedDmgPath = null; + }); + + const zipExtractPath = path.join(tempRoot, "zip"); + check("ZIP extracts cleanly", () => { + runProcess("mkdir", ["-p", zipExtractPath]); + runProcess("ditto", ["-x", "-k", "--sequesterRsrc", zipPath, zipExtractPath]); + }); + const zipAppPath = check("ZIP contains one app bundle", () => + findSingleAppBundle(zipExtractPath, "ZIP"), + ); + verifyAppBundle(zipAppPath, "ZIP app", { + ...options, + check, + full: false, + tempRoot, + }); + + writeReport(report, options.reportPath, options.summaryPath); + console.log(`[macos-distribution] verification report: ${options.reportPath}`); + return report; + } catch (error) { + try { + writeReport(report, options.reportPath, options.summaryPath); + } catch (reportError) { + console.error( + `[macos-distribution] failed to write report: ${ + reportError instanceof Error ? reportError.message : String(reportError) + }`, + ); + } + throw error; + } finally { + if (mountedDmgPath) { + spawnSync("hdiutil", ["detach", "-force", mountedDmgPath], { encoding: "utf8" }); + } + rmSync(tempRoot, { force: true, recursive: true }); + } +} + +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null; +if (invokedPath === fileURLToPath(import.meta.url)) { + try { + verifyMacOSDistribution(); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + } +} diff --git a/scripts/windows-cmake-generators.mjs b/scripts/windows-cmake-generators.mjs new file mode 100644 index 000000000..132621639 --- /dev/null +++ b/scripts/windows-cmake-generators.mjs @@ -0,0 +1,36 @@ +export const WINDOWS_CMAKE_GENERATORS = Object.freeze([ + { name: "Visual Studio 18 2026", label: "VS 2026", toolset: "v143" }, + { name: "Visual Studio 17 2022", label: "VS 2022" }, + { name: "Visual Studio 16 2019", label: "VS 2019" }, +]); + +export const WINDOWS_VISUAL_STUDIO_INSTALL_DIRS = Object.freeze(["18", "2022", "2019"]); + +export function configureWithWindowsCmakeGenerator({ + prefix, + configure, + clearCache, + log = console.log, +}) { + let lastError; + + for (let index = 0; index < WINDOWS_CMAKE_GENERATORS.length; index += 1) { + const generator = WINDOWS_CMAKE_GENERATORS[index]; + clearCache(); + + try { + configure(generator.name, generator.toolset); + return generator.name; + } catch (error) { + lastError = error; + const nextGenerator = WINDOWS_CMAKE_GENERATORS[index + 1]; + if (nextGenerator) { + log( + `[${prefix}] ${generator.label} generator unavailable, trying ${nextGenerator.label}...`, + ); + } + } + } + + throw lastError; +} diff --git a/scripts/write-release-checksums.mjs b/scripts/write-release-checksums.mjs new file mode 100644 index 000000000..747fefb3c --- /dev/null +++ b/scripts/write-release-checksums.mjs @@ -0,0 +1,64 @@ +import { createHash } from "node:crypto"; +import { createReadStream, existsSync, readdirSync, statSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const projectRoot = process.cwd(); +const releaseRoot = path.join(projectRoot, "release"); +const outputFileName = process.argv[2] ?? "SHA256SUMS.txt"; +const outputPath = path.join(releaseRoot, outputFileName); +const releaseArtifactExtensions = new Set([".AppImage", ".blockmap", ".dmg", ".exe", ".zip"]); + +function relativePath(filePath) { + return path.relative(projectRoot, filePath).replaceAll("\\", "/"); +} + +function artifactSubjectName(filePath) { + return path.basename(filePath); +} + +function isReleaseArtifact(fileName) { + if (fileName.startsWith("SHA256SUMS")) { + return false; + } + + if (fileName.startsWith("latest") && fileName.endsWith(".yml")) { + return true; + } + + return releaseArtifactExtensions.has(path.extname(fileName)); +} + +function sha256(filePath) { + return new Promise((resolve, reject) => { + const hash = createHash("sha256"); + createReadStream(filePath) + .on("error", reject) + .on("data", (chunk) => hash.update(chunk)) + .on("end", () => resolve(hash.digest("hex"))); + }); +} + +if (!existsSync(releaseRoot)) { + throw new Error("[release-checksums] release directory is missing"); +} + +const releaseArtifacts = readdirSync(releaseRoot) + .map((entry) => path.join(releaseRoot, entry)) + .filter((filePath) => statSync(filePath).isFile()) + .filter((filePath) => isReleaseArtifact(path.basename(filePath))) + .sort((left, right) => relativePath(left).localeCompare(relativePath(right))); + +if (releaseArtifacts.length === 0) { + throw new Error("[release-checksums] no release artifacts found"); +} + +const checksums = []; +for (const filePath of releaseArtifacts) { + checksums.push(`${await sha256(filePath)} ${artifactSubjectName(filePath)}`); +} + +writeFileSync(outputPath, `${checksums.join("\n")}\n`); +console.log(`[release-checksums] wrote ${relativePath(outputPath)}`); +for (const artifact of releaseArtifacts) { + console.log(`[release-checksums] ${relativePath(artifact)}`); +} diff --git a/services/recordly-share/LICENSE b/services/recordly-share/LICENSE new file mode 100644 index 000000000..3f323cc23 --- /dev/null +++ b/services/recordly-share/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Aritro Paul + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/services/recordly-share/worker/.dev.vars.example b/services/recordly-share/worker/.dev.vars.example new file mode 100644 index 000000000..72cfc85a4 --- /dev/null +++ b/services/recordly-share/worker/.dev.vars.example @@ -0,0 +1,17 @@ +# The shared secret Recordly uses to authenticate with this worker. +# During the Deploy to Cloudflare flow you'll be prompted to set secrets — paste +# a value here. Or set it later: Worker → Settings → Variables and Secrets → add +# a Secret named API_SECRET. +# +# Generate a strong value with: openssl rand -hex 32 +API_SECRET= +# Local integration tests only. Never enable this in production. +ALLOW_API_SECRET_UPLOADS=false + +# Use the same public Supabase project configuration as the desktop app so +# signed-in users can publish without seeing an API-secret field. +SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co +SUPABASE_PUBLISHABLE_KEY=sb_publishable_YOUR_KEY + +# Only this Supabase user may administer this single-owner deployment. +OWNER_USER_ID= diff --git a/services/recordly-share/worker/.env.example b/services/recordly-share/worker/.env.example new file mode 100644 index 000000000..92048e4f7 --- /dev/null +++ b/services/recordly-share/worker/.env.example @@ -0,0 +1,15 @@ +# Cloudflare Worker configuration +# Copy this to .dev.vars for local development, or configure variables/secrets in Cloudflare. +# Non-secret bindings (including the D1 database ID) belong in wrangler.jsonc. + +# D1 database ID (from `npx wrangler d1 create recordly-share-db`) +D1_DATABASE_ID= + +# Server-side library administration only; never place this in the desktop app. +API_SECRET= +SUPABASE_URL= +SUPABASE_PUBLISHABLE_KEY= +ALLOW_API_SECRET_UPLOADS=false + +# Only this Supabase user may administer this single-owner deployment. +OWNER_USER_ID= diff --git a/services/recordly-share/worker/.gitignore b/services/recordly-share/worker/.gitignore new file mode 100644 index 000000000..b9bda4007 --- /dev/null +++ b/services/recordly-share/worker/.gitignore @@ -0,0 +1,10 @@ +.wrangler/ +node_modules/ +.dev.vars +web/.astro/ +videos/ + +# Ship prebuilt share-page assets so the Deploy-to-Cloudflare button (which +# clones this repo) serves them without a build step. Overrides root `dist/`. +!web/dist/ +!web/dist/** diff --git a/services/recordly-share/worker/CREATOR_PROFILE.md b/services/recordly-share/worker/CREATOR_PROFILE.md new file mode 100644 index 000000000..bd673de91 --- /dev/null +++ b/services/recordly-share/worker/CREATOR_PROFILE.md @@ -0,0 +1,9 @@ +# Public creator profile + +This single-owner share service displays an optional public creator profile on recordings. Configure the following Worker environment variables (in `.dev.vars` for local development or Cloudflare for deployment): + +- `CREATOR_NAME`: public display name (100 characters maximum) +- `CREATOR_BIO`: optional short description (200 characters maximum) +- `CREATOR_WEBSITE`: optional public HTTP(S) website + +These values are returned by the share data endpoint after the recording access checks. They apply to existing and future recordings on this service. No account email or operating-system identity is inferred. Unconfigured services display “Creator not provided”. diff --git a/services/recordly-share/worker/HOSTED.md b/services/recordly-share/worker/HOSTED.md new file mode 100644 index 000000000..290e6b6b4 --- /dev/null +++ b/services/recordly-share/worker/HOSTED.md @@ -0,0 +1,17 @@ +# Hosted sharing + +Self-hosting remains the default. Hosted sharing requires a separate operator-owned Wrangler configuration; do not commit account IDs, staging users, credentials, or real deployment URLs. + +Configure the existing `DB`, `VIDEOS_BUCKET`, and `ASSETS` bindings and scheduled cleanup trigger as in `wrangler.jsonc`, then add: + +- `HOSTED_MODE` set to `true`. +- `SUPABASE_URL` set to the HTTPS auth project URL. +- `SUPABASE_PUBLISHABLE_KEY` set to that project's publishable key. Never use a service-role key. +- `REQUEST_RATE_LIMIT` and `ACCOUNT_RATE_LIMIT` Cloudflare rate-limiter bindings. Hosted requests fail closed without both. Choose separate namespaces and limits suitable for video playback and multipart uploads. +- Optionally `STAGING_ALLOWED_USER_ID` to restrict a staging deployment to one verified user. Leave unset for general hosted access. + +Existing databases are upgraded by `ensureSchema`; migrations 0008 and 0009 also describe the owner and upload-session schema. Legacy recordings without an owner do not appear in authenticated hosted libraries. Do not infer ownership from client-provided fields. + +Deploy the worker before configuring desktop `VITE_CLOUD_SHARE_ENDPOINT` to its HTTPS `/api/upload` URL. Production builds default to cloud sharing unavailable when no endpoint is configured. + +Verified accounts can hold five recordings, including pending uploads, with a maximum of 1,000,000,000 bytes each and fourteen-day expiry. These are the current free limits; this change does not implement paid entitlements. Scheduled cleanup removes expired recordings and abandoned uploads. Test with an isolated database and bucket before production rollout. diff --git a/services/recordly-share/worker/README.md b/services/recordly-share/worker/README.md new file mode 100644 index 000000000..124c81457 --- /dev/null +++ b/services/recordly-share/worker/README.md @@ -0,0 +1,52 @@ +# Recordly Share + +Recordly Share is the self-hosted video publishing and review service used by Recordly. It runs as a Cloudflare Worker with R2 video storage, D1 metadata, range streaming, a responsive viewer, timestamped comments, reactions, passwords, expiration, and a private recording library. + +## Local development + +```sh +cp .dev.vars.example .dev.vars +npm install +npm --prefix web install +npm --prefix web run build +npm run dev +``` + +Set `OWNER_USER_ID` to the deployment owner’s Supabase user ID. Only that user may administer this single-owner library. Set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` in `.dev.vars` to the same public project configuration used by the desktop app. Recordly sends the signed-in user's access token when it publishes to: + +- Endpoint: `http://localhost:8787/api/upload` + +The Worker validates Supabase access tokens before accepting uploads. `API_SECRET` remains a server-side library-administration password. An API-secret upload fallback can be enabled only for isolated local tests by setting `ALLOW_API_SECRET_UPLOADS=true`; do not enable it in production. + +Anyone with a valid link can watch and leave timestamped feedback using a display name; commenting does not require an account. Per-IP rate limiting and comment-length limits remain enabled. + +## Deployment + +```sh +npm install +npm --prefix web install +npm --prefix web run build +npx wrangler secret put API_SECRET +npx wrangler secret put SUPABASE_PUBLISHABLE_KEY +npm run deploy +``` + +Set `SUPABASE_URL` as a Worker variable. The ID-less configuration provisions `recordly-share-db` and `recordly-videos` for a new deployment. Desktop builds currently upload only to localhost; production service integration is deferred. + +## Attribution + +This service is adapted from an MIT-licensed open-source project. The required original copyright and permission notice is preserved in [`../LICENSE`](../LICENSE) and Recordly's root `THIRD_PARTY_NOTICES.md`. Product-facing pages use Recordly branding; legal attribution must remain with distributed copies. + +## Worker source layout + +`src/index.js` contains the fetch/scheduled entry points and error boundaries. `router.js` dispatches requests and applies route access checks. The implementation lives in focused modules: + +- `schema.js`: database bootstrap and migrations. +- `auth.js` and `accounts.js`: owner/dashboard access, video passwords, and optional viewer accounts. +- `uploads.js`: upload creation, multipart transfers, and metadata. +- `media.js`: streaming, captions, share data, and social previews. +- `feedback.js`: comments and reactions. +- `library.js`: listing, renewal, deletion, view counts, and expiry cleanup. +- `crypto.js`, `http.js`, and `video.js`: shared cryptographic, response, and video metadata helpers. + +Run `npm test` here to exercise these modules through the Worker endpoints and database migrations. diff --git a/services/recordly-share/worker/STAGING.md b/services/recordly-share/worker/STAGING.md new file mode 100644 index 000000000..f92482581 --- /dev/null +++ b/services/recordly-share/worker/STAGING.md @@ -0,0 +1,90 @@ +# Cloudflare staging + +Provisioned on 2026-09-23 in account `b000d832666fb4eb4bc63df58fecbb11`: + +- D1: `recordly-share-staging-db`, ID `1d930cc5-644f-4b29-932d-ef75257b2f8c`. +- R2: `recordly-videos-staging`, Standard storage, automatically placed in Oceania. Public access is disabled; the bucket is empty. +- D1 initialized using `schema.sql`. Verified tables: chapters, comment_sessions, comment_users, comments, password_attempts, reactions, transcript_segments, videos. No customer data inserted. + +The owner-approved staging Worker is deployed at `https://recordly-share-staging.youngchen3442.workers.dev`, version `81cf4abb-5703-4f4c-b74b-54ff428009d7`. Existing `landingvideos` storage and DNS were not changed. Wrangler uses an approved OAuth grant limited to account/user read, Worker-script write, and background access; no DNS/zone/D1 write scopes were granted. + +## Local validation + +Use `wrangler.staging.jsonc` explicitly. The default self-host configuration is unchanged. The owner approved an isolated public staging deployment on 2026-09-23. Staging enables workers.dev but disables preview URLs, custom-domain routes and API-secret uploads. The ignored `.wrangler/staging-deploy.json` includes the existing Supabase URL and publishable key; no service-role key is used. The deployment is live. Use `--experimental-provision=false` with Wrangler 4.99.0: existing resources are already configured and verified, so automatic provisioning is unnecessary and requires additional R2 permissions. The local-only `service_binding_extra_handlers` flag must not be included in deployments. + +From this directory, build the web assets if needed, then validate the Worker without publishing: + +```sh +npm --prefix web run build +npx wrangler deploy --dry-run --experimental-provision=false --config wrangler.staging.jsonc --outdir /tmp/recordly-share-staging-build +``` + +## Environment separation + +Production should use separate resources. The local development endpoint now points to staging through ignored `.env.development.local`; packaged production configuration is unchanged. The desktop trusts only this exact staging origin alongside its existing origins. Payments remain a later step. + +## Deployed hosted behavior + +`HOSTED_MODE=true` selects the multi-account path. Staging opts into it; existing self-host configurations keep their current behavior. Hosted mode validates the bearer with Supabase, requires a confirmed email and a non-anonymous user, and ignores shared API secrets/dashboard cookies. Legacy comment account and shared-password dashboard routes are disabled in hosted mode. + +Schema v4 / migration `0008_recording_owners.sql` adds nullable `videos.owner_id` and its index. Existing unowned rows remain inaccessible to hosted users; no ownership is guessed or backfilled. Runtime migration supports existing v3 databases. The remote staging database received migrations 0008 and 0009 through its console on 2026-09-23. Verified owner column = 1, upload tracking tables = 2, recordings = 0. These manual changes are not registered in Wrangler migration history. Do not blindly rerun the ALTER migration. + +Recording creation binds the owner from the verified session, never the request body. One atomic conditional insert caps each owner at five stored rows, including pending and expired rows. Explicit owner deletion frees a slot. Library/view queries are scoped; all upload, metadata, renew, and delete routes check ownership before handling storage. Public share-link playback remains intentional. + +Validation: all 83 local Worker tests passed, including all nine mutation routes against a different account, unowned-row isolation, five-slot concurrent reservations, slot release after deletion, and legacy migration/self-host regressions. + +## Upload limits and cleanup + +Hosted mode caps recordings at the owner-approved 1 GB (1,000,000,000 bytes), five stored rows per account, and thumbnails at 2 MiB. The limit is enforced on reservation and actual streamed bytes using FixedLengthStream. Direct uploads are capped at 50 MiB; multipart uses the desktop's fixed 25 MiB chunks with a bounded final chunk and an exact manifest. Transfers time out after five minutes. Completed objects are checked before publication and cannot be overwritten. + +Schema v5 / `0009_upload_sessions.sql` adds `recording_uploads` and `recording_locks`. Multipart starts are idempotent, supplied upload IDs must match the stored session, and record mutations are serialized across isolates. Deletion aborts multipart sessions and removes R2 objects before freeing the slot; storage failures keep the row for retry. Metadata payloads and transcript/chapter counts are bounded, and retrying publication does not duplicate transcript rows. + +Cleanup code removes unfinished reservations older than 24 hours and recordings whose 14-day free-plan expiry has elapsed, in batches of 100. It rechecks eligibility under the mutation lock and recovers crash locks older than one day. **The owner explicitly approved automatic staging cleanup. Deployed hourly at minute 17 UTC (`17 * * * *`), removing expired recordings and unfinished reservations older than 24 hours.** Verified in the R2 dashboard on 2026-09-23: the enabled Default Multipart Abort Rule aborts unfinished multipart uploads after seven days, across the bucket. This covers orphaned sessions left by a crash between R2 creation and D1 persistence. Public R2 access remains disabled and no custom domain is assigned. No lifecycle mutation was needed. + +Hosted mode requires native Worker rate-limit bindings: 120 requests/minute per IP before auth/storage, and 60 requests/minute per verified account. These are permissive per-location abuse controls, not an exact global spend cap. The five-slot reservation remains atomic in D1. Staging namespaces are `2026092301` and `2026092302`; production must use distinct namespaces. References: [Cloudflare rate limits](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) and [FixedLengthStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/). + +## Desktop and verification + +The share dialog now has a HeroUI recordings manager with pending-upload visibility and explicit delete confirmation. It calls authenticated main-process IPC, which validates trusted origins and forbids redirects. `VITE_CLOUD_SHARE_ENDPOINT` controls the endpoint; development falls back to localhost, while packaged builds fail clearly until configured. UI preview at `tests/ui/shared-recordings.html` uses disposable in-memory data only; expansion, confirmation, and deletion/slot count were checked in the browser. + +A local backend is running at `127.0.0.1:8787` with isolated D1/R2 state under `/tmp/recordly-hosted-test-state` and the app's existing public Supabase configuration. Its generated config is ignored under `.wrangler/hosted-local.json`. No service-role key is used. Live unsigned health requests returned 401. The local server is no longer the development app endpoint. A fresh owner-approved magic-link request was followed by a signed-in native app session, and the real staging library loaded successfully as zero of five recordings. The owner-approved disposable native upload/playback/delete test subsequently passed. + +Latest validation: 83 Worker tests, 12 auth/desktop contract tests, TypeScript checking, and staging deployment dry run passed. Deliberately truncated streams and injected storage failures emit expected runtime diagnostics in negative tests. + +## Remaining external steps + +1. Verify an authenticated feedback submission. Owner sign-in and native staging upload/playback/deletion are verified. +2. Free links expire after 14 days and hourly staging cleanup is deployed. Configure usage alerts before launch. The R2 bucket already aborts incomplete multipart uploads after seven days. +3. Plan separate production resources, verify another-account isolation live, and configure production builds before release. Payments remain last. + +## Staging deployment verification (2026-09-23) + +- Deployed with existing D1/R2 bindings, Supabase public auth settings, owner allowlist, and both native rate-limit bindings. No service-role key was supplied. +- Live unsigned `/api/health` and `/api/videos` return 401 with `Verified sign-in required`; an invalid bearer also returns 401. Legacy `/library` and `/auth/register` return 404. These checks exercise remote D1 schema initialization without creating recordings. +- The exact staging origin was added to the desktop allowlist. Six desktop contract tests passed, including rejection of a sibling workers.dev origin and a lookalike hostname. TypeScript checking passed. +- Fixed a development startup failure by explicitly prebundling React Aria's CommonJS `use-sync-external-store/shim` dependency. Verified the native HUD, project dashboard, and sign-in form render again. +- The user approved one fresh magic-link email to their existing owner account for the desktop callback test. Do not substitute a dashboard-generated link or extract session credentials. + +The Home dashboard Shared tab now uses the real HeroUI recordings manager with refresh, quota usage, open-share controls, and explicit deletion confirmation. Verified visually in the signed-in native app: `0 of 5 recordings · 1 GB each`, `No shared recordings yet.` No existing project was uploaded. + +## Native end-to-end test passed (2026-09-23) + +The owner approved uploading a generated two-second 1920×1080 colour-pattern source and deleting its disposable cloud recording. Imported `/tmp/recordly-staging-test-20260923.mp4`, named the local project **Disposable staging test**, and published through the normal desktop share flow. The selected High export setting produced 1554×972 video with the editor's default 16:10 canvas; this was not a full-resolution 1080p output or a 1 GB stress test. + +The public share page rendered the correct title and decoded all two seconds (`ended=true`, no video error). The signed-in library showed one of five slots and a 1.4 MB recording. Deleted only that approved disposable cloud recording through its confirmation UI. The library returned to zero of five, and reloading its public link showed **Recording unavailable**. The local synthetic project/source remain for repeat testing. No user recording was uploaded or deleted. + +Still unverified live: large multipart uploads, 1 GB boundary under real network conditions, another-account rejection, feedback diagnostics/attachments, and packaged desktop protocol callbacks. Local boundary/concurrency/isolation tests are documented above. Free-plan links expire after 14 days. The next successful hourly cleanup removes stored objects and releases slots, in batches of 100; cleanup backlog or storage failures may delay this. + +## Free-plan expiry policy (2026-09-23) + +The owner selected 14 days. New hosted upload reservations get a server-assigned expiry 14 days from creation. All hosted accounts currently use the free policy; client-supplied plan/expiry claims are ignored. Paid entitlements are not wired yet. Free renewal is denied (403) to prevent indefinite extensions. The separate self-host mode keeps its existing 30-day setting. Desktop publishing and library copy disclose the free-plan expiry. Twenty-seven hosted/library tests passed, including expiry, renewal bypass prevention, expired playback denial, and self-host regressions. + +The 14-day policy was deployed successfully to staging as version `2a38fbd4-a3c9-4028-a5ca-68a32b95a60a`. + +## Publish UI and cleanup (2026-09-23) + +The desktop toolbar now opens **Publish**, with HeroUI Link/Local choices. Link expands inline notes and Share using Motion (Framer Motion's `motion/react` package) and reduced-motion support. Local retains export settings and save flow. Share preparation no longer closes the popover; busy sharing disables destination switching and dismissal while preserving cancellation. Native UI inspection verified both modes and the complete notes/expiry disclosure layout. + +Hourly cleanup was explicitly approved after automatic approval review required separate confirmation for permanent cloud deletion. Deployed version `81cf4abb-5703-4f4c-b74b-54ff428009d7`; Wrangler confirmed `17 * * * *`. The scheduled handler now reports failures to Cloudflare instead of swallowing them. Local scheduler tests verify expired bytes/rows are deleted while active recordings remain. 28 Worker tests and 15 export/feedback tests passed, plus TypeScript checking. A real scheduled run has not yet been observed. + +The shared player now uses uploaded first-frame posters and falls back to the video’s decoded first frame when an older recording has no poster. Deployed to staging on 2026-09-23; Framer and custom domains are unchanged. diff --git a/services/recordly-share/worker/icon-64.png b/services/recordly-share/worker/icon-64.png new file mode 100644 index 000000000..fc3c7a673 Binary files /dev/null and b/services/recordly-share/worker/icon-64.png differ diff --git a/services/recordly-share/worker/migrations/0002_share_enhancements.sql b/services/recordly-share/worker/migrations/0002_share_enhancements.sql new file mode 100644 index 000000000..dcc791506 --- /dev/null +++ b/services/recordly-share/worker/migrations/0002_share_enhancements.sql @@ -0,0 +1,30 @@ +-- Migration 2: Password protection, CTA, view tracking, reactions, comments + +-- Add password and CTA columns to videos +ALTER TABLE videos ADD COLUMN password_hash TEXT; +ALTER TABLE videos ADD COLUMN cta_url TEXT; +ALTER TABLE videos ADD COLUMN cta_text TEXT; +ALTER TABLE videos ADD COLUMN last_notified_view_count INTEGER NOT NULL DEFAULT 0; + +-- Reactions table +CREATE TABLE IF NOT EXISTS reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + emoji TEXT NOT NULL, + client_ip TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_reactions_video_id ON reactions(video_id); +CREATE INDEX IF NOT EXISTS idx_reactions_ip ON reactions(video_id, client_ip); + +-- Comments table +CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + author_name TEXT NOT NULL DEFAULT 'Anonymous', + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_comments_video_id ON comments(video_id); diff --git a/services/recordly-share/worker/migrations/0003_chapters_speakers.sql b/services/recordly-share/worker/migrations/0003_chapters_speakers.sql new file mode 100644 index 000000000..726c0c0e6 --- /dev/null +++ b/services/recordly-share/worker/migrations/0003_chapters_speakers.sql @@ -0,0 +1,9 @@ +-- Migration: Add speaker labels, chapters, and meeting flag +ALTER TABLE transcript_segments ADD COLUMN speaker TEXT; +ALTER TABLE videos ADD COLUMN is_meeting INTEGER NOT NULL DEFAULT 0; +CREATE TABLE IF NOT EXISTS chapters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + title TEXT NOT NULL +); diff --git a/services/recordly-share/worker/migrations/0004_security.sql b/services/recordly-share/worker/migrations/0004_security.sql new file mode 100644 index 000000000..33d3c0193 --- /dev/null +++ b/services/recordly-share/worker/migrations/0004_security.sql @@ -0,0 +1,2 @@ +-- Add client_ip tracking for comment rate limiting +ALTER TABLE comments ADD COLUMN client_ip TEXT NOT NULL DEFAULT ''; diff --git a/services/recordly-share/worker/migrations/0005_add_summary.sql b/services/recordly-share/worker/migrations/0005_add_summary.sql new file mode 100644 index 000000000..99ffa9334 --- /dev/null +++ b/services/recordly-share/worker/migrations/0005_add_summary.sql @@ -0,0 +1,2 @@ +-- Add summary column to videos +ALTER TABLE videos ADD COLUMN summary TEXT; diff --git a/services/recordly-share/worker/migrations/0006_password_salt_and_indexes.sql b/services/recordly-share/worker/migrations/0006_password_salt_and_indexes.sql new file mode 100644 index 000000000..53e94b5ac --- /dev/null +++ b/services/recordly-share/worker/migrations/0006_password_salt_and_indexes.sql @@ -0,0 +1,17 @@ +-- Migration 0006: salted share passwords, password brute-force tracking, +-- and the missing chapters lookup index. +-- Applied by CloudflareDeployService for token-based self-host deploys. +-- The hosted worker applies the same change at runtime (SCHEMA_VERSION 2). + +ALTER TABLE videos ADD COLUMN password_salt TEXT; + +CREATE INDEX IF NOT EXISTS idx_chapters_video_id ON chapters(video_id); + +CREATE TABLE IF NOT EXISTS password_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL, + client_ip TEXT NOT NULL, + attempted_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_password_attempts ON password_attempts(video_id, client_ip, attempted_at); diff --git a/services/recordly-share/worker/migrations/0007_comment_accounts.sql b/services/recordly-share/worker/migrations/0007_comment_accounts.sql new file mode 100644 index 000000000..8c60eeb97 --- /dev/null +++ b/services/recordly-share/worker/migrations/0007_comment_accounts.sql @@ -0,0 +1,20 @@ +-- Optional viewer accounts; posting comments does not require an account. + +CREATE TABLE IF NOT EXISTS comment_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + password_salt TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS comment_sessions ( + token_hash TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES comment_users(id) ON DELETE CASCADE, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_comment_sessions_user ON comment_sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_comment_sessions_expiry ON comment_sessions(expires_at); diff --git a/services/recordly-share/worker/migrations/0008_recording_owners.sql b/services/recordly-share/worker/migrations/0008_recording_owners.sql new file mode 100644 index 000000000..dccdc38b1 --- /dev/null +++ b/services/recordly-share/worker/migrations/0008_recording_owners.sql @@ -0,0 +1,2 @@ +ALTER TABLE videos ADD COLUMN owner_id TEXT; +CREATE INDEX IF NOT EXISTS idx_videos_owner ON videos(owner_id); diff --git a/services/recordly-share/worker/migrations/0009_upload_sessions.sql b/services/recordly-share/worker/migrations/0009_upload_sessions.sql new file mode 100644 index 000000000..93cb6ef55 --- /dev/null +++ b/services/recordly-share/worker/migrations/0009_upload_sessions.sql @@ -0,0 +1,2 @@ +CREATE TABLE IF NOT EXISTS recording_uploads (share_code TEXT PRIMARY KEY REFERENCES videos(share_code) ON DELETE CASCADE, upload_id TEXT NOT NULL); +CREATE TABLE IF NOT EXISTS recording_locks (share_code TEXT PRIMARY KEY, token TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now'))); diff --git a/services/recordly-share/worker/package-lock.json b/services/recordly-share/worker/package-lock.json new file mode 100644 index 000000000..ffccd9799 --- /dev/null +++ b/services/recordly-share/worker/package-lock.json @@ -0,0 +1,3914 @@ +{ + "name": "recordly-share", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "recordly-share", + "version": "1.0.0", + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.9.12", + "vitest": "^3.2.0", + "wrangler": "^4.98.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/vitest-pool-workers": { + "version": "0.9.14", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.9.14.tgz", + "integrity": "sha512-1D/1KFa32EymaYDojWcrmUyMbHJwewn/mZSxsAqXdybvKMrHVaoaqGpLoH9bgWSfbrGs4af4RrmIXMINOqc4Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "0.2.14", + "cjs-module-lexer": "^1.2.3", + "devalue": "^5.3.2", + "miniflare": "4.20251011.0", + "semver": "^7.7.1", + "wrangler": "4.44.0", + "zod": "^3.22.3" + }, + "peerDependencies": { + "@vitest/runner": "2.0.x - 3.2.x", + "@vitest/snapshot": "2.0.x - 3.2.x", + "vitest": "2.0.x - 3.2.x" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/unenv-preset": { + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.8.tgz", + "integrity": "sha512-Ky929MfHh+qPhwCapYrRPwPVHtA2Ioex/DbGZyskGyNRDe9Ru3WThYZivyNVaPy5ergQSgMs9OKrM9Ajtz9F6w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.21", + "workerd": "^1.20250927.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/unenv": { + "version": "2.0.0-rc.21", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz", + "integrity": "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.7", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "ufo": "^1.6.1" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.44.0.tgz", + "integrity": "sha512-BLOUigckcWZ0r4rm7b5PuaTpb9KP9as0XeCRSJ8kqcNgXcKoUD3Ij8FlPvN25KybLnFnetaO0ZdfRYUPWle4qw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.0", + "@cloudflare/unenv-preset": "2.7.8", + "blake3-wasm": "2.1.5", + "esbuild": "0.25.4", + "miniflare": "4.20251011.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.21", + "workerd": "1.20251011.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20251011.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20251011.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251011.0.tgz", + "integrity": "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20251011.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251011.0.tgz", + "integrity": "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20251011.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251011.0.tgz", + "integrity": "sha512-BccMiBzFlWZyFghIw2szanmYJrJGBGHomw2y/GV6pYXChFzMGZkeCEMfmCyJj29xczZXxcZmUVJxNy4eJxO8QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20251011.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251011.0.tgz", + "integrity": "sha512-79o/216lsbAbKEVDZYXR24ivEIE2ysDL9jvo0rDTkViLWju9dAp3CpyetglpJatbSi3uWBPKZBEOqN68zIjVsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20251011.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251011.0.tgz", + "integrity": "sha512-RIXUQRchFdqEvaUqn1cXZXSKjpqMaSaVAkI5jNZ8XzAw/bw2bcdOVUtakrflgxDprltjFb0PTNtuss1FKtH9Jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.16.tgz", + "integrity": "sha512-yNm/fYEcnpRjYduLMaddTK9XKYil6xB88+qFg79ZdZhHu1PadfoQmFW7pVTx7FZqMBNcUuThiAhxhENgtAO2/w==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/birpc": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", + "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "4.20251011.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251011.0.tgz", + "integrity": "sha512-DlZ7vR5q/RE9eLsxsrXzfSZIF2f6O5k0YsFrSKhWUtdefyGtJt4sSpR6V+Af/waaZ6+zIFy9lsknHBCm49sEYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", + "stoppable": "1.1.0", + "undici": "7.14.0", + "workerd": "1.20251011.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/miniflare/node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20251011.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251011.0.tgz", + "integrity": "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20251011.0", + "@cloudflare/workerd-darwin-arm64": "1.20251011.0", + "@cloudflare/workerd-linux-64": "1.20251011.0", + "@cloudflare/workerd-linux-arm64": "1.20251011.0", + "@cloudflare/workerd-windows-64": "1.20251011.0" + } + }, + "node_modules/wrangler": { + "version": "4.99.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.99.0.tgz", + "integrity": "sha512-i7GA2mZETTyq3ljWdEzM908FjLaMWZ1AaAHKaOJ8pFA/tonf2VqIWDyBGzKleIVBbNQxOTIY2wnbv0iaK3rC6g==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260609.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260609.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260609.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260609.1.tgz", + "integrity": "sha512-AK8tYLQm+8BqQMzjZ55ZfuhfIm1eCkj+Ykxz6kWXojdACwjjU03MrwdM9fBDdgzU3upXOs4e1scOFHySlfVQjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260609.1.tgz", + "integrity": "sha512-4kKXfr7ZHU6xQ/R9ShdSuj1A1bEouoRcHzUWdjnuMPBlRsAAVanlxAVYISotFUulLEinayOpRFbhpsfwzrpSSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260609.1.tgz", + "integrity": "sha512-T2Ebir2OPHAvvZ0HUh5mi1lN8q30sVi4lf7LIpc28AHoWtoOmJ0jA5AJK4IYJm1MKEbBldq+QsckaHOCQFmRpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260609.1.tgz", + "integrity": "sha512-INfcYoSsKqEIvPL69/3RkqYoP8WUR0VEN6loWN/3tekXLoJrVOj3E5NjIetsdS8MJN6zc3st/ae4bMuWRRzoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260609.1.tgz", + "integrity": "sha512-EWhfxKI1aqUr7S8xuGxgmRCumEzB8iSsCIz6oEqJN+3pZuW3EWiKDGFW4EY1BmwNINLW1eO5VMGYb8Fj6FVYxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "4.20260609.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260609.0.tgz", + "integrity": "sha512-4ZfNh9ACDa/mKKQvTSO2vigyQS2MB7dEU02KRPle4FqL7S6nek+2Fq6WGzazZbt1OORYgb4OGVLnOCx+My2NNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.24.8", + "workerd": "1.20260609.1", + "ws": "8.20.1", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/wrangler/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/wrangler/node_modules/undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260609.1.tgz", + "integrity": "sha512-KF/Y/8f4VoXCk87NuU6RqmO0X5fdzcrxU3XzAgoPUpnH9t1ZyzRgX1O/9sJvjItxroCBTEBzKssda02Dz9i6BA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260609.1", + "@cloudflare/workerd-darwin-arm64": "1.20260609.1", + "@cloudflare/workerd-linux-64": "1.20260609.1", + "@cloudflare/workerd-linux-arm64": "1.20260609.1", + "@cloudflare/workerd-windows-64": "1.20260609.1" + } + }, + "node_modules/wrangler/node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/services/recordly-share/worker/package.json b/services/recordly-share/worker/package.json new file mode 100644 index 000000000..07049b345 --- /dev/null +++ b/services/recordly-share/worker/package.json @@ -0,0 +1,16 @@ +{ + "name": "recordly-share", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "wrangler dev --config wrangler.jsonc", + "deploy": "wrangler deploy --config wrangler.jsonc", + "db:init": "wrangler d1 execute recordly-share-db --file=./schema.sql", + "test": "vitest run" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.9.12", + "vitest": "^3.2.0", + "wrangler": "^4.98.0" + } +} diff --git a/services/recordly-share/worker/schema.sql b/services/recordly-share/worker/schema.sql new file mode 100644 index 000000000..dab5e2e7f --- /dev/null +++ b/services/recordly-share/worker/schema.sql @@ -0,0 +1,100 @@ +-- Recordly Share — consolidated D1 schema (based on Voom, through migration 0009). +-- Keep in sync with SCHEMA_STATEMENTS in src/schema.js (the runtime source of +-- truth for fresh databases). Statements are idempotent so this file is safe +-- to re-apply to an existing database; the numbered files in migrations/ +-- exist for upgrading databases created from an older schema.sql. + +CREATE TABLE IF NOT EXISTS videos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + share_code TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + duration REAL NOT NULL DEFAULT 0, + width INTEGER NOT NULL DEFAULT 0, + height INTEGER NOT NULL DEFAULT 0, + has_webcam INTEGER NOT NULL DEFAULT 0, + file_size INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT NOT NULL, + upload_completed INTEGER NOT NULL DEFAULT 0, + view_count INTEGER NOT NULL DEFAULT 0, + password_hash TEXT, + cta_url TEXT, + cta_text TEXT, + last_notified_view_count INTEGER NOT NULL DEFAULT 0, + is_meeting INTEGER NOT NULL DEFAULT 0, + summary TEXT, + password_salt TEXT, + owner_id TEXT +); +CREATE INDEX IF NOT EXISTS idx_videos_owner ON videos(owner_id); +CREATE INDEX IF NOT EXISTS idx_videos_share_code ON videos(share_code); +CREATE INDEX IF NOT EXISTS idx_videos_expires_at ON videos(expires_at); + +CREATE TABLE IF NOT EXISTS transcript_segments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + start_time REAL NOT NULL, + end_time REAL NOT NULL, + text TEXT NOT NULL, + speaker TEXT +); +CREATE INDEX IF NOT EXISTS idx_transcript_video_id ON transcript_segments(video_id); + +CREATE TABLE IF NOT EXISTS reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + emoji TEXT NOT NULL, + client_ip TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_reactions_video_id ON reactions(video_id); +CREATE INDEX IF NOT EXISTS idx_reactions_ip ON reactions(video_id, client_ip); + +CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + author_name TEXT NOT NULL DEFAULT 'Anonymous', + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + client_ip TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_comments_video_id ON comments(video_id); + +CREATE TABLE IF NOT EXISTS chapters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + title TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_chapters_video_id ON chapters(video_id); + +CREATE TABLE IF NOT EXISTS password_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL, + client_ip TEXT NOT NULL, + attempted_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_password_attempts ON password_attempts(video_id, client_ip, attempted_at); + +CREATE TABLE IF NOT EXISTS comment_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + password_salt TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS comment_sessions ( + token_hash TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES comment_users(id) ON DELETE CASCADE, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_comment_sessions_user ON comment_sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_comment_sessions_expiry ON comment_sessions(expires_at); + +CREATE TABLE IF NOT EXISTS recording_uploads (share_code TEXT PRIMARY KEY REFERENCES videos(share_code) ON DELETE CASCADE, upload_id TEXT NOT NULL); +CREATE TABLE IF NOT EXISTS recording_locks (share_code TEXT PRIMARY KEY, token TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now'))); diff --git a/services/recordly-share/worker/src/accounts.js b/services/recordly-share/worker/src/accounts.js new file mode 100644 index 000000000..261fa5178 --- /dev/null +++ b/services/recordly-share/worker/src/accounts.js @@ -0,0 +1,132 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { errorResponse, parseCookies } from './http.js'; +import { generateSalt, sha256Hex, timingSafeEqual } from './crypto.js'; +import { checkLoginRateLimit, clearLoginRateLimit } from './auth.js'; + +const COMMENT_SESSION_COOKIE = 'recordly_comment_session'; + +const COMMENT_SESSION_SECONDS = 30 * 24 * 60 * 60; + +async function hashCommentPassword(password, salt) { + const material = await crypto.subtle.importKey( + 'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveBits'] + ); + const bits = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + hash: 'SHA-256', + salt: new TextEncoder().encode(salt), + iterations: 210000, + }, + material, + 256, + ); + return Array.from(new Uint8Array(bits), b => b.toString(16).padStart(2, '0')).join(''); +} + +function generateSessionToken() { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); +} + +function commentSessionCookie(request, token, maxAge = COMMENT_SESSION_SECONDS) { + const secure = new URL(request.url).protocol === 'https:' ? '; Secure' : ''; + return `${COMMENT_SESSION_COOKIE}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${maxAge}${secure}`; +} + +export async function commentViewer(request, env) { + const cookies = parseCookies(request.headers.get('Cookie') || ''); + const token = cookies[COMMENT_SESSION_COOKIE]; + if (!token) return null; + const tokenHash = await sha256Hex(token); + return env.DB.prepare( + `SELECT u.id, u.email, u.display_name + FROM comment_sessions s + JOIN comment_users u ON u.id = s.user_id + WHERE s.token_hash = ? AND datetime(s.expires_at) > datetime('now')` + ).bind(tokenHash).first(); +} + +async function createCommentSession(request, env, user) { + const token = generateSessionToken(); + const tokenHash = await sha256Hex(token); + const expiresAt = new Date(Date.now() + COMMENT_SESSION_SECONDS * 1000).toISOString(); + await env.DB.prepare( + 'INSERT INTO comment_sessions (token_hash, user_id, expires_at) VALUES (?, ?, ?)' + ).bind(tokenHash, user.id, expiresAt).run(); + return new Response(JSON.stringify({ + user: { email: user.email, displayName: user.display_name }, + }), { + headers: { + 'Content-Type': 'application/json', + 'Set-Cookie': commentSessionCookie(request, token), + }, + }); +} + +export async function handleCommentRegister(request, env) { + const ip = request.headers.get('CF-Connecting-IP') || 'unknown'; + if (!checkLoginRateLimit(`register:${ip}`)) return errorResponse('Too many attempts', 429); + const body = await request.json(); + const email = String(body.email || '').trim().toLowerCase(); + const displayName = String(body.displayName || '').trim(); + const password = String(body.password || ''); + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || email.length > 254) { + return errorResponse('Enter a valid email address'); + } + if (displayName.length < 2 || displayName.length > 100) { + return errorResponse('Display name must be between 2 and 100 characters'); + } + if (password.length < 8 || password.length > 256) { + return errorResponse('Password must be between 8 and 256 characters'); + } + const salt = generateSalt(); + const passwordHash = await hashCommentPassword(password, salt); + try { + const result = await env.DB.prepare( + 'INSERT INTO comment_users (email, display_name, password_hash, password_salt) VALUES (?, ?, ?, ?)' + ).bind(email, displayName, passwordHash, salt).run(); + return createCommentSession(request, env, { id: result.meta.last_row_id, email, display_name: displayName }); + } catch (error) { + if (/unique|constraint/i.test(error.message || '')) { + return errorResponse('An account with this email already exists', 409); + } + throw error; + } +} + +export async function handleCommentLogin(request, env) { + const ip = request.headers.get('CF-Connecting-IP') || 'unknown'; + if (!checkLoginRateLimit(`comment:${ip}`)) return errorResponse('Too many attempts', 429); + const body = await request.json(); + const email = String(body.email || '').trim().toLowerCase(); + const password = String(body.password || ''); + const user = await env.DB.prepare( + 'SELECT id, email, display_name, password_hash, password_salt FROM comment_users WHERE email = ?' + ).bind(email).first(); + if (!user) return errorResponse('Incorrect email or password', 401); + const actualHash = await hashCommentPassword(password, user.password_salt); + if (!timingSafeEqual(actualHash, user.password_hash)) { + return errorResponse('Incorrect email or password', 401); + } + clearLoginRateLimit(`comment:${ip}`); + return createCommentSession(request, env, user); +} + +export async function handleCommentLogout(request, env) { + const cookies = parseCookies(request.headers.get('Cookie') || ''); + const token = cookies[COMMENT_SESSION_COOKIE]; + if (token) { + await env.DB.prepare('DELETE FROM comment_sessions WHERE token_hash = ?') + .bind(await sha256Hex(token)).run(); + } + return new Response(JSON.stringify({ ok: true }), { + headers: { + 'Content-Type': 'application/json', + 'Set-Cookie': commentSessionCookie(request, '', 0), + }, + }); +} diff --git a/services/recordly-share/worker/src/auth.js b/services/recordly-share/worker/src/auth.js new file mode 100644 index 000000000..e25d3e664 --- /dev/null +++ b/services/recordly-share/worker/src/auth.js @@ -0,0 +1,219 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { generateSalt, sha256Hex, timingSafeEqual, hashRecordingPassword } from "./crypto.js"; +import { errorResponse, jsonResponse, parseCookies } from "./http.js"; + +export async function isAuthorized(request, env) { + const auth = request.headers.get("Authorization"); + if (!auth) return false; + const match = /^Bearer ([^\s]+)$/.exec(auth); + if (!match) return false; + const token = match[1]; + if (token.length > 8192) return false; + if ( + env.ALLOW_API_SECRET_UPLOADS === "true" && + env.API_SECRET && + timingSafeEqual(token, env.API_SECRET) + ) + return true; + if (!env.SUPABASE_URL || !env.SUPABASE_PUBLISHABLE_KEY || !env.OWNER_USER_ID) return false; + try { + const authBase = new URL(env.SUPABASE_URL); + const isLocal = authBase.hostname === "localhost" || authBase.hostname === "127.0.0.1"; + if (authBase.protocol !== "https:" && !(authBase.protocol === "http:" && isLocal)) + return false; + const userUrl = new URL("/auth/v1/user", authBase); + const response = await fetch(userUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: env.SUPABASE_PUBLISHABLE_KEY, + }, + }); + if (!response.ok) return false; + const user = await response.json(); + return typeof user.id === "string" && timingSafeEqual(user.id, env.OWNER_USER_ID); + } catch { + return false; + } +} + +// Hosted accounts never inherit self-host API-secret or dashboard-cookie access. +export async function hostedUser(request, env) { + const auth = request.headers.get("Authorization") || ""; + if (!/^Bearer [^\s]+$/.test(auth) || auth.length > 8200) return null; + if (!env.SUPABASE_URL || !env.SUPABASE_PUBLISHABLE_KEY) return null; + try { + const base = new URL(env.SUPABASE_URL); + if (base.protocol !== "https:") return null; + const response = await fetch(new URL("/auth/v1/user", base), { + headers: { Authorization: auth, apikey: env.SUPABASE_PUBLISHABLE_KEY }, + redirect: "error", + signal: AbortSignal.timeout(10000), + }); + if (!response.ok) return null; + const user = await response.json(); + if ( + typeof user.id !== "string" || + !user.id || + !user.email || + !user.email_confirmed_at || + user.is_anonymous + ) + return null; + if (env.STAGING_ALLOWED_USER_ID && user.id !== env.STAGING_ALLOWED_USER_ID) return null; + return { id: user.id }; + } catch { + return null; + } +} + +async function generateAuthToken(shareCode, expiresAt, apiSecret) { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + encoder.encode(apiSecret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(shareCode + expiresAt)); + return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join(""); +} + +export async function verifyPasswordAuth(request, env, shareCode, video) { + if (!video.password_hash) return true; + if (!env.API_SECRET) return false; + const cookies = parseCookies(request.headers.get("Cookie") || ""); + const authToken = cookies[`voom_auth_${shareCode}`]; + if (!authToken) return false; + const expected = await generateAuthToken(shareCode, video.expires_at, env.API_SECRET); + return timingSafeEqual(authToken, expected); +} + +// --- Dashboard session helpers --- + +export function dashboardPassword(env) { + return env.DASHBOARD_PASSWORD || env.API_SECRET; +} + +export async function expectedSessionToken( + env, + expiresAt = Math.floor(Date.now() / 1000) + 604800, +) { + const password = dashboardPassword(env); + if (!password) throw new Error("Dashboard sign-in is not configured"); + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + encoder.encode(password), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const sig = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(`voom-dashboard-v2:${expiresAt}`), + ); + return ( + `${expiresAt}.` + + Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join("") + ); +} + +export async function isDashboardAuthed(request, env) { + if (env.HOSTED_MODE === "true") return false; + return (await isAuthorized(request, env)) || dashboardCookieAuthed(request, env); +} + +export async function dashboardCookieAuthed(request, env) { + if (!dashboardPassword(env)) return false; + const cookies = parseCookies(request.headers.get("Cookie") || ""); + const sessionToken = cookies["voom_session"]; + if (!sessionToken || !/^\d+\.[0-9a-f]{64}$/.test(sessionToken)) return false; + const expiresAt = Number(sessionToken.split(".")[0]); + if (!Number.isSafeInteger(expiresAt) || expiresAt <= Math.floor(Date.now() / 1000)) + return false; + return timingSafeEqual(sessionToken, await expectedSessionToken(env, expiresAt)); +} + +// best-effort, per-isolate login rate limiter (real protection is the password's entropy) +const _loginAttempts = new Map(); + +export function checkLoginRateLimit(ip) { + const now = Date.now(); + const entry = _loginAttempts.get(ip); + if (entry && now < entry.resetAt) { + if (entry.count >= 10) return false; + entry.count++; + } else { + _loginAttempts.set(ip, { count: 1, resetAt: now + 5 * 60 * 1000 }); + } + return true; +} + +export function clearLoginRateLimit(ip) { + _loginAttempts.delete(ip); +} + +// --- Password Verification --- + +export async function handleVerifyPassword(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT * FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')", + ) + .bind(shareCode) + .first(); + + if (!video || !video.password_hash) return errorResponse("Not found", 404); + if (!env.API_SECRET) return errorResponse("Password protection is not configured", 503); + + // Brute-force protection: 10 attempts per IP per video per 5 minutes. + const clientIP = request.headers.get("CF-Connecting-IP") || "unknown"; + const recent = await env.DB.prepare( + "SELECT COUNT(*) as cnt FROM password_attempts WHERE video_id = ? AND client_ip = ? AND datetime(attempted_at) > datetime('now', '-5 minutes')", + ) + .bind(video.id, clientIP) + .first(); + if (recent && recent.cnt >= 10) + return errorResponse("Too many attempts — try again later", 429); + + const body = await request.json(); + const password = body.password || ""; + + const clientHash = await sha256Hex(password); + const slowHash = video.password_hash.startsWith("pbkdf2-sha256:210000:"); + const actual = slowHash + ? await hashRecordingPassword(clientHash, video.password_salt) + : video.password_salt + ? await sha256Hex(video.password_salt + clientHash) + : clientHash; + const matches = timingSafeEqual(actual, video.password_hash); + if (matches && !slowHash) { + const salt = generateSalt(); + const upgraded = await hashRecordingPassword(clientHash, salt); + await env.DB.prepare("UPDATE videos SET password_hash = ?, password_salt = ? WHERE id = ?") + .bind(upgraded, salt, video.id) + .run(); + } + + if (!matches) { + await env.DB.prepare("INSERT INTO password_attempts (video_id, client_ip) VALUES (?, ?)") + .bind(video.id, clientIP) + .run(); + return jsonResponse({ error: "Incorrect password" }, 403); + } + + // Issue an HMAC session token (never the stored hash). + const authToken = await generateAuthToken(shareCode, video.expires_at, env.API_SECRET); + const expires = new Date(video.expires_at + "Z"); + + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": `voom_auth_${shareCode}=${authToken}; Path=/; Expires=${expires.toUTCString()}; HttpOnly; SameSite=Lax; Secure`, + }, + }); +} diff --git a/services/recordly-share/worker/src/crypto.js b/services/recordly-share/worker/src/crypto.js new file mode 100644 index 000000000..532ff105a --- /dev/null +++ b/services/recordly-share/worker/src/crypto.js @@ -0,0 +1,36 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + + + +// Constant-time string comparison — avoids leaking match length via timing. +export function timingSafeEqual(a, b) { + if (typeof a !== 'string' || typeof b !== 'string') return false; + const enc = new TextEncoder(); + const ab = enc.encode(a); + const bb = enc.encode(b); + let diff = ab.length ^ bb.length; + const len = Math.max(ab.length, bb.length); + for (let i = 0; i < len; i++) { + diff |= (ab[i % ab.length] ?? 0) ^ (bb[i % bb.length] ?? 0); + } + return diff === 0; +} + +export async function sha256Hex(input) { + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); + return Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join(''); +} + +export function generateSalt() { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); +} + +// The prefix versions the algorithm and work factor without changing legacy rows. +export async function hashRecordingPassword(clientHash, salt) { + const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(clientHash), 'PBKDF2', false, ['deriveBits']); + const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: new TextEncoder().encode(salt), iterations: 210000 }, key, 256); + return 'pbkdf2-sha256:210000:' + Array.from(new Uint8Array(bits), b => b.toString(16).padStart(2, '0')).join(''); +} diff --git a/services/recordly-share/worker/src/feedback.js b/services/recordly-share/worker/src/feedback.js new file mode 100644 index 000000000..cbfbce0ad --- /dev/null +++ b/services/recordly-share/worker/src/feedback.js @@ -0,0 +1,131 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { errorResponse, jsonResponse } from './http.js'; +import { verifyPasswordAuth } from './auth.js'; + +// --- Reactions --- + +export async function handleReact(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT id, password_hash, expires_at FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')" + ).bind(shareCode).first(); + + if (!video) return errorResponse('Not found', 404); + + // Password check + if (video.password_hash) { + const authed = await verifyPasswordAuth(request, env, shareCode, video); + if (!authed) return errorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const { timestamp, emoji } = body; + const allowedEmojis = ['👍', '❤️', '😂', '😮', '🔥', '👏']; + if (!allowedEmojis.includes(emoji)) return errorResponse('Invalid emoji'); + if (typeof timestamp !== 'number' || timestamp < 0) return errorResponse('Invalid timestamp'); + + // Rate limit: 50 reactions per IP per video + const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown'; + const count = await env.DB.prepare( + 'SELECT COUNT(*) as cnt FROM reactions WHERE video_id = ? AND client_ip = ?' + ).bind(video.id, clientIP).first(); + if (count && count.cnt >= 50) return errorResponse('Rate limit exceeded', 429); + + await env.DB.prepare( + 'INSERT INTO reactions (video_id, timestamp, emoji, client_ip) VALUES (?, ?, ?, ?)' + ).bind(video.id, timestamp, emoji, clientIP).run(); + + return jsonResponse({ ok: true }); +} + +export async function handleGetReactions(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT id, password_hash, expires_at FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')" + ).bind(shareCode).first(); + + if (!video) return errorResponse('Not found', 404); + + // Same gate as POST /react — listing must not bypass the password. + const authed = await verifyPasswordAuth(request, env, shareCode, video); + if (!authed) return errorResponse('Password required', 401); + + const reactions = await env.DB.prepare( + 'SELECT timestamp, emoji, created_at FROM reactions WHERE video_id = ? ORDER BY created_at DESC LIMIT 500' + ).bind(video.id).all(); + + return jsonResponse({ reactions: reactions.results || [] }); +} + +// --- Comments --- + +export async function handleComment(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT id, password_hash, expires_at FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')" + ).bind(shareCode).first(); + + if (!video) return errorResponse('Not found', 404); + + // Password check + if (video.password_hash) { + const authed = await verifyPasswordAuth(request, env, shareCode, video); + if (!authed) return errorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const { timestamp, text, author_name: authorName } = body; + if (typeof timestamp !== 'number' || timestamp < 0) return errorResponse('Invalid timestamp'); + if (!authorName || typeof authorName !== 'string' || authorName.trim().length === 0) { + return errorResponse('Display name is required'); + } + if (authorName.length > 100) return errorResponse('Display name is too long'); + if (!text || text.trim().length === 0) return errorResponse('Text is required'); + if (text.length > 2000) return errorResponse('Text too long'); + + // Rate limit: 5 comments per IP per 5 minutes + const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown'; + const recent = await env.DB.prepare( + "SELECT COUNT(*) as cnt FROM comments WHERE video_id = ? AND client_ip = ? AND datetime(created_at) > datetime('now', '-5 minutes')" + ).bind(video.id, clientIP).first(); + if (recent && recent.cnt >= 5) return errorResponse('Rate limit exceeded', 429); + + const inserted = await env.DB.prepare( + 'INSERT INTO comments (video_id, timestamp, author_name, text, client_ip) VALUES (?, ?, ?, ?, ?)' + ).bind(video.id, timestamp, authorName.trim(), text.trim().substring(0, 2000), clientIP).run(); + + return jsonResponse({ ok: true, id: inserted.meta.last_row_id }); +} + +export async function handleGetComments(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT id, password_hash, expires_at FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')" + ).bind(shareCode).first(); + + if (!video) return errorResponse('Not found', 404); + + // Same gate as POST /comment — viewer comments can be sensitive. + const authed = await verifyPasswordAuth(request, env, shareCode, video); + if (!authed) return errorResponse('Password required', 401); + + const url = new URL(request.url); + const requestedPage = parseInt(url.searchParams.get('page') || '1', 10); + const requestedLimit = parseInt(url.searchParams.get('limit') || '50', 10); + const page = Number.isSafeInteger(requestedPage) ? Math.max(1, Math.min(requestedPage, Math.floor(Number.MAX_SAFE_INTEGER / 100))) : 1; + const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(requestedLimit, 100)) : 50; + const offset = (page - 1) * limit; + + const total = await env.DB.prepare( + 'SELECT COUNT(*) as cnt FROM comments WHERE video_id = ?' + ).bind(video.id).first(); + + const comments = await env.DB.prepare( + 'SELECT id, timestamp, author_name, text, created_at FROM comments WHERE video_id = ? ORDER BY timestamp ASC, id ASC LIMIT ? OFFSET ?' + ).bind(video.id, limit, offset).all(); + + return jsonResponse({ + comments: comments.results || [], + total: total ? total.cnt : 0, + page, + limit, + }); +} diff --git a/services/recordly-share/worker/src/hostedUploads.js b/services/recordly-share/worker/src/hostedUploads.js new file mode 100644 index 000000000..fab2ff617 --- /dev/null +++ b/services/recordly-share/worker/src/hostedUploads.js @@ -0,0 +1,301 @@ +import { errorResponse, jsonResponse } from "./http.js"; +import { handleDelete } from "./library.js"; +import { handleMetadata, decodeUploadId } from "./uploads.js"; + +// Match the desktop uploader's chunks. Values are bytes, not client assertions. +export const MAX_RECORDING_BYTES = 1_000_000_000; +export const PART_BYTES = 25 * 1024 * 1024; +const THUMBNAIL_BYTES = 2 * 1024 * 1024; +const TYPES = new Set(["video/mp4", "video/webm", "video/quicktime", "image/gif"]); + +export async function limitedJson(request, max = 1024 * 1024) { + const reader = request.body?.getReader(); + if (!reader) throw new Error("INVALID_JSON"); + let size = 0; + const chunks = []; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > max) { + await reader.cancel(); + throw new Error("BODY_TOO_LARGE"); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + try { + const value = JSON.parse(new TextDecoder().decode(bytes)); + if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(); + return value; + } catch { + throw new Error("INVALID_JSON"); + } +} + +// Serialize upload/delete/finalize for a recording across Worker isolates. A crash +// leaves a lock for maintenance instead of allowing a competing writer through. +export async function withRecordingLock(env, code, action) { + const token = crypto.randomUUID(); + const result = await env.DB.prepare( + "INSERT OR IGNORE INTO recording_locks (share_code, token) VALUES (?, ?)", + ) + .bind(code, token) + .run(); + if (!result.meta.changes) return errorResponse("Recording is busy. Try again shortly.", 409); + try { + return await action(); + } finally { + await env.DB.prepare("DELETE FROM recording_locks WHERE share_code = ? AND token = ?") + .bind(code, token) + .run(); + } +} + +async function exactUpload(request, length, consume) { + if (!request.body || request.headers.get("Content-Length") !== String(length)) { + return errorResponse("Upload length does not match the reserved size", 400); + } + // Cloudflare enforces too many AND too few bytes while streaming to R2; + // never buffer a recording or trust Content-Length alone. + // biome-ignore lint/correctness/noUndeclaredVariables: FixedLengthStream is a Cloudflare Workers runtime global. + const stream = new FixedLengthStream(length); + const results = await Promise.allSettled([ + request.body.pipeTo(stream.writable, { signal: AbortSignal.timeout(5 * 60 * 1000) }), + consume(stream.readable), + ]); + if (results[0].status === "rejected") + return errorResponse("Upload body does not match the reserved size", 400); + if (results[1].status === "rejected") { + if (/FixedLengthStream|fixed.length|expected bytes/i.test(String(results[1].reason))) + return errorResponse("Upload body does not match the reserved size", 400); + throw results[1].reason; + } + return jsonResponse({ ok: true, ...(results[1].value?.partNumber ? results[1].value : {}) }); +} + +export async function abortStoredMultipart(env, code) { + const session = await env.DB.prepare( + "SELECT upload_id FROM recording_uploads WHERE share_code = ?", + ) + .bind(code) + .first(); + if (session) { + // R2 abort is idempotent. Do not swallow service failures and release quota + // while parts might still occupy storage. + await env.VIDEOS_BUCKET.resumeMultipartUpload( + `videos/${code}.mp4`, + session.upload_id, + ).abort(); + await env.DB.prepare("DELETE FROM recording_uploads WHERE share_code = ?").bind(code).run(); + } +} + +export async function handleHostedRecording(request, env, ownerId, route, code, rawId, rawPart) { + const owned = await env.DB.prepare( + "SELECT id FROM videos WHERE share_code = ? AND owner_id = ?", + ) + .bind(code, ownerId) + .first(); + if (!owned) return errorResponse("Video not found", 404); + return withRecordingLock(env, code, async () => { + const video = await env.DB.prepare( + "SELECT * FROM videos WHERE share_code = ? AND owner_id = ?", + ) + .bind(code, ownerId) + .first(); + if (!video) return errorResponse("Video not found", 404); + const key = `videos/${code}.mp4`; + if (route === "delete") { + await abortStoredMultipart(env, code); + return handleDelete(env, code); + } + // A renewal endpoint must not let free accounts bypass their fixed expiry. + if (route === "renew") + return errorResponse( + "Free recordings expire after 14 days and cannot be renewed.", + 403, + ); + if ( + !video.upload_completed && + Date.parse(`${video.created_at.replace(" ", "T")}Z`) < Date.now() - 86400000 + ) { + return errorResponse("Upload expired. Delete it and start again.", 410); + } + if (route === "metadata") { + const body = await limitedJson(request); + const object = await env.VIDEOS_BUCKET.head(key); + if (!object || object.size !== video.file_size) + return errorResponse("Upload the complete recording before publishing", 409); + if (video.upload_completed) return jsonResponse({ ok: true }); + if ( + (body.title !== undefined && + (typeof body.title !== "string" || + !body.title.trim() || + body.title.length > 200)) || + (body.summary !== undefined && + (typeof body.summary !== "string" || body.summary.length > 10000)) || + (body.isMeeting !== undefined && typeof body.isMeeting !== "boolean") + ) + return errorResponse("Invalid recording metadata"); + for (const [name, max] of [ + ["segments", 5000], + ["chapters", 200], + ]) { + if ( + body[name] !== undefined && + (!Array.isArray(body[name]) || body[name].length > max) + ) + return errorResponse("Too much recording metadata"); + } + if ( + (body.segments || []).some( + (s) => + !s || + !Number.isFinite(s.startTime) || + !Number.isFinite(s.endTime) || + s.startTime < 0 || + s.endTime < s.startTime || + typeof s.text !== "string" || + s.text.length > 2000 || + (s.speaker != null && + (typeof s.speaker !== "string" || s.speaker.length > 100)), + ) + ) + return errorResponse("Invalid transcript"); + if ( + (body.chapters || []).some( + (c) => + !c || + !Number.isFinite(c.timestamp) || + c.timestamp < 0 || + typeof c.title !== "string" || + c.title.length > 200, + ) + ) + return errorResponse("Invalid chapters"); + // A failed metadata attempt may have inserted some chunks. Retry replaces + // those chunks rather than accumulating duplicates. + await env.DB.batch([ + env.DB.prepare("DELETE FROM transcript_segments WHERE video_id = ?").bind(video.id), + env.DB.prepare("DELETE FROM chapters WHERE video_id = ?").bind(video.id), + ]); + return handleMetadata( + new Request(request.url, { method: "POST", body: JSON.stringify(body) }), + env, + code, + ); + } + if (route === "upload-thumbnail") { + const length = Number(request.headers.get("Content-Length")); + if (!Number.isSafeInteger(length) || length <= 0 || length > THUMBNAIL_BYTES) + return errorResponse("Thumbnail must be at most 2 MB", 413); + if (request.headers.get("Content-Type") !== "image/jpeg") + return errorResponse("Thumbnail must be JPEG"); + return exactUpload(request, length, (stream) => + env.VIDEOS_BUCKET.put(`thumbnails/${code}.jpg`, stream, { + httpMetadata: { contentType: "image/jpeg" }, + }), + ); + } + // Posters may arrive after metadata publishes the recording. Media remains immutable. + if (video.upload_completed) + return errorResponse("Published recordings cannot be overwritten", 409); + const session = await env.DB.prepare( + "SELECT upload_id FROM recording_uploads WHERE share_code = ?", + ) + .bind(code) + .first(); + const object = await env.VIDEOS_BUCKET.head(key); + if (route === "upload-data") { + if (session) return errorResponse("A multipart upload is already in progress", 409); + if (video.file_size > 50 * 1024 * 1024) + return errorResponse("Use multipart upload for recordings over 50 MB", 413); + if (!TYPES.has(request.headers.get("Content-Type"))) + return errorResponse("Unsupported recording type"); + if (object) + return object.size === video.file_size + ? jsonResponse({ ok: true }) + : errorResponse("Stored recording size mismatch", 409); + return exactUpload(request, video.file_size, (stream) => + env.VIDEOS_BUCKET.put(key, stream, { + httpMetadata: { contentType: request.headers.get("Content-Type") }, + }), + ); + } + if (route === "upload-multipart") { + if (object) return errorResponse("Recording bytes have already been uploaded", 409); + if (session) return jsonResponse({ uploadId: session.upload_id }); + const upload = await env.VIDEOS_BUCKET.createMultipartUpload(key, { + httpMetadata: { contentType: "video/mp4" }, + }); + try { + await env.DB.prepare( + "INSERT INTO recording_uploads (share_code, upload_id) VALUES (?, ?)", + ) + .bind(code, upload.uploadId) + .run(); + } catch (error) { + await upload.abort(); + throw error; + } + return jsonResponse({ uploadId: upload.uploadId }); + } + const uploadId = decodeUploadId(rawId || ""); + if (!uploadId || !session || session.upload_id !== uploadId) + return errorResponse("Upload session not found", 404); + if (route === "upload-abort") { + await abortStoredMultipart(env, code); + return jsonResponse({ ok: true }); + } + if (object) + return route === "upload-complete" && object.size === video.file_size + ? jsonResponse({ ok: true }) + : errorResponse("Recording bytes have already been uploaded", 409); + const upload = env.VIDEOS_BUCKET.resumeMultipartUpload(key, uploadId); + const count = Math.ceil(video.file_size / PART_BYTES); + if (route === "upload-part") { + const number = Number(rawPart); + if (!Number.isInteger(number) || number < 1 || number > count) + return errorResponse("Invalid part number"); + const length = Math.min(PART_BYTES, video.file_size - (number - 1) * PART_BYTES); + return exactUpload(request, length, (stream) => upload.uploadPart(number, stream)); + } + if (route === "upload-complete") { + const body = await limitedJson(request, 65536); + if ( + !Array.isArray(body.parts) || + body.parts.length !== count || + body.parts.some( + (part, i) => + !part || + part.partNumber !== i + 1 || + typeof part.etag !== "string" || + !part.etag || + part.etag.length > 200, + ) + ) + return errorResponse("Invalid multipart manifest"); + const result = await upload.complete(body.parts); + if (result.size !== video.file_size) { + await env.VIDEOS_BUCKET.delete(key); + await env.DB.prepare("DELETE FROM recording_uploads WHERE share_code = ?") + .bind(code) + .run(); + return errorResponse("Completed recording size mismatch", 400); + } + // Keep the ID until deletion for retry idempotence and crash recovery. + return jsonResponse({ ok: true }); + } + return errorResponse("Not found", 404); + }); +} diff --git a/services/recordly-share/worker/src/http.js b/services/recordly-share/worker/src/http.js new file mode 100644 index 000000000..5434db610 --- /dev/null +++ b/services/recordly-share/worker/src/http.js @@ -0,0 +1,24 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + + + +export function jsonResponse(data, status = 200) { + return new Response(JSON.stringify(data), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +export function errorResponse(message, status = 400) { + return jsonResponse({ error: message }, status); +} + +export function parseCookies(cookieStr) { + const cookies = {}; + cookieStr.split(';').forEach(c => { + const [key, ...val] = c.trim().split('='); + if (key) cookies[key] = val.join('='); + }); + return cookies; +} diff --git a/services/recordly-share/worker/src/index.js b/services/recordly-share/worker/src/index.js new file mode 100644 index 000000000..b82ee7371 --- /dev/null +++ b/services/recordly-share/worker/src/index.js @@ -0,0 +1,48 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { handleRequest } from "./router.js"; +import { errorResponse } from "./http.js"; +import { ensureSchema } from "./schema.js"; +import { cleanupExpired } from "./library.js"; + +export default { + async fetch(request, env) { + try { + return await handleRequest(request, env); + } catch (e) { + if (e.message === "BODY_TOO_LARGE") + return errorResponse("Request body is too large", 413); + if (e.message === "INVALID_JSON") return errorResponse("Invalid JSON body", 400); + // D1/R2 transient failures and bugs land here instead of surfacing as + // opaque Workers runtime errors. Keep the response app-shaped (JSON). + console.error( + "unhandled error", + request.method, + new URL(request.url).pathname, + e.message, + e.stack, + ); + return errorResponse("Internal error", 500); + } + }, + + async scheduled(event, env) { + try { + await ensureSchema(env); + await cleanupExpired(env); + } catch (e) { + console.error("cron cleanup failed", e.message, e.stack); + throw e; // Surface failed runs to Cloudflare monitoring instead of reporting success. + } + }, +}; + +// Kept for existing consumers and the Worker integration tests. +export { __resetSchemaCacheForTests } from "./schema.js"; +export { generateShareCode } from "./uploads.js"; +export { escapeHTML } from "./media.js"; +export { parseCookies } from "./http.js"; +export { timingSafeEqual, sha256Hex, generateSalt } from "./crypto.js"; +export { formatVTTTime } from "./media.js"; +export { expectedSessionToken, dashboardPassword } from "./auth.js"; diff --git a/services/recordly-share/worker/src/library.js b/services/recordly-share/worker/src/library.js new file mode 100644 index 000000000..a99659964 --- /dev/null +++ b/services/recordly-share/worker/src/library.js @@ -0,0 +1,133 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { abortStoredMultipart, withRecordingLock } from "./hostedUploads.js"; +import { EXPIRY_DAYS } from "./video.js"; +import { errorResponse, jsonResponse } from "./http.js"; + +export async function handleRenew(env, shareCode) { + const newExpiry = new Date(Date.now() + EXPIRY_DAYS * 24 * 60 * 60 * 1000).toISOString(); + + const result = await env.DB.prepare("UPDATE videos SET expires_at = ? WHERE share_code = ?") + .bind(newExpiry, shareCode) + .run(); + + if (result.meta.changes === 0) return errorResponse("Video not found", 404); + + return jsonResponse({ expiresAt: newExpiry }); +} + +export async function handleDelete(env, shareCode) { + const video = await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?") + .bind(shareCode) + .first(); + if (!video) return errorResponse("Video not found", 404); + + await Promise.all([ + env.VIDEOS_BUCKET.delete(`videos/${shareCode}.mp4`), + env.VIDEOS_BUCKET.delete(`thumbnails/${shareCode}.jpg`), + ]); + await env.DB.batch(deleteVideoStatements(env, video.id)); + + return jsonResponse({ ok: true }); +} + +// One round-trip delete of a video and all child rows. Explicit child deletes +// rather than relying on ON DELETE CASCADE: legacy self-host databases were +// created from a base schema without cascade on every table. +function deleteVideoStatements(env, videoId) { + return [ + env.DB.prepare("DELETE FROM chapters WHERE video_id = ?").bind(videoId), + env.DB.prepare("DELETE FROM reactions WHERE video_id = ?").bind(videoId), + env.DB.prepare("DELETE FROM comments WHERE video_id = ?").bind(videoId), + env.DB.prepare("DELETE FROM transcript_segments WHERE video_id = ?").bind(videoId), + env.DB.prepare("DELETE FROM password_attempts WHERE video_id = ?").bind(videoId), + env.DB.prepare("DELETE FROM videos WHERE id = ?").bind(videoId), + ]; +} + +// --- Cron Cleanup --- + +export async function cleanupExpired(env) { + if (env.HOSTED_MODE === "true") { + // Release only crash locks older than a full day; live requests cannot run + // this long. Then use the same per-recording lock as interactive mutations. + await env.DB.prepare( + "DELETE FROM recording_locks WHERE datetime(created_at) < datetime('now', '-1 day')", + ).run(); + const stale = await env.DB.prepare( + "SELECT share_code FROM videos WHERE owner_id IS NOT NULL AND (datetime(expires_at) < datetime('now') OR (upload_completed = 0 AND datetime(created_at) < datetime('now', '-1 day'))) LIMIT 100", + ).all(); + for (const row of stale.results || []) { + await withRecordingLock(env, row.share_code, async () => { + const eligible = await env.DB.prepare( + "SELECT id FROM videos WHERE share_code = ? AND (datetime(expires_at) < datetime('now') OR (upload_completed = 0 AND datetime(created_at) < datetime('now', '-1 day')))", + ) + .bind(row.share_code) + .first(); + if (!eligible) return; + await abortStoredMultipart(env, row.share_code); + await handleDelete(env, row.share_code); + }); + } + return; + } + const expired = await env.DB.prepare( + "SELECT id, share_code FROM videos WHERE datetime(expires_at) < datetime('now')", + ).all(); + + for (const video of expired.results || []) { + await Promise.all([ + env.VIDEOS_BUCKET.delete(`videos/${video.share_code}.mp4`), + env.VIDEOS_BUCKET.delete(`thumbnails/${video.share_code}.jpg`), + ]); + await env.DB.batch(deleteVideoStatements(env, video.id)); + } + + // Drop stale password rate-limit rows so the table can't grow unboundedly. + await env.DB.prepare( + "DELETE FROM password_attempts WHERE datetime(attempted_at) < datetime('now', '-1 day')", + ).run(); + await env.DB.prepare( + "DELETE FROM comment_sessions WHERE datetime(expires_at) < datetime('now')", + ).run(); +} + +// --- Check Views (authenticated) --- + +export async function handleCheckViews(request, env, ownerId = null) { + const body = await request.json(); + const { shareCodes } = body; + if (!Array.isArray(shareCodes) || shareCodes.length === 0) + return errorResponse("shareCodes required"); + if (shareCodes.length > 90) return errorResponse("Too many shareCodes (max 90)"); + + const placeholders = shareCodes.map(() => "?").join(","); + const results = await env.DB.prepare( + `SELECT share_code, view_count FROM videos WHERE share_code IN (${placeholders}) AND (? IS NULL OR owner_id = ?)`, + ) + .bind(...shareCodes, ownerId, ownerId) + .all(); + + const views = {}; + for (const row of results.results || []) { + views[row.share_code] = row.view_count || 0; + } + + return jsonResponse({ views }); +} + +// --- Library: list all shared videos (dashboard) --- + +export async function handleListVideos(env, ownerId = null) { + const rows = await env.DB.prepare( + `SELECT share_code, title, duration, width, height, file_size, created_at, expires_at, + view_count, is_meeting, summary, upload_completed, (password_hash IS NOT NULL) AS is_protected + FROM videos + WHERE (upload_completed = 1 OR ? IS NOT NULL) AND (? IS NULL OR owner_id = ?) + ORDER BY datetime(created_at) DESC`, + ) + .bind(ownerId, ownerId, ownerId) + .all(); + return jsonResponse({ videos: rows.results || [] }); +} diff --git a/services/recordly-share/worker/src/media.js b/services/recordly-share/worker/src/media.js new file mode 100644 index 000000000..1741b699c --- /dev/null +++ b/services/recordly-share/worker/src/media.js @@ -0,0 +1,334 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { verifyPasswordAuth } from './auth.js'; +import { jsonResponse } from './http.js'; +import { finiteNonnegative } from './video.js'; + +function formatDuration(seconds) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + return `${m}:${String(s).padStart(2, '0')}`; +} + +function formatDate(isoString) { + const d = new Date(isoString + 'Z'); + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +export function formatVTTTime(seconds) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + const ms = Math.floor((s % 1) * 1000); + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(Math.floor(s)).padStart(2, '0')}.${String(ms).padStart(3, '0')}`; +} + +export function escapeHTML(str) { + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); +} + +function formatTimestamp(seconds) { + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${m}:${String(s).padStart(2, '0')}`; +} + +// --- Video Streaming --- + +export async function handleVideoStream(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT * FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')" + ) + .bind(shareCode) + .first(); + + if (!video) return new Response('Not found', { status: 404 }); + + // Password protection check for video stream + if (video.password_hash) { + const authed = await verifyPasswordAuth(request, env, shareCode, video); + if (!authed) return new Response('Unauthorized', { status: 401 }); + } + + const key = `videos/${shareCode}.mp4`; + const rangeHeader = request.headers.get('Range'); + + let object; + if (rangeHeader) { + const match = rangeHeader.match(/bytes=(\d*)-(\d*)/); + if (match && (match[1] || match[2])) { + const suffix = !match[1]; // "bytes=-N" — last N bytes + let r2Range; + if (suffix) { + r2Range = { suffix: parseInt(match[2], 10) }; + } else { + const start = parseInt(match[1], 10); + const end = match[2] ? parseInt(match[2], 10) : undefined; + r2Range = { offset: start, length: end !== undefined ? end - start + 1 : undefined }; + } + + try { + object = await env.VIDEOS_BUCKET.get(key, { range: r2Range }); + } catch (_e) { + // R2 throws on an unsatisfiable range (e.g. offset past end of object). + return new Response('Range not satisfiable', { status: 416 }); + } + + if (!object) return new Response('Not found', { status: 404 }); + + const totalSize = object.size; // R2Object.size is the full stored object size + const start = suffix ? Math.max(0, totalSize - r2Range.suffix) : r2Range.offset; + const actualEnd = !suffix && r2Range.length !== undefined ? Math.min(totalSize - 1, start + r2Range.length - 1) : totalSize - 1; + + return new Response(object.body, { + status: 206, + headers: { + 'Content-Type': 'video/mp4', + 'Content-Range': `bytes ${start}-${actualEnd}/${totalSize}`, + 'Content-Length': String(actualEnd - start + 1), + 'Accept-Ranges': 'bytes', + 'Cache-Control': video.password_hash ? 'private, no-store' : 'public, max-age=3600', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Expose-Headers': 'Content-Range, Content-Length, Accept-Ranges', + }, + }); + } + } + + object = await env.VIDEOS_BUCKET.get(key); + if (!object) return new Response('Not found', { status: 404 }); + + return new Response(object.body, { + status: 200, + headers: { + 'Content-Type': 'video/mp4', + 'Content-Length': String(object.size), + 'Accept-Ranges': 'bytes', + 'Cache-Control': video.password_hash ? 'private, no-store' : 'public, max-age=3600', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Expose-Headers': 'Content-Range, Content-Length, Accept-Ranges', + }, + }); +} + +// --- VTT Captions --- + +export async function handleVTT(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT * FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')" + ).bind(shareCode).first(); + + if (!video) return new Response('Not found', { status: 404 }); + + // Password protection check + if (video.password_hash) { + const authed = await verifyPasswordAuth(request, env, shareCode, video); + if (!authed) return new Response('Unauthorized', { status: 401 }); + } + + const segments = await env.DB.prepare( + 'SELECT start_time, end_time, text, speaker FROM transcript_segments WHERE video_id = ? ORDER BY start_time' + ).bind(video.id).all(); + + let vtt = 'WEBVTT\n\n'; + (segments.results || []).forEach((seg, i) => { + const start = formatVTTTime(seg.start_time); + const end = formatVTTTime(seg.end_time); + const speaker = seg.speaker ? `<v ${seg.speaker}>` : ''; + vtt += `${i + 1}\n${start} --> ${end}\n${speaker}${seg.text}\n\n`; + }); + + return new Response(vtt, { + headers: { + 'Content-Type': 'text/vtt; charset=utf-8', + 'Cache-Control': video.password_hash ? 'private, no-store' : 'public, max-age=3600', + 'Access-Control-Allow-Origin': '*', + }, + }); +} + +// --- Share Data (JSON endpoint for SPA) --- + +export async function handleShareData(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT * FROM videos WHERE share_code = ? AND upload_completed = 1" + ).bind(shareCode).first(); + + if (!video) return jsonResponse({ expired: true }, 404); + + const isExpired = new Date(video.expires_at + 'Z') < new Date(); + if (isExpired) return jsonResponse({ expired: true }, 404); + + if (video.password_hash) { + const authed = await verifyPasswordAuth(request, env, shareCode, video); + if (!authed) { + return jsonResponse({ password_protected: true, title: video.title }, 401); + } + } + + // View-count bump and the two reads are independent — run them together. + const [, segments, chapters] = await Promise.all([ + env.DB.prepare('UPDATE videos SET view_count = view_count + 1 WHERE id = ?').bind(video.id).run(), + env.DB.prepare( + 'SELECT start_time, end_time, text, speaker FROM transcript_segments WHERE video_id = ? ORDER BY start_time' + ).bind(video.id).all(), + env.DB.prepare( + 'SELECT timestamp, title FROM chapters WHERE video_id = ? ORDER BY timestamp' + ).bind(video.id).all(), + ]); + + return jsonResponse({ + video: { + title: video.title, + duration: video.duration, + width: video.width, + height: video.height, + summary: video.summary, + has_webcam: video.has_webcam, + cta_url: video.cta_url, + cta_text: video.cta_text, + created_at: video.created_at, + view_count: (video.view_count || 0) + 1, + is_meeting: video.is_meeting, + }, + segments: (segments.results || []), + chapters: (chapters.results || []), + shareCode, + creator: typeof env.CREATOR_NAME === 'string' && env.CREATOR_NAME.trim() ? { + name: env.CREATOR_NAME.trim().slice(0, 100), + bio: typeof env.CREATOR_BIO === 'string' ? env.CREATOR_BIO.trim().slice(0, 200) : null, + website: typeof env.CREATOR_WEBSITE === 'string' && /^https?:\/\//i.test(env.CREATOR_WEBSITE) ? env.CREATOR_WEBSITE : null, + } : null, + }); +} + +// --- OG Page (bot-only, minimal HTML with meta tags) --- + +export async function handleOGPage(request, env, shareCode) { + const video = await env.DB.prepare( + "SELECT * FROM videos WHERE share_code = ? AND upload_completed = 1" + ).bind(shareCode).first(); + + if (!video) return new Response('Not found', { status: 404 }); + + const isExpired = new Date(video.expires_at + 'Z') < new Date(); + if (isExpired) return new Response('Not found', { status: 404 }); + + const baseUrl = new URL(request.url).origin; + + // Don't leak title/summary/thumbnail of password-protected videos to + // crawlers — the OG path has no way to present the password gate. + if (video.password_hash) { + const lockedHtml = `<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<title>Protected video — Recordly + + + + + + +

This recording is password protected.

+`; + return new Response(lockedHtml, { + headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=3600' }, + }); + } + + const desc = video.summary ? escapeHTML(video.summary) : `${formatTimestamp(video.duration)} screen recording`; + + const html = ` + + + +${escapeHTML(video.title)} — Recordly + + + + + + + + + + + + + + + + + + + + + + + +

${escapeHTML(video.title)}

+ +`; + + return new Response(html, { + headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=3600' }, + }); +} + +// --- OG Image --- + +export async function handleOGImage(env, shareCode) { + const video = await env.DB.prepare( + "SELECT * FROM videos WHERE share_code = ? AND upload_completed = 1 AND datetime(expires_at) > datetime('now')" + ) + .bind(shareCode) + .first(); + + if (!video) return new Response('Not found', { status: 404 }); + + // Serve uploaded thumbnail if available \u2014 but never for password-protected + // videos, whose poster frame may itself be sensitive. + if (!video.password_hash) { + const thumb = await env.VIDEOS_BUCKET.get(`thumbnails/${shareCode}.jpg`); + if (thumb) { + return new Response(thumb.body, { + status: 200, + headers: { + 'Content-Type': 'image/jpeg', + 'Cache-Control': 'public, max-age=86400', + }, + }); + } + } + + // Fallback SVG + const locked = !!video.password_hash; + const duration = formatDuration(video.duration); + const date = formatDate(video.created_at); + const rawTitle = locked ? 'Protected video' : video.title; + const title = rawTitle.length > 60 ? rawTitle.substring(0, 57) + '...' : rawTitle; + const res = !locked && video.width > 0 ? `${finiteNonnegative(video.width)}\u00d7${finiteNonnegative(video.height)}` : ''; + + const svg = ` + + + + + ${escapeHTML(title)} + ${duration} \u00b7 ${date}${res ? ' \u00b7 ' + res : ''} + RECORDLY +`; + + return new Response(svg, { + status: 200, + headers: { + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'public, max-age=86400', + 'X-Content-Type-Options': 'nosniff', + }, + }); +} diff --git a/services/recordly-share/worker/src/router.js b/services/recordly-share/worker/src/router.js new file mode 100644 index 000000000..99e5546e0 --- /dev/null +++ b/services/recordly-share/worker/src/router.js @@ -0,0 +1,397 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { handleHostedRecording, limitedJson } from "./hostedUploads.js"; +import { ensureSchema } from "./schema.js"; +import { + commentViewer, + handleCommentLogin, + handleCommentLogout, + handleCommentRegister, +} from "./accounts.js"; +import { errorResponse, jsonResponse } from "./http.js"; +import { + checkLoginRateLimit, + clearLoginRateLimit, + dashboardCookieAuthed, + dashboardPassword, + expectedSessionToken, + handleVerifyPassword, + hostedUser, + isAuthorized, + isDashboardAuthed, + verifyPasswordAuth, +} from "./auth.js"; +import { timingSafeEqual } from "./crypto.js"; +import { handleCheckViews, handleDelete, handleListVideos, handleRenew } from "./library.js"; +import { + decodeUploadId, + handleMetadata, + handleMultipartAbort, + handleMultipartComplete, + handleMultipartPart, + handleMultipartStart, + handleUpload, + handleUploadData, + handleUploadThumbnail, +} from "./uploads.js"; +import { + handleOGImage, + handleOGPage, + handleShareData, + handleVTT, + handleVideoStream, +} from "./media.js"; +import { handleComment, handleGetComments, handleGetReactions, handleReact } from "./feedback.js"; + +export async function handleRequest(request, env) { + const url = new URL(request.url); + const path = url.pathname; + + const hosted = env.HOSTED_MODE === "true"; + if (hosted) { + if (!env.REQUEST_RATE_LIMIT || !env.ACCOUNT_RATE_LIMIT) + return errorResponse("Hosted rate limits are not configured", 503); + const { success } = await env.REQUEST_RATE_LIMIT.limit({ + key: request.headers.get("CF-Connecting-IP") || "local", + }); + if (!success) return errorResponse("Too many requests. Try again in a minute.", 429); + } + await ensureSchema(env); + // The shared-password dashboard and legacy comment accounts are self-host only. + if (hosted && (path.startsWith("/library") || path.startsWith("/auth/"))) { + return errorResponse("Not found", 404); + } + + if (path === "/auth/session" && request.method === "GET") { + const viewer = await commentViewer(request, env); + return jsonResponse({ + user: viewer ? { email: viewer.email, displayName: viewer.display_name } : null, + }); + } + if (path === "/auth/register" && request.method === "POST") { + return handleCommentRegister(request, env); + } + if (path === "/auth/login" && request.method === "POST") { + return handleCommentLogin(request, env); + } + if (path === "/auth/logout" && request.method === "POST") { + return handleCommentLogout(request, env); + } + + // Library login (public — no bearer required; form POST) + if (path === "/library/login" && request.method === "POST") { + const ip = request.headers.get("CF-Connecting-IP") || "unknown"; + if (!checkLoginRateLimit(ip)) { + return new Response("Too many attempts — try again later", { status: 429 }); + } + const form = await request.formData(); + const password = form.get("password") || ""; + if (timingSafeEqual(password, dashboardPassword(env))) { + clearLoginRateLimit(ip); + const token = await expectedSessionToken(env); + return new Response(null, { + status: 302, + headers: { + Location: "/library", + "Set-Cookie": `voom_session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800`, + }, + }); + } + return new Response(null, { status: 302, headers: { Location: "/library/login?error=1" } }); + } + + // Library logout + if (path === "/library/logout" && request.method === "GET") { + return new Response(null, { + status: 302, + headers: { + Location: "/library/login", + "Set-Cookie": "voom_session=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0", + }, + }); + } + + // Library dashboard (auth-gated) + // Internal asset is /lib.html (not /library.html) so the ASSETS binding + // does not auto-serve it before the worker runs (no run_worker_first needed). + if (path === "/library" && request.method === "GET") { + if (!(await isDashboardAuthed(request, env))) { + return new Response(null, { status: 302, headers: { Location: "/library/login" } }); + } + return env.ASSETS.fetch(new Request(new URL("/lib", url), request)); + } + + // Library login page (public) + if (path === "/library/login" && request.method === "GET") { + return env.ASSETS.fetch(new Request(new URL("/lib-login", url), request)); + } + + // API routes (authenticated) + if (path.startsWith("/api/")) { + if (request.method === "OPTIONS") { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Authorization, Content-Type", + }, + }); + } + + let ownerId = null; + if (hosted) { + const user = await hostedUser(request, env); + if (!user) return errorResponse("Verified sign-in required", 401); + ownerId = user.id; + if (!(await env.ACCOUNT_RATE_LIMIT.limit({ key: ownerId })).success) + return errorResponse("Too many account requests. Try again in a minute.", 429); + if (!["/api/health", "/api/videos", "/api/upload", "/api/check-views"].includes(path)) { + const route = path.match( + /^\/api\/(upload-data|upload-thumbnail|upload-multipart|upload-part|upload-complete|upload-abort|metadata|renew|delete)\/([a-z0-9]+)(?:\/([^/]+)(?:\/(\d+))?)?$/, + ); + if (!route) return errorResponse("Not found", 404); + const expected = + { + "upload-data": "PUT", + "upload-thumbnail": "PUT", + "upload-part": "PUT", + delete: "DELETE", + }[route[1]] || "POST"; + if ( + request.method !== expected || + (route[1] === "upload-part" ? !route[4] : route[4] !== undefined) || + (["upload-part", "upload-complete", "upload-abort"].includes(route[1]) + ? !route[3] + : route[3] !== undefined) + ) + return errorResponse("Not found", 404); + return handleHostedRecording(request, env, ownerId, ...route.slice(1)); + } + } else if ( + !(await isAuthorized(request, env)) && + !(await dashboardCookieAuthed(request, env)) + ) { + return errorResponse("Unauthorized", 401); + } + + // Connection check used by the desktop app to validate a worker URL + secret. + if (path === "/api/health" && request.method === "GET") { + return jsonResponse({ ok: true, app: "recordly" }); + } + + if (path === "/api/videos" && request.method === "GET") { + return handleListVideos(env, ownerId); + } + + if (path === "/api/upload" && request.method === "POST") { + return handleUpload(request, env, ownerId); + } + + const uploadDataMatch = path.match(/^\/api\/upload-data\/([a-z0-9]+)$/); + if (uploadDataMatch && request.method === "PUT") { + return handleUploadData(request, env, uploadDataMatch[1]); + } + + const thumbnailMatch = path.match(/^\/api\/upload-thumbnail\/([a-z0-9]+)$/); + if (thumbnailMatch && request.method === "PUT") { + return handleUploadThumbnail(request, env, thumbnailMatch[1]); + } + + const multipartStartMatch = path.match(/^\/api\/upload-multipart\/([a-z0-9]+)$/); + if (multipartStartMatch && request.method === "POST") { + return handleMultipartStart(request, env, multipartStartMatch[1]); + } + + const multipartPartMatch = path.match(/^\/api\/upload-part\/([a-z0-9]+)\/(.+?)\/(\d+)$/); + if (multipartPartMatch && request.method === "PUT") { + const multipartUploadId = decodeUploadId(multipartPartMatch[2]); + if (!multipartUploadId) return errorResponse("Invalid multipart upload ID"); + return handleMultipartPart( + request, + env, + multipartPartMatch[1], + multipartUploadId, + parseInt(multipartPartMatch[3], 10), + ); + } + + const multipartCompleteMatch = path.match(/^\/api\/upload-complete\/([a-z0-9]+)\/(.+)$/); + if (multipartCompleteMatch && request.method === "POST") { + const multipartUploadId = decodeUploadId(multipartCompleteMatch[2]); + if (!multipartUploadId) return errorResponse("Invalid multipart upload ID"); + return handleMultipartComplete( + request, + env, + multipartCompleteMatch[1], + multipartUploadId, + ); + } + + const multipartAbortMatch = path.match(/^\/api\/upload-abort\/([a-z0-9]+)\/(.+)$/); + if (multipartAbortMatch && request.method === "POST") { + const multipartUploadId = decodeUploadId(multipartAbortMatch[2]); + if (!multipartUploadId) return errorResponse("Invalid multipart upload ID"); + return handleMultipartAbort(request, env, multipartAbortMatch[1], multipartUploadId); + } + + const metadataMatch = path.match(/^\/api\/metadata\/([a-z0-9]+)$/); + if (metadataMatch && request.method === "POST") { + return handleMetadata(request, env, metadataMatch[1]); + } + + const renewMatch = path.match(/^\/api\/renew\/([a-z0-9]+)$/); + if (renewMatch && request.method === "POST") { + return handleRenew(env, renewMatch[1]); + } + + const deleteMatch = path.match(/^\/api\/delete\/([a-z0-9]+)$/); + if (deleteMatch && request.method === "DELETE") { + return handleDelete(env, deleteMatch[1]); + } + + if (path === "/api/check-views" && request.method === "POST") { + return handleCheckViews( + hosted + ? new Request(request.url, { + method: "POST", + body: JSON.stringify(await limitedJson(request, 16384)), + }) + : request, + env, + ownerId, + ); + } + + return errorResponse("Not found", 404); + } + + // CORS preflight for public endpoints + if (request.method === "OPTIONS") { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); + } + + if (hosted && request.method === "POST") { + request = new Request(request, { body: JSON.stringify(await limitedJson(request, 16384)) }); + } + + // Password verification (public) + const verifyMatch = path.match(/^\/s\/([a-z0-9]+)\/verify-password$/); + if (verifyMatch && request.method === "POST") { + return handleVerifyPassword(request, env, verifyMatch[1]); + } + + // Share data endpoint (public, for SPA) + const dataMatch = path.match(/^\/s\/([a-z0-9]+)\/data$/); + if (dataMatch && request.method === "GET") { + return handleShareData(request, env, dataMatch[1]); + } + + // Reactions (public) + const reactMatch = path.match(/^\/s\/([a-z0-9]+)\/react$/); + if (reactMatch && request.method === "POST") { + return handleReact(request, env, reactMatch[1]); + } + const reactGetMatch = path.match(/^\/s\/([a-z0-9]+)\/reactions$/); + if (reactGetMatch && request.method === "GET") { + return handleGetReactions(request, env, reactGetMatch[1]); + } + + // Comments (public) + const commentMatch = path.match(/^\/s\/([a-z0-9]+)\/comment$/); + if (commentMatch && request.method === "POST") { + return handleComment(request, env, commentMatch[1]); + } + const commentGetMatch = path.match(/^\/s\/([a-z0-9]+)\/comments$/); + if (commentGetMatch && request.method === "GET") { + return handleGetComments(request, env, commentGetMatch[1]); + } + + // VTT captions + const vttMatch = path.match(/^\/vtt\/([a-z0-9]+)$/); + if (vttMatch && request.method === "GET") { + return handleVTT(request, env, vttMatch[1]); + } + + // Share page — serve Astro SPA or OG for bots + const shareMatch = path.match(/^\/s\/([a-z0-9]+)$/); + if (shareMatch && request.method === "GET") { + const shareCode = shareMatch[1]; + const ua = request.headers.get("User-Agent") || ""; + if ( + /bot|crawl|spider|facebook|twitter|slack|discord|telegram|whatsapp|linkedin/i.test(ua) + ) { + return handleOGPage(request, env, shareCode); + } + return env.ASSETS.fetch(new Request(new URL("/share", url), request)); + } + + // Video streaming + const videoMatch = path.match(/^\/v\/([a-z0-9]+)$/); + if (videoMatch && request.method === "GET") { + return handleVideoStream(request, env, videoMatch[1]); + } + + // OG image + const ogMatch = path.match(/^\/og\/([a-z0-9]+)$/); + if (ogMatch && request.method === "GET") { + return handleOGImage(env, ogMatch[1]); + } + + // Embed player — serve Astro embed page + const embedMatch = path.match(/^\/embed\/([a-z0-9]+)$/); + if (embedMatch && request.method === "GET") { + return env.ASSETS.fetch(new Request(new URL("/embed", url), request)); + } + + // Thumbnail (high-res poster) — gated like the OG image: the poster frame + // of a password-protected or expired video may itself be sensitive. + const thumbMatch = path.match(/^\/thumb\/([a-z0-9]+)$/); + if (thumbMatch && request.method === "GET") { + const video = await env.DB.prepare( + "SELECT password_hash, expires_at FROM videos WHERE share_code = ? AND datetime(expires_at) > datetime('now')", + ) + .bind(thumbMatch[1]) + .first(); + if (!video) return new Response("Not found", { status: 404 }); + if (!(await verifyPasswordAuth(request, env, thumbMatch[1], video))) { + return new Response("Not found", { status: 404 }); + } + const thumb = await env.VIDEOS_BUCKET.get(`thumbnails/${thumbMatch[1]}.jpg`); + if (thumb) { + return new Response(thumb.body, { + // Recheck the unlock cookie on every protected poster request. + headers: { + "Content-Type": "image/jpeg", + "Cache-Control": video.password_hash + ? "private, no-store" + : "public, max-age=86400", + }, + }); + } + return new Response("Not found", { status: 404 }); + } + + // Static assets from R2 + if (path === "/icon-64.png" && request.method === "GET") { + const obj = await env.VIDEOS_BUCKET.get("static/icon-64.png"); + if (obj) { + return new Response(obj.body, { + headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=604800" }, + }); + } + } + + if (path === "/") { + return new Response("Recordly Share", { status: 200 }); + } + + // Fall through to static assets + return env.ASSETS.fetch(request); +} diff --git a/services/recordly-share/worker/src/schema.js b/services/recordly-share/worker/src/schema.js new file mode 100644 index 000000000..d670f4412 --- /dev/null +++ b/services/recordly-share/worker/src/schema.js @@ -0,0 +1,222 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +// --- Self-bootstrap: runtime schema migration --- +// When deployed via the "Deploy to Cloudflare" button, D1 is auto-provisioned +// empty, so the worker migrates its own schema at runtime on first request. +// Authentication uses the API_SECRET set during deploy (dashboard or prompt). + +const SCHEMA_VERSION = 5; + +// Consolidated current schema (base schema.sql + migrations 0002-0009). All +// statements are idempotent, so this is safe on both fresh (button-deployed) +// and existing (token-deployed) databases. +const SCHEMA_STATEMENTS = [ + `CREATE TABLE IF NOT EXISTS recording_uploads (share_code TEXT PRIMARY KEY REFERENCES videos(share_code) ON DELETE CASCADE, upload_id TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS recording_locks (share_code TEXT PRIMARY KEY, token TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')))`, + + `CREATE TABLE IF NOT EXISTS videos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + share_code TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + duration REAL NOT NULL DEFAULT 0, + width INTEGER NOT NULL DEFAULT 0, + height INTEGER NOT NULL DEFAULT 0, + has_webcam INTEGER NOT NULL DEFAULT 0, + file_size INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT NOT NULL, + upload_completed INTEGER NOT NULL DEFAULT 0, + view_count INTEGER NOT NULL DEFAULT 0, + password_hash TEXT, + cta_url TEXT, + cta_text TEXT, + last_notified_view_count INTEGER NOT NULL DEFAULT 0, + is_meeting INTEGER NOT NULL DEFAULT 0, + summary TEXT, + password_salt TEXT, + owner_id TEXT + )`, + `CREATE INDEX IF NOT EXISTS idx_videos_owner ON videos(owner_id)`, + `CREATE INDEX IF NOT EXISTS idx_videos_share_code ON videos(share_code)`, + `CREATE INDEX IF NOT EXISTS idx_videos_expires_at ON videos(expires_at)`, + `CREATE TABLE IF NOT EXISTS transcript_segments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + start_time REAL NOT NULL, + end_time REAL NOT NULL, + text TEXT NOT NULL, + speaker TEXT + )`, + `CREATE INDEX IF NOT EXISTS idx_transcript_video_id ON transcript_segments(video_id)`, + `CREATE TABLE IF NOT EXISTS reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + emoji TEXT NOT NULL, + client_ip TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE INDEX IF NOT EXISTS idx_reactions_video_id ON reactions(video_id)`, + `CREATE INDEX IF NOT EXISTS idx_reactions_ip ON reactions(video_id, client_ip)`, + `CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + author_name TEXT NOT NULL DEFAULT 'Anonymous', + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + client_ip TEXT NOT NULL DEFAULT '' + )`, + `CREATE INDEX IF NOT EXISTS idx_comments_video_id ON comments(video_id)`, + `CREATE TABLE IF NOT EXISTS chapters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + title TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_chapters_video_id ON chapters(video_id)`, + `CREATE TABLE IF NOT EXISTS password_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL, + client_ip TEXT NOT NULL, + attempted_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE INDEX IF NOT EXISTS idx_password_attempts ON password_attempts(video_id, client_ip, attempted_at)`, + `CREATE TABLE IF NOT EXISTS comment_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + password_salt TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE TABLE IF NOT EXISTS comment_sessions ( + token_hash TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES comment_users(id) ON DELETE CASCADE, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE INDEX IF NOT EXISTS idx_comment_sessions_user ON comment_sessions(user_id)`, + `CREATE INDEX IF NOT EXISTS idx_comment_sessions_expiry ON comment_sessions(expires_at)`, +]; + +// Incremental migrations for databases that are already at an older version. +// SCHEMA_STATEMENTS only covers fresh databases (CREATE TABLE IF NOT EXISTS +// cannot add columns to existing tables), so any change that ALTERs an +// existing table MUST appear here under a bumped SCHEMA_VERSION. +const SCHEMA_MIGRATIONS = { + 5: [ + `CREATE TABLE IF NOT EXISTS recording_uploads (share_code TEXT PRIMARY KEY REFERENCES videos(share_code) ON DELETE CASCADE, upload_id TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS recording_locks (share_code TEXT PRIMARY KEY, token TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')))`, + ], + 4: [ + `ALTER TABLE videos ADD COLUMN owner_id TEXT`, + `CREATE INDEX IF NOT EXISTS idx_videos_owner ON videos(owner_id)`, + ], + 2: [ + `CREATE INDEX IF NOT EXISTS idx_chapters_video_id ON chapters(video_id)`, + `ALTER TABLE videos ADD COLUMN password_salt TEXT`, + `CREATE TABLE IF NOT EXISTS password_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL, + client_ip TEXT NOT NULL, + attempted_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE INDEX IF NOT EXISTS idx_password_attempts ON password_attempts(video_id, client_ip, attempted_at)`, + ], + 3: [ + `CREATE TABLE IF NOT EXISTS comment_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + password_salt TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE TABLE IF NOT EXISTS comment_sessions ( + token_hash TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES comment_users(id) ON DELETE CASCADE, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE INDEX IF NOT EXISTS idx_comment_sessions_user ON comment_sessions(user_id)`, + `CREATE INDEX IF NOT EXISTS idx_comment_sessions_expiry ON comment_sessions(expires_at)`, + ], +}; + +let schemaReady = false; + +// Test-only: lets the vitest suite force ensureSchema to re-run after it +// rebuilds the database into an older shape. Never called in production. +export function __resetSchemaCacheForTests() { + schemaReady = false; +} + +export async function ensureSchema(env) { + if (schemaReady) return; + await env.DB.prepare( + `CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)`, + ).run(); + const row = await env.DB.prepare( + `SELECT value FROM _meta WHERE key = 'schema_version'`, + ).first(); + let current = row ? parseInt(row.value, 10) : 0; + + if (current === 0) { + // No version stamp ≠ fresh: databases created before versioning existed + // have tables but no _meta row. Treating one as fresh would skip the + // ALTERs (CREATE TABLE IF NOT EXISTS no-ops on existing tables). + const videos = await env.DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'videos'`, + ).first(); + if (videos) current = 1; + } + + if (current >= 2) { + // Repair pass: v4.1.0 stamped pre-versioning databases as current without + // running the v2 ALTERs. If anything v2 introduced is missing, rewind so + // the migration loop below re-runs (its statements tolerate re-application). + const cols = await env.DB.prepare(`PRAGMA table_info(videos)`).all(); + const attempts = await env.DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'password_attempts'`, + ).first(); + if (!(cols.results || []).some((c) => c.name === "password_salt") || !attempts) current = 1; + } + + if (current >= 3) { + const commentUsers = await env.DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'comment_users'`, + ).first(); + const commentSessions = await env.DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'comment_sessions'`, + ).first(); + if (!commentUsers || !commentSessions) current = 2; + } + + if (current < SCHEMA_VERSION) { + if (current === 0) { + // Fresh database: the consolidated schema already reflects every migration. + await env.DB.batch(SCHEMA_STATEMENTS.map((sql) => env.DB.prepare(sql))); + } else { + // Existing database: apply each migration step in order. ALTER TABLE + // is not idempotent, so tolerate duplicate-column errors from re-runs. + for (let v = current + 1; v <= SCHEMA_VERSION; v++) { + for (const sql of SCHEMA_MIGRATIONS[v] || []) { + try { + await env.DB.prepare(sql).run(); + } catch (e) { + if (!/duplicate column|already exists/i.test(e.message || "")) throw e; + } + } + } + } + await env.DB.prepare( + `INSERT INTO _meta (key, value) VALUES ('schema_version', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value`, + ) + .bind(String(SCHEMA_VERSION)) + .run(); + } + schemaReady = true; +} diff --git a/services/recordly-share/worker/src/uploads.js b/services/recordly-share/worker/src/uploads.js new file mode 100644 index 000000000..94162bf0e --- /dev/null +++ b/services/recordly-share/worker/src/uploads.js @@ -0,0 +1,277 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +import { limitedJson, MAX_RECORDING_BYTES } from "./hostedUploads.js"; +import { errorResponse, jsonResponse } from "./http.js"; +import { EXPIRY_DAYS, FREE_EXPIRY_DAYS, finiteNonnegative } from "./video.js"; +import { generateSalt, hashRecordingPassword } from "./crypto.js"; + +export function generateShareCode() { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +} + +export async function handleUpload(request, env, ownerId = null) { + const body = ownerId ? await limitedJson(request, 16384) : await request.json(); + const { + title, + duration, + width, + height, + hasWebcam, + fileSize, + password_hash, + cta_url, + cta_text, + } = body; + + if ( + ownerId && + (!Number.isSafeInteger(fileSize) || fileSize <= 0 || fileSize > MAX_RECORDING_BYTES) + ) + return errorResponse("Free recordings must be between 1 byte and 1 GB", 413); + if (ownerId && (typeof title !== "string" || !title.trim() || title.length > 200)) + return errorResponse("Title must be between 1 and 200 characters"); + if ( + ownerId && + ((cta_url && (typeof cta_url !== "string" || cta_url.length > 2048)) || + (cta_text && (typeof cta_text !== "string" || cta_text.length > 200)) || + (password_hash && + (typeof password_hash !== "string" || !/^[a-f0-9]{64}$/.test(password_hash)))) + ) + return errorResponse("Invalid recording options"); + if (!title) return errorResponse("title is required"); + if (password_hash && !env.API_SECRET) + return errorResponse("Password protection is not configured", 503); + + // CTA links render as
on the share page — only allow web URLs so a + // stored javascript:/data: URL can never reach that sink. + if (cta_url && !/^https?:\/\//i.test(cta_url)) { + return errorResponse("cta_url must be an http(s) URL"); + } + + const shareCode = generateShareCode(); + const expiresAt = new Date( + Date.now() + (ownerId ? FREE_EXPIRY_DAYS : EXPIRY_DAYS) * 24 * 60 * 60 * 1000, + ).toISOString(); + + // Versioned slow hash of the client digest, with a unique per-record salt. + let storedHash = null; + let salt = null; + if (password_hash) { + salt = generateSalt(); + storedHash = await hashRecordingPassword(password_hash, salt); + } + + const inserted = await env.DB.prepare( + `INSERT INTO videos (share_code, title, duration, width, height, has_webcam, file_size, expires_at, password_hash, password_salt, cta_url, cta_text, owner_id) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + WHERE ? IS NULL OR (SELECT COUNT(*) FROM videos WHERE owner_id = ?) < 5`, + ) + .bind( + shareCode, + title, + finiteNonnegative(duration), + finiteNonnegative(width), + finiteNonnegative(height), + hasWebcam ? 1 : 0, + finiteNonnegative(fileSize), + expiresAt, + storedHash, + salt, + cta_url || null, + cta_text || null, + ownerId, + ownerId, + ownerId, + ) + .run(); + + // The count and reservation are one SQLite statement: parallel requests cannot + // claim the sixth slot. Pending and expired rows count until explicitly deleted. + if (inserted.meta.changes === 0) + return errorResponse( + "Free accounts can store up to 5 recordings. Delete one before uploading another.", + 409, + ); + + const baseUrl = new URL(request.url).origin; + + return jsonResponse({ + shareCode, + uploadURL: `${baseUrl}/api/upload-data/${shareCode}`, + shareURL: `${baseUrl}/s/${shareCode}`, + expiresAt, + }); +} + +export async function handleUploadData(request, env, shareCode) { + const video = await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?") + .bind(shareCode) + .first(); + if (!video) return errorResponse("Video not found", 404); + + const contentType = request.headers.get("Content-Type") || "video/mp4"; + + await env.VIDEOS_BUCKET.put(`videos/${shareCode}.mp4`, request.body, { + httpMetadata: { contentType }, + }); + + return jsonResponse({ ok: true }); +} + +export async function handleUploadThumbnail(request, env, shareCode) { + const video = await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?") + .bind(shareCode) + .first(); + if (!video) return errorResponse("Video not found", 404); + + const contentType = request.headers.get("Content-Type") || "image/jpeg"; + + await env.VIDEOS_BUCKET.put(`thumbnails/${shareCode}.jpg`, request.body, { + httpMetadata: { contentType }, + }); + + return jsonResponse({ ok: true }); +} + +export async function handleMultipartStart(request, env, shareCode) { + const video = await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?") + .bind(shareCode) + .first(); + if (!video) return errorResponse("Video not found", 404); + + const key = `videos/${shareCode}.mp4`; + const multipartUpload = await env.VIDEOS_BUCKET.createMultipartUpload(key, { + httpMetadata: { contentType: "video/mp4" }, + }); + + return jsonResponse({ uploadId: multipartUpload.uploadId }); +} + +export function decodeUploadId(value) { + try { + const decoded = decodeURIComponent(value); + return decoded && decoded.length <= 2048 ? decoded : null; + } catch { + return null; + } +} + +export async function handleMultipartPart(request, env, shareCode, uploadId, partNumber) { + const video = await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?") + .bind(shareCode) + .first(); + if (!video) return errorResponse("Video not found", 404); + if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > 10000) { + return errorResponse("Invalid multipart part number"); + } + + const key = `videos/${shareCode}.mp4`; + const multipartUpload = env.VIDEOS_BUCKET.resumeMultipartUpload(key, uploadId); + + const part = await multipartUpload.uploadPart(partNumber, request.body); + + return jsonResponse({ partNumber: part.partNumber, etag: part.etag }); +} + +export async function handleMultipartAbort(request, env, shareCode, uploadId) { + const key = `videos/${shareCode}.mp4`; + try { + const multipartUpload = env.VIDEOS_BUCKET.resumeMultipartUpload(key, uploadId); + await multipartUpload.abort(); + } catch (_e) { + // Ignore errors — upload may already be completed or expired + } + return jsonResponse({ ok: true }); +} + +export async function handleMultipartComplete(request, env, shareCode, uploadId) { + const video = await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?") + .bind(shareCode) + .first(); + if (!video) return errorResponse("Video not found", 404); + + const key = `videos/${shareCode}.mp4`; + const multipartUpload = env.VIDEOS_BUCKET.resumeMultipartUpload(key, uploadId); + + const body = await request.json(); + const parts = body.parts; // [{ partNumber, etag }, ...] + if (!Array.isArray(parts) || parts.length === 0 || parts.length > 10000) { + return errorResponse("Invalid multipart parts"); + } + if ( + parts.some( + (part) => + !part || + !Number.isInteger(part.partNumber) || + part.partNumber < 1 || + part.partNumber > 10000 || + typeof part.etag !== "string" || + !part.etag, + ) + ) { + return errorResponse("Invalid multipart parts"); + } + + await multipartUpload.complete(parts); + + return jsonResponse({ ok: true }); +} + +export async function handleMetadata(request, env, shareCode) { + const video = await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?") + .bind(shareCode) + .first(); + if (!video) return errorResponse("Video not found", 404); + + const body = await request.json(); + const { segments, title, summary, chapters, isMeeting } = body; + + // Chunk inserts so a multi-hour transcript can't exceed D1 batch limits. + const BATCH_CHUNK = 100; + if (segments && segments.length > 0) { + const stmt = env.DB.prepare( + "INSERT INTO transcript_segments (video_id, start_time, end_time, text, speaker) VALUES (?, ?, ?, ?, ?)", + ); + const batch = segments.map((seg) => + stmt.bind(video.id, seg.startTime, seg.endTime, seg.text, seg.speaker || null), + ); + for (let i = 0; i < batch.length; i += BATCH_CHUNK) { + await env.DB.batch(batch.slice(i, i + BATCH_CHUNK)); + } + } + + if (chapters && chapters.length > 0) { + const stmt = env.DB.prepare( + "INSERT INTO chapters (video_id, timestamp, title) VALUES (?, ?, ?)", + ); + const batch = chapters.map((ch) => stmt.bind(video.id, ch.timestamp, ch.title)); + for (let i = 0; i < batch.length; i += BATCH_CHUNK) { + await env.DB.batch(batch.slice(i, i + BATCH_CHUNK)); + } + } + + // Update title, summary, is_meeting, and mark upload complete + const updateFields = ["upload_completed = 1"]; + const updateBinds = []; + if (title) { + updateFields.push("title = ?"); + updateBinds.push(title); + } + if (summary !== undefined) { + updateFields.push("summary = ?"); + updateBinds.push(summary || null); + } + if (isMeeting !== undefined) { + updateFields.push("is_meeting = ?"); + updateBinds.push(isMeeting ? 1 : 0); + } + updateBinds.push(shareCode); + await env.DB.prepare(`UPDATE videos SET ${updateFields.join(", ")} WHERE share_code = ?`) + .bind(...updateBinds) + .run(); + + return jsonResponse({ ok: true }); +} diff --git a/services/recordly-share/worker/src/video.js b/services/recordly-share/worker/src/video.js new file mode 100644 index 000000000..58a1a1111 --- /dev/null +++ b/services/recordly-share/worker/src/video.js @@ -0,0 +1,11 @@ +// Adapted from Voom (MIT), Copyright (c) 2026 Aritro Paul. +// See ../../LICENSE and ../../../../THIRD_PARTY_NOTICES.md for attribution. + +export const EXPIRY_DAYS = 30; +// Hosted accounts use the free policy until server-verified paid entitlements exist. +export const FREE_EXPIRY_DAYS = 14; + +export function finiteNonnegative(value) { + const number = Number(value); + return Number.isFinite(number) && number >= 0 ? number : 0; +} diff --git a/services/recordly-share/worker/test/api.test.js b/services/recordly-share/worker/test/api.test.js new file mode 100644 index 000000000..52a18339c --- /dev/null +++ b/services/recordly-share/worker/test/api.test.js @@ -0,0 +1,603 @@ +import { describe, it, expect, beforeAll, vi } from 'vitest'; +import { env, SELF } from 'cloudflare:test'; +import worker, { sha256Hex } from '../src/index.js'; + +const AUTH = { Authorization: 'Bearer test-secret' }; +const BASE = 'https://share.test'; + +async function createShare(extra = {}) { + const res = await SELF.fetch(`${BASE}/api/upload`, { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'Test Recording', duration: 12.5, width: 1920, height: 1080, fileSize: 1000, ...extra }), + }); + expect(res.status).toBe(200); + return res.json(); +} + +async function completeUpload(shareCode, bytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7])) { + const put = await SELF.fetch(`${BASE}/api/upload-data/${shareCode}`, { + method: 'PUT', + headers: { ...AUTH, 'Content-Type': 'video/mp4' }, + body: bytes, + }); + expect(put.status).toBe(200); + const meta = await SELF.fetch(`${BASE}/api/metadata/${shareCode}`, { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({ segments: [{ startTime: 0, endTime: 1, text: 'hello world' }] }), + }); + expect(meta.status).toBe(200); +} + +describe('auth', () => { + it('rejects /api/* without a token', async () => { + const res = await SELF.fetch(`${BASE}/api/health`); + expect(res.status).toBe(401); + }); + + it('rejects a wrong token', async () => { + const res = await SELF.fetch(`${BASE}/api/health`, { headers: { Authorization: 'Bearer wrong' } }); + expect(res.status).toBe(401); + }); + + it('rejects a Bearer header with no token', async () => { + const res = await SELF.fetch(`${BASE}/api/health`, { headers: { Authorization: 'Bearer' } }); + expect(res.status).toBe(401); + }); + + it('accepts the correct token', async () => { + const res = await SELF.fetch(`${BASE}/api/health`, { headers: AUTH }); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ ok: true, app: 'recordly' }); + }); +}); + +describe('comment accounts', () => { + it('allows comments without signing in and uses the supplied display name', async () => { + const { shareCode } = await createShare(); + await completeUpload(shareCode); + + const anonymous = await SELF.fetch(`${BASE}/s/${shareCode}/comment`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + timestamp: 2, + author_name: 'Link Reviewer', + text: 'No account required', + }), + }); + expect(anonymous.status).toBe(200); + + const comments = await SELF.fetch(`${BASE}/s/${shareCode}/comments`); + const data = await comments.json(); + expect(data.comments).toEqual(expect.arrayContaining([ + expect.objectContaining({ + author_name: 'Link Reviewer', + text: 'No account required', + }), + ])); + }); + + it('supports signing back in and invalidates a logged-out session', async () => { + const register = await SELF.fetch(`${BASE}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'CF-Connecting-IP': '203.0.113.80' }, + body: JSON.stringify({ + displayName: 'Signed-in Reviewer', + email: 'reviewer@example.com', + password: 'correct-horse-battery-staple', + }), + }); + expect(register.status).toBe(200); + + const login = await SELF.fetch(`${BASE}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'CF-Connecting-IP': '203.0.113.81' }, + body: JSON.stringify({ + email: 'reviewer@example.com', + password: 'correct-horse-battery-staple', + }), + }); + expect(login.status).toBe(200); + const cookie = login.headers.get('Set-Cookie').split(';')[0]; + + const logout = await SELF.fetch(`${BASE}/auth/logout`, { + method: 'POST', + headers: { Cookie: cookie }, + }); + expect(logout.status).toBe(200); + + const session = await SELF.fetch(`${BASE}/auth/session`, { headers: { Cookie: cookie } }); + expect(await session.json()).toEqual({ user: null }); + }); +}); + +describe('upload validation', () => { + it('requires a title', async () => { + const res = await SELF.fetch(`${BASE}/api/upload`, { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + expect(res.status).toBe(400); + }); + + it('rejects javascript: CTA URLs', async () => { + const res = await SELF.fetch(`${BASE}/api/upload`, { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'x', cta_url: 'javascript:alert(1)' }), + }); + expect(res.status).toBe(400); + }); + + it('accepts https CTA URLs and returns a well-formed share code', async () => { + const data = await createShare({ cta_url: 'https://example.com', cta_text: 'Visit' }); + expect(data.shareCode).toMatch(/^[0-9a-f]{64}$/); + expect(data.shareURL).toContain(`/s/${data.shareCode}`); + }); + + it('uploads and completes a video through the multipart API', async () => { + const { shareCode } = await createShare({ fileSize: 6 }); + const start = await SELF.fetch(`${BASE}/api/upload-multipart/${shareCode}`, { + method: 'POST', + headers: AUTH, + }); + expect(start.status).toBe(200); + const { uploadId } = await start.json(); + + const part = await SELF.fetch( + `${BASE}/api/upload-part/${shareCode}/${encodeURIComponent(uploadId)}/1`, + { + method: 'PUT', + headers: { ...AUTH, 'Content-Type': 'video/mp4' }, + body: new Uint8Array([10, 20, 30, 40, 50, 60]), + }, + ); + expect(part.status).toBe(200); + const uploadedPart = await part.json(); + + const complete = await SELF.fetch( + `${BASE}/api/upload-complete/${shareCode}/${encodeURIComponent(uploadId)}`, + { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({ parts: [uploadedPart] }), + }, + ); + expect(complete.status).toBe(200); + + const meta = await SELF.fetch(`${BASE}/api/metadata/${shareCode}`, { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + expect(meta.status).toBe(200); + + const video = await SELF.fetch(`${BASE}/v/${shareCode}`); + expect(video.status).toBe(200); + expect(Array.from(new Uint8Array(await video.arrayBuffer()))).toEqual([10, 20, 30, 40, 50, 60]); + }); +}); + +describe('share data + view counts', () => { + it('serves data for a completed upload and increments views', async () => { + const { shareCode } = await createShare(); + await completeUpload(shareCode); + + const res1 = await SELF.fetch(`${BASE}/s/${shareCode}/data`); + expect(res1.status).toBe(200); + const d1 = await res1.json(); + expect(d1.video.title).toBe('Test Recording'); + expect(d1.video.view_count).toBe(1); + expect(d1.segments).toHaveLength(1); + + const res2 = await SELF.fetch(`${BASE}/s/${shareCode}/data`); + const d2 = await res2.json(); + expect(d2.video.view_count).toBe(2); + }); + + it('404s for incomplete uploads', async () => { + const { shareCode } = await createShare(); + const res = await SELF.fetch(`${BASE}/s/${shareCode}/data`); + expect(res.status).toBe(404); + }); +}); + +describe('video streaming + ranges', () => { + let shareCode; + const bytes = new Uint8Array(100).map((_, i) => i); + + beforeAll(async () => { + ({ shareCode } = await createShare()); + await completeUpload(shareCode, bytes); + }); + + it('serves the full object without a Range header', async () => { + const res = await SELF.fetch(`${BASE}/v/${shareCode}`); + expect(res.status).toBe(200); + expect(res.headers.get('Content-Length')).toBe('100'); + expect(res.headers.get('Accept-Ranges')).toBe('bytes'); + }); + + it('serves a bounded range with the full size in Content-Range', async () => { + const res = await SELF.fetch(`${BASE}/v/${shareCode}`, { headers: { Range: 'bytes=10-19' } }); + expect(res.status).toBe(206); + expect(res.headers.get('Content-Range')).toBe('bytes 10-19/100'); + expect(res.headers.get('Content-Length')).toBe('10'); + const body = new Uint8Array(await res.arrayBuffer()); + expect(Array.from(body)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); + }); + + it('serves an open-ended range', async () => { + const res = await SELF.fetch(`${BASE}/v/${shareCode}`, { headers: { Range: 'bytes=90-' } }); + expect(res.status).toBe(206); + expect(res.headers.get('Content-Range')).toBe('bytes 90-99/100'); + expect(res.headers.get('Content-Length')).toBe('10'); + }); + + it('serves a suffix range (bytes=-N)', async () => { + const res = await SELF.fetch(`${BASE}/v/${shareCode}`, { headers: { Range: 'bytes=-5' } }); + expect(res.status).toBe(206); + expect(res.headers.get('Content-Range')).toBe('bytes 95-99/100'); + const body = new Uint8Array(await res.arrayBuffer()); + expect(Array.from(body)).toEqual([95, 96, 97, 98, 99]); + }); + + it('returns 416 for an unsatisfiable range', async () => { + const res = await SELF.fetch(`${BASE}/v/${shareCode}`, { headers: { Range: 'bytes=5000-' } }); + expect(res.status).toBe(416); + }); +}); + +describe('password protection', () => { + const password = 'hunter2'; + let clientHash; + + beforeAll(async () => { + clientHash = await sha256Hex(password); // what the desktop app sends at share time + }); + + async function protectedShare() { + const share = await createShare({ password_hash: clientHash }); + await completeUpload(share.shareCode); + return share.shareCode; + } + + it('gates /data behind the password', async () => { + const shareCode = await protectedShare(); + const res = await SELF.fetch(`${BASE}/s/${shareCode}/data`); + expect(res.status).toBe(401); + expect((await res.json()).password_protected).toBe(true); + }); + + it('gates reactions, comments, and the thumbnail behind the password', async () => { + const shareCode = await protectedShare(); + + expect((await SELF.fetch(`${BASE}/s/${shareCode}/reactions`)).status).toBe(401); + expect((await SELF.fetch(`${BASE}/s/${shareCode}/comments`)).status).toBe(401); + expect((await SELF.fetch(`${BASE}/thumb/${shareCode}`)).status).toBe(404); + + // …and unlocks them all with the auth cookie. + const good = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + expect(good.status).toBe(200); + const cookie = good.headers.get('Set-Cookie').split(';')[0]; + + expect((await SELF.fetch(`${BASE}/s/${shareCode}/reactions`, { headers: { Cookie: cookie } })).status).toBe(200); + expect((await SELF.fetch(`${BASE}/s/${shareCode}/comments`, { headers: { Cookie: cookie } })).status).toBe(200); + }); + + it('rejects password attempts on expired videos', async () => { + const shareCode = await protectedShare(); + await env.DB.prepare( + "UPDATE videos SET expires_at = datetime('now', '-1 day') WHERE share_code = ?" + ).bind(shareCode).run(); + + const res = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + expect(res.status).toBe(404); + }); + + it('rejects a wrong password, accepts the right one, sets an HttpOnly cookie', async () => { + const shareCode = await protectedShare(); + + const bad = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password: 'wrong' }), + }); + expect(bad.status).toBe(403); + + const good = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + expect(good.status).toBe(200); + const cookie = good.headers.get('Set-Cookie'); + expect(cookie).toContain(`voom_auth_${shareCode}=`); + expect(cookie).toContain('HttpOnly'); + expect(cookie).toContain('Secure'); + + const authed = await SELF.fetch(`${BASE}/s/${shareCode}/data`, { + headers: { Cookie: cookie.split(';')[0] }, + }); + expect(authed.status).toBe(200); + }); + + it('stores passwords salted — never the bare client hash', async () => { + const shareCode = await protectedShare(); + const row = await env.DB.prepare('SELECT password_hash, password_salt FROM videos WHERE share_code = ?') + .bind(shareCode).first(); + expect(row.password_salt).toMatch(/^[0-9a-f]{32}$/); + expect(row.password_hash).toMatch(/^pbkdf2-sha256:210000:[0-9a-f]{64}$/); + }); + + it('lazily upgrades legacy unsalted rows on successful verify', async () => { + const shareCode = await protectedShare(); + // Regress the row to the legacy (pre-salt) format. + await env.DB.prepare('UPDATE videos SET password_hash = ?, password_salt = NULL WHERE share_code = ?') + .bind(clientHash, shareCode).run(); + + const res = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + expect(res.status).toBe(200); + + const row = await env.DB.prepare('SELECT password_hash, password_salt FROM videos WHERE share_code = ?') + .bind(shareCode).first(); + expect(row.password_salt).toMatch(/^[0-9a-f]{32}$/); + expect(row.password_hash).toMatch(/^pbkdf2-sha256:210000:[0-9a-f]{64}$/); // re-stored salted + }); + + it('rate-limits brute-force attempts (429 after 10 failures)', async () => { + const shareCode = await protectedShare(); + for (let i = 0; i < 10; i++) { + const res = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'CF-Connecting-IP': '203.0.113.7' }, + body: JSON.stringify({ password: `wrong-${i}` }), + }); + expect(res.status).toBe(403); + } + const blocked = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'CF-Connecting-IP': '203.0.113.7' }, + body: JSON.stringify({ password }), + }); + expect(blocked.status).toBe(429); + }); + + it('hides title and thumbnail from the OG page and image', async () => { + const shareCode = await protectedShare(); + const og = await SELF.fetch(`${BASE}/s/${shareCode}`, { headers: { 'User-Agent': 'Twitterbot/1.0' } }); + expect(og.status).toBe(200); + const html = await og.text(); + expect(html).toContain('Protected video'); + expect(html).not.toContain('Test Recording'); + + const img = await SELF.fetch(`${BASE}/og/${shareCode}`); + expect(img.status).toBe(200); + expect(img.headers.get('Content-Type')).toContain('svg'); + const svg = await img.text(); + expect(svg).toContain('Protected video'); + expect(svg).not.toContain('Test Recording'); + }); +}); + +describe('OG page (public video)', () => { + it('escapes the title in meta tags', async () => { + const { shareCode } = await createShare({ title: '' }); + await completeUpload(shareCode); + const res = await SELF.fetch(`${BASE}/s/${shareCode}`, { headers: { 'User-Agent': 'Slackbot 1.0' } }); + const html = await res.text(); + expect(html).not.toContain(''; + const { shareCode } = await createShare({ width: payload, height: payload, duration: 'invalid', fileSize: -1 }); + const row = await env.DB.prepare('SELECT width, height, duration, file_size FROM videos WHERE share_code = ?').bind(shareCode).first(); + expect(row).toMatchObject({ width: 0, height: 0, duration: 0, file_size: 0 }); + await completeUpload(shareCode); + await env.DB.prepare('UPDATE videos SET width = ?, height = ? WHERE share_code = ?').bind(payload, payload, shareCode).run(); + const response = await SELF.fetch(`${BASE}/s/${shareCode}`, { headers: { 'User-Agent': 'Twitterbot/1.0' } }); + expect(await response.text()).not.toContain(''); + }); +}); + + +it('normalizes invalid comment pagination and clamps zero limits', async () => { + const { shareCode } = await createShare(); + await completeUpload(shareCode); + for (const query of ['page=invalid&limit=invalid', 'page=-5&limit=0', 'page=1&limit=-1']) { + const response = await SELF.fetch(`${BASE}/s/${shareCode}/comments?${query}`); + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.comments).toEqual([]); + } +}); + +it('fails password verification gracefully when its signing secret is absent', async () => { + const { shareCode } = await createShare({ password_hash: await sha256Hex('secret') }); + await completeUpload(shareCode); + const response = await worker.fetch(new Request(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: 'secret' }), + }), { ...env, API_SECRET: '' }, {}); + expect(response.status).toBe(503); + expect(await response.json()).toMatchObject({ error: 'Password protection is not configured' }); +}); + +it('returns stable comment IDs and orders equal timestamps by ID', async () => { + const { shareCode } = await createShare(); + await completeUpload(shareCode); + const ids = []; + for (const text of ['first', 'second']) { + const response = await SELF.fetch(`${BASE}/s/${shareCode}/comment`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ timestamp: 1, author_name: 'Viewer', text }), + }); + expect(response.status).toBe(200); + ids.push((await response.json()).id); + } + const response = await SELF.fetch(`${BASE}/s/${shareCode}/comments`); + expect((await response.json()).comments.map((comment) => comment.id)).toEqual(ids); + expect(ids[1]).toBeGreaterThan(ids[0]); +}); + +it('rejects creating protected shares without a signing secret', async () => { + const lookup = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({ id: 'owner' }))); + try { + const response = await worker.fetch(new Request(`${BASE}/api/upload`, { + method: 'POST', headers: { Authorization: 'Bearer owner-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'Protected', password_hash: 'hash' }), + }), { ...env, API_SECRET: '', ALLOW_API_SECRET_UPLOADS: 'false', SUPABASE_URL: 'https://auth.example.test', SUPABASE_PUBLISHABLE_KEY: 'key', OWNER_USER_ID: 'owner' }, {}); + expect(response.status).toBe(503); + await response.arrayBuffer(); + } finally { lookup.mockRestore(); } +}); + +it('rejects dashboard cookies without configured secrets instead of crashing', async () => { + const config = { ...env, API_SECRET: undefined, DASHBOARD_PASSWORD: undefined }; + const response = await worker.fetch(new Request(`${BASE}/api/videos`, { + headers: { Cookie: 'voom_session=untrusted' }, + }), config, {}); + expect(response.status).toBe(401); + await response.arrayBuffer(); +}); +it('clamps a bounded video range to the actual object size', async () => { + const { shareCode } = await createShare(); + await completeUpload(shareCode); + const response = await SELF.fetch(`${BASE}/v/${shareCode}`, { headers: { Range: 'bytes=6-200' } }); + expect(response.status).toBe(206); + expect(response.headers.get('Content-Range')).toBe('bytes 6-7/8'); + expect(response.headers.get('Content-Length')).toBe('2'); + expect(Array.from(new Uint8Array(await response.arrayBuffer()))).toEqual([6, 7]); +}); + +it('counts successful registrations without allowing login to reset their quota', async () => { + const headers = { 'Content-Type': 'application/json', 'CF-Connecting-IP': '192.0.2.201' }; + for (let i = 0; i < 10; i++) { + const response = await SELF.fetch(`${BASE}/auth/register`, { + method: 'POST', headers, + body: JSON.stringify({ email: `quota-${i}@example.com`, displayName: 'Quota Test', password: 'test-password' }), + }); + expect(response.status).toBe(200); + } + const login = await SELF.fetch(`${BASE}/auth/login`, { + method: 'POST', headers, + body: JSON.stringify({ email: 'quota-0@example.com', password: 'test-password' }), + }); + expect(login.status).toBe(200); + const blocked = await SELF.fetch(`${BASE}/auth/register`, { + method: 'POST', headers, + body: JSON.stringify({ email: 'quota-extra@example.com', displayName: 'Quota Test', password: 'test-password' }), + }); + expect(blocked.status).toBe(429); +}); + +it('upgrades salted legacy recording passwords only after correct verification', async () => { + const password = 'legacy-recording-password'; + const { shareCode } = await createShare(); + await completeUpload(shareCode); + const salt = 'legacy-salt'; + const hash = await sha256Hex(salt + await sha256Hex(password)); + await env.DB.prepare('UPDATE videos SET password_hash = ?, password_salt = ? WHERE share_code = ?').bind(hash, salt, shareCode).run(); + const verify = (value) => SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: value }), + }); + expect((await verify('wrong')).status).toBe(403); + expect((await env.DB.prepare('SELECT password_hash FROM videos WHERE share_code = ?').bind(shareCode).first()).password_hash).toBe(hash); + expect((await verify(password)).status).toBe(200); + const row = await env.DB.prepare('SELECT password_hash FROM videos WHERE share_code = ?').bind(shareCode).first(); + expect(row.password_hash).toMatch(/^pbkdf2-sha256:210000:[0-9a-f]{64}$/); + expect((await verify(password)).status).toBe(200); +}); diff --git a/services/recordly-share/worker/test/helpers.test.js b/services/recordly-share/worker/test/helpers.test.js new file mode 100644 index 000000000..42cec6004 --- /dev/null +++ b/services/recordly-share/worker/test/helpers.test.js @@ -0,0 +1,96 @@ +import { describe, it, expect } from 'vitest'; +import { + generateShareCode, + escapeHTML, + parseCookies, + timingSafeEqual, + sha256Hex, + generateSalt, + formatVTTTime, +} from '../src/index.js'; + +const SHARE_CODE_CHARS = '0123456789abcdef'; +const SHARE_CODE_LENGTH = 64; + +describe('generateShareCode', () => { + it('produces codes of the documented length and charset', () => { + for (let i = 0; i < 50; i++) { + const code = generateShareCode(); + expect(code).toHaveLength(SHARE_CODE_LENGTH); + expect(code).toMatch(/^[a-z0-9]+$/); // must satisfy every route regex + for (const ch of code) expect(SHARE_CODE_CHARS).toContain(ch); + } + }); + + it('generates distinct random codes', () => { + expect(new Set(Array.from({ length: 100 }, generateShareCode)).size).toBe(100); + }); +}); + +describe('escapeHTML', () => { + it('escapes the four double-quote-context metacharacters', () => { + expect(escapeHTML(`&`)).toBe( + '<img src=x onerror="alert(1)">&' + ); + }); + + it('leaves plain text untouched', () => { + expect(escapeHTML('My recording 2024')).toBe('My recording 2024'); + }); + + // Documented limitation: single quotes pass through, so escaped values are + // only safe in double-quoted attributes and element content. + it("does not escape single quotes — keep attributes double-quoted", () => { + expect(escapeHTML("it's")).toBe("it's"); + }); +}); + +describe('parseCookies', () => { + it('parses multiple cookies and preserves = inside values', () => { + const cookies = parseCookies('a=1; voom_auth_abc=dGVzdA==; b=x=y'); + expect(cookies.a).toBe('1'); + expect(cookies.voom_auth_abc).toBe('dGVzdA=='); + expect(cookies.b).toBe('x=y'); + }); + + it('returns empty object for empty string', () => { + expect(parseCookies('')).toEqual({}); + }); +}); + +describe('timingSafeEqual', () => { + it('matches equal strings', () => { + expect(timingSafeEqual('secret-token', 'secret-token')).toBe(true); + }); + it('rejects unequal strings of same and different length', () => { + expect(timingSafeEqual('secret-token', 'secret-tokeX')).toBe(false); + expect(timingSafeEqual('secret', 'secret-token')).toBe(false); + expect(timingSafeEqual('', 'x')).toBe(false); + }); + it('rejects non-strings', () => { + expect(timingSafeEqual(undefined, 'x')).toBe(false); + expect(timingSafeEqual('x', null)).toBe(false); + }); +}); + +describe('sha256Hex / generateSalt', () => { + it('produces the known SHA-256 of "hunter2"', async () => { + expect(await sha256Hex('hunter2')).toBe( + 'f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7' + ); + }); + it('salt is 32 hex chars and unique', () => { + const a = generateSalt(); + const b = generateSalt(); + expect(a).toMatch(/^[0-9a-f]{32}$/); + expect(a).not.toBe(b); + }); +}); + +describe('formatVTTTime', () => { + it('formats with millisecond precision', () => { + expect(formatVTTTime(0)).toBe('00:00:00.000'); + expect(formatVTTTime(61.5)).toBe('00:01:01.500'); + expect(formatVTTTime(3661.25)).toBe('01:01:01.250'); + }); +}); diff --git a/services/recordly-share/worker/test/hosted.test.js b/services/recordly-share/worker/test/hosted.test.js new file mode 100644 index 000000000..f3cf17625 --- /dev/null +++ b/services/recordly-share/worker/test/hosted.test.js @@ -0,0 +1,479 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { env } from "cloudflare:test"; +import worker from "../src/index.js"; +import { ensureSchema } from "../src/schema.js"; + +const config = { + ...env, + HOSTED_MODE: "true", + REQUEST_RATE_LIMIT: { limit: async () => ({ success: true }) }, + ACCOUNT_RATE_LIMIT: { limit: async () => ({ success: true }) }, + SUPABASE_URL: "https://auth.example.test", + SUPABASE_PUBLISHABLE_KEY: "public-key", +}; +let owner; +beforeEach(async () => { + await ensureSchema(env); + owner = crypto.randomUUID(); + vi.spyOn(globalThis, "fetch").mockImplementation(async (_url, options) => { + const id = options.headers.Authorization.slice(7); + return Response.json({ + id, + email: `${id}@example.test`, + email_confirmed_at: "2026-09-23T00:00:00Z", + is_anonymous: false, + }); + }); +}); +afterEach(() => vi.restoreAllMocks()); +const call = (path, method = "GET", body, account = owner, headers = {}) => + worker.fetch( + new Request(`https://share.test${path}`, { + method, + headers: { + Authorization: `Bearer ${account}`, + "Content-Type": "application/json", + ...headers, + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }), + config, + ); +async function create(account = owner) { + const response = await call( + "/api/upload", + "POST", + { title: "Owned recording", fileSize: 8, owner_id: "spoofed" }, + account, + ); + expect(response.status).toBe(200); + return (await response.json()).shareCode; +} + +it("enforces fourteen-day free expiry and rejects renewal or client-claimed paid plans", async () => { + const started = Date.now(); + const response = await call("/api/upload", "POST", { + title: "Free expiry", + fileSize: 8, + plan: "paid", + expiresAt: "2099-01-01T00:00:00Z", + }); + expect(response.status).toBe(200); + const ticket = await response.json(); + const expiry = Date.parse(ticket.expiresAt); + const fortnight = 14 * 86400000; + expect(expiry).toBeGreaterThanOrEqual(started + fortnight); + expect(expiry).toBeLessThanOrEqual(Date.now() + fortnight); + expect((await call(`/api/renew/${ticket.shareCode}`, "POST", {})).status).toBe(403); + const row = await env.DB.prepare("SELECT expires_at FROM videos WHERE share_code = ?") + .bind(ticket.shareCode) + .first(); + expect(row.expires_at).toBe(ticket.expiresAt); + + await put(ticket.shareCode, new Uint8Array(8)); + expect((await call(`/api/metadata/${ticket.shareCode}`, "POST", {})).status).toBe(200); + expect((await call(`/v/${ticket.shareCode}`)).status).toBe(200); + await env.DB.prepare( + "UPDATE videos SET expires_at = datetime('now', '-1 second') WHERE share_code = ?", + ) + .bind(ticket.shareCode) + .run(); + expect((await call(`/v/${ticket.shareCode}`)).status).toBe(404); + expect((await call(`/s/${ticket.shareCode}/data`)).status).toBe(404); + expect((await call(`/api/renew/${ticket.shareCode}`, "POST", {})).status).toBe(403); +}); + +it("requires a verified non-anonymous identity and never accepts a shared secret or cookie", async () => { + for (const user of [ + { id: owner }, + { id: owner, email: "a@example.test", email_confirmed_at: "now", is_anonymous: true }, + ]) { + fetch.mockResolvedValueOnce(Response.json(user)); + expect((await call("/api/health")).status).toBe(401); + } + fetch.mockResolvedValueOnce(new Response("", { status: 401 })); + expect((await call("/api/health", "GET", undefined, "test-secret")).status).toBe(401); + expect( + ( + await worker.fetch( + new Request("https://share.test/api/videos", { + headers: { Cookie: "voom_session=anything" }, + }), + config, + ) + ).status, + ).toBe(401); +}); + +it("isolates every recording mutation before its handler can touch storage", async () => { + const code = await create(); + const other = crypto.randomUUID(); + for (const [method, path] of [ + ["PUT", `upload-data/${code}`], + ["PUT", `upload-thumbnail/${code}`], + ["POST", `upload-multipart/${code}`], + ["PUT", `upload-part/${code}/fake/1`], + ["POST", `upload-complete/${code}/fake`], + ["POST", `upload-abort/${code}/fake`], + ["POST", `metadata/${code}`], + ["POST", `renew/${code}`], + ["DELETE", `delete/${code}`], + ]) + expect((await call(`/api/${path}`, method, {}, other)).status).toBe(404); + const row = await env.DB.prepare("SELECT owner_id FROM videos WHERE share_code = ?") + .bind(code) + .first(); + expect(row.owner_id).toBe(owner); +}); + +it("scopes list and view queries and hides legacy unowned rows", async () => { + const mine = await create(); + const theirs = await create(crypto.randomUUID()); + const legacy = await create(); + await env.DB.prepare("UPDATE videos SET upload_completed = 1 WHERE share_code IN (?, ?, ?)") + .bind(mine, theirs, legacy) + .run(); + await env.DB.prepare("UPDATE videos SET owner_id = NULL WHERE share_code = ?") + .bind(legacy) + .run(); + const list = await (await call("/api/videos")).json(); + expect(list.videos.map((v) => v.share_code)).toEqual([mine]); + const views = await ( + await call("/api/check-views", "POST", { shareCodes: [mine, theirs, legacy] }) + ).json(); + expect(Object.keys(views.views)).toEqual([mine]); + expect((await call(`/api/delete/${legacy}`, "DELETE")).status).toBe(404); +}); + +it("allows exactly five concurrent reservations and releases a slot after owner deletion", async () => { + const responses = await Promise.all( + Array.from({ length: 9 }, () => + call("/api/upload", "POST", { title: "Concurrent", fileSize: 8 }), + ), + ); + expect(responses.filter((r) => r.status === 200)).toHaveLength(5); + expect(responses.filter((r) => r.status === 409)).toHaveLength(4); + const row = await env.DB.prepare("SELECT share_code FROM videos WHERE owner_id = ? LIMIT 1") + .bind(owner) + .first(); + await env.DB.prepare( + "UPDATE videos SET expires_at = datetime('now', '-1 day') WHERE owner_id = ?", + ) + .bind(owner) + .run(); + expect( + ( + await call("/api/upload", "POST", { + title: "Expired still occupies storage", + fileSize: 8, + }) + ).status, + ).toBe(409); + expect((await call(`/api/delete/${row.share_code}`, "DELETE")).status).toBe(200); + await create(); +}); + +it("disables the self-host shared-password dashboard and comment-account routes", async () => { + expect((await call("/library")).status).toBe(404); + expect((await call("/library/login", "POST", {})).status).toBe(404); + expect((await call("/auth/register", "POST", {})).status).toBe(404); +}); + +const put = (code, bytes, extra = {}, path = "upload-data") => + worker.fetch( + new Request(`https://share.test/api/${path}/${code}`, { + method: "PUT", + headers: { + Authorization: `Bearer ${owner}`, + "Content-Type": "video/mp4", + "Content-Length": String(bytes.length), + ...extra, + }, + body: bytes, + }), + config, + ); + +it("rejects invalid reservations and refuses publication without matching bytes", async () => { + for (const size of [0, -1, 0.5, 1_000_000_001, "8", null]) { + expect( + (await call("/api/upload", "POST", { title: "Invalid", fileSize: size })).status, + ).toBe(413); + } + const code = await create(); + expect((await call(`/api/metadata/${code}`, "POST", {})).status).toBe(409); + expect((await put(code, new Uint8Array(9))).status).toBe(400); + expect(await env.VIDEOS_BUCKET.head(`videos/${code}.mp4`)).toBeNull(); +}); + +it("validates actual streamed bytes even when Content-Length lies", async () => { + for (const size of [7, 9]) { + const code = await create(); + expect((await put(code, new Uint8Array(size), { "Content-Length": "8" })).status).toBe(400); + expect(await env.VIDEOS_BUCKET.head(`videos/${code}.mp4`)).toBeNull(); + } +}); + +it("uploads, publishes, retries metadata safely, serves playback, and deletes", async () => { + const code = await create(); + expect((await put(code, new Uint8Array(8))).status).toBe(200); + const metadata = { title: "Ready", segments: [{ startTime: 0, endTime: 1, text: "Hello" }] }; + expect((await call(`/api/metadata/${code}`, "POST", metadata)).status).toBe(200); + expect((await call(`/api/metadata/${code}`, "POST", metadata)).status).toBe(200); + const segments = await env.DB.prepare( + "SELECT COUNT(*) AS total FROM transcript_segments WHERE video_id = (SELECT id FROM videos WHERE share_code = ?)", + ) + .bind(code) + .first(); + expect(segments.total).toBe(1); + expect((await put(code, new Uint8Array(8))).status).toBe(409); + expect((await worker.fetch(new Request(`https://share.test/v/${code}`), config)).status).toBe( + 200, + ); + expect((await call(`/api/delete/${code}`, "DELETE")).status).toBe(200); + expect(await env.VIDEOS_BUCKET.head(`videos/${code}.mp4`)).toBeNull(); +}); + +it("reuses one multipart session, enforces part sizes, and completes exactly once", async () => { + const code = await create(); + const start = await (await call(`/api/upload-multipart/${code}`, "POST")).json(); + const retry = await (await call(`/api/upload-multipart/${code}`, "POST")).json(); + expect(retry.uploadId).toBe(start.uploadId); + const id = encodeURIComponent(start.uploadId); + expect((await call(`/api/upload-abort/${code}/wrong`, "POST")).status).toBe(404); + const part = await worker.fetch( + new Request(`https://share.test/api/upload-part/${code}/${id}/1`, { + method: "PUT", + headers: { Authorization: `Bearer ${owner}`, "Content-Length": "8" }, + body: new Uint8Array(8), + }), + config, + ); + expect(part.status).toBe(200); + const manifest = { parts: [await part.json()] }; + expect((await call(`/api/upload-complete/${code}/${id}`, "POST", { parts: [] })).status).toBe( + 400, + ); + expect((await call(`/api/upload-complete/${code}/${id}`, "POST", manifest)).status).toBe(200); + expect((await call(`/api/upload-complete/${code}/${id}`, "POST", manifest)).status).toBe(200); + expect((await call(`/api/metadata/${code}`, "POST", {})).status).toBe(200); + expect((await call(`/api/delete/${code}`, "DELETE")).status).toBe(200); +}); + +it("aborts abandoned multipart uploads and frees only stale slots", async () => { + const { cleanupExpired } = await import("../src/library.js"); + const stale = await create(); + const active = await create(); + await call(`/api/upload-multipart/${stale}`, "POST"); + await env.DB.prepare( + "UPDATE videos SET created_at = datetime('now', '-2 days') WHERE share_code = ?", + ) + .bind(stale) + .run(); + await cleanupExpired(config); + expect( + await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?").bind(stale).first(), + ).toBeNull(); + expect( + await env.DB.prepare("SELECT upload_id FROM recording_uploads WHERE share_code = ?") + .bind(stale) + .first(), + ).toBeNull(); + expect( + await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?").bind(active).first(), + ).not.toBeNull(); +}); + +it("bounds control payloads and thumbnail bytes", async () => { + expect( + (await call("/api/upload", "POST", { title: "x".repeat(17000), fileSize: 8 })).status, + ).toBe(413); + const code = await create(); + expect( + ( + await put( + code, + new Uint8Array(1), + { "Content-Type": "image/jpeg", "Content-Length": String(3 * 1024 * 1024) }, + "upload-thumbnail", + ) + ).status, + ).toBe(413); +}); + +it("limits staging to its explicitly allowed account", async () => { + const restricted = { ...config, STAGING_ALLOWED_USER_ID: owner }; + expect( + ( + await worker.fetch( + new Request("https://share.test/api/health", { + headers: { Authorization: `Bearer ${owner}` }, + }), + restricted, + ) + ).status, + ).toBe(200); + expect( + ( + await worker.fetch( + new Request("https://share.test/api/health", { + headers: { Authorization: "Bearer different-user" }, + }), + restricted, + ) + ).status, + ).toBe(401); +}); + +it("leaves quota occupied when R2 cleanup fails, then succeeds on retry", async () => { + const code = await create(); + await call(`/api/upload-multipart/${code}`, "POST"); + const failing = { + ...config, + VIDEOS_BUCKET: { + resumeMultipartUpload: () => ({ + abort: async () => { + throw new Error("Storage unavailable"); + }, + }), + }, + }; + expect( + ( + await worker.fetch( + new Request(`https://share.test/api/delete/${code}`, { + method: "DELETE", + headers: { Authorization: `Bearer ${owner}` }, + }), + failing, + ) + ).status, + ).toBe(500); + expect( + await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?").bind(code).first(), + ).not.toBeNull(); + expect((await call(`/api/delete/${code}`, "DELETE")).status).toBe(200); +}); + +it("blocks competing mutations and preserves another account’s locks", async () => { + const code = await create(); + await env.DB.prepare("INSERT INTO recording_locks (share_code, token) VALUES (?, ?)") + .bind(code, "held-by-upload") + .run(); + expect((await call(`/api/delete/${code}`, "DELETE")).status).toBe(409); + expect( + (await call(`/api/delete/${code}`, "DELETE", undefined, crypto.randomUUID())).status, + ).toBe(404); + expect( + ( + await env.DB.prepare("SELECT token FROM recording_locks WHERE share_code = ?") + .bind(code) + .first() + ).token, + ).toBe("held-by-upload"); +}); + +it("fails closed without rate limits and throttles before expensive auth/storage work", async () => { + const request = () => + new Request("https://share.test/api/health", { + headers: { Authorization: `Bearer ${owner}` }, + }); + expect( + (await worker.fetch(request(), { ...config, REQUEST_RATE_LIMIT: undefined })).status, + ).toBe(503); + const denied = { limit: async () => ({ success: false }) }; + fetch.mockClear(); + expect((await worker.fetch(request(), { ...config, REQUEST_RATE_LIMIT: denied })).status).toBe( + 429, + ); + expect(fetch).not.toHaveBeenCalled(); + expect((await worker.fetch(request(), { ...config, ACCOUNT_RATE_LIMIT: denied })).status).toBe( + 429, + ); +}); + +it("accepts the full 1 GB allowance while rejecting the next byte", async () => { + const accepted = await call("/api/upload", "POST", { + title: "Full allowance", + fileSize: 1_000_000_000, + }); + expect(accepted.status).toBe(200); + const rejected = await call("/api/upload", "POST", { + title: "Over allowance", + fileSize: 1_000_000_001, + }); + expect(rejected.status).toBe(413); +}); + +it("scheduled cleanup removes expired completed objects and preserves active recordings", async () => { + const expired = await create(); + const active = await create(); + for (const code of [expired, active]) { + await put(code, new Uint8Array(8)); + await call(`/api/metadata/${code}`, "POST", {}); + } + await env.DB.prepare( + "UPDATE videos SET expires_at = datetime('now', '-1 second') WHERE share_code = ?", + ) + .bind(expired) + .run(); + await worker.scheduled({}, config); + expect(await env.VIDEOS_BUCKET.head(`videos/${expired}.mp4`)).toBeNull(); + expect( + await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?").bind(expired).first(), + ).toBeNull(); + expect(await env.VIDEOS_BUCKET.head(`videos/${active}.mp4`)).not.toBeNull(); + expect( + await env.DB.prepare("SELECT id FROM videos WHERE share_code = ?").bind(active).first(), + ).not.toBeNull(); +}); + +it("accepts the owner poster after publication without allowing media replacement", async () => { + const code = await create(); + expect((await put(code, new Uint8Array(8))).status).toBe(200); + expect((await call(`/api/metadata/${code}`, "POST", {})).status).toBe(200); + const thumbnail = new Uint8Array([0xff, 0xd8, 0xff, 0xd9]); + expect( + (await put(code, thumbnail, { "Content-Type": "image/jpeg" }, "upload-thumbnail")).status, + ).toBe(200); + const poster = await env.VIDEOS_BUCKET.get(`thumbnails/${code}.jpg`); + expect(new Uint8Array(await poster.arrayBuffer())).toEqual(thumbnail); + expect((await put(code, new Uint8Array(8))).status).toBe(409); + expect( + (await call(`/api/upload-thumbnail/${code}`, "PUT", {}, crypto.randomUUID())).status, + ).toBe(404); +}); + +it("clears a completed size-mismatch session so a fresh multipart upload can start", async () => { + const code = await create(); + const start = await (await call(`/api/upload-multipart/${code}`, "POST")).json(); + const brokenStorage = { + ...config, + VIDEOS_BUCKET: { + head: (...args) => env.VIDEOS_BUCKET.head(...args), + delete: (...args) => env.VIDEOS_BUCKET.delete(...args), + resumeMultipartUpload: () => ({ complete: async () => ({ size: 7 }) }), + }, + }; + const response = await worker.fetch( + new Request( + `https://share.test/api/upload-complete/${code}/${encodeURIComponent(start.uploadId)}`, + { + method: "POST", + headers: { Authorization: `Bearer ${owner}`, "Content-Type": "application/json" }, + body: JSON.stringify({ parts: [{ partNumber: 1, etag: "fixture" }] }), + }, + ), + brokenStorage, + ); + expect(response.status).toBe(400); + expect( + await env.DB.prepare("SELECT upload_id FROM recording_uploads WHERE share_code = ?") + .bind(code) + .first(), + ).toBeNull(); + const retry = await (await call(`/api/upload-multipart/${code}`, "POST")).json(); + expect(retry.uploadId).not.toBe(start.uploadId); + // The fake storage above cannot actually complete the original R2 session. + await env.VIDEOS_BUCKET.resumeMultipartUpload(`videos/${code}.mp4`, start.uploadId).abort(); +}); diff --git a/services/recordly-share/worker/test/library.test.js b/services/recordly-share/worker/test/library.test.js new file mode 100644 index 000000000..dbe345089 --- /dev/null +++ b/services/recordly-share/worker/test/library.test.js @@ -0,0 +1,165 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { env, SELF } from 'cloudflare:test'; +import { expectedSessionToken } from '../src/index.js'; + +const AUTH = { Authorization: 'Bearer test-secret' }; +const BASE = 'https://share.test'; + +async function createAndCompleteShare(extra = {}) { + const res = await SELF.fetch(`${BASE}/api/upload`, { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'Library Test', duration: 30, width: 1920, height: 1080, fileSize: 5000, ...extra }), + }); + expect(res.status).toBe(200); + const { shareCode } = await res.json(); + + await SELF.fetch(`${BASE}/api/upload-data/${shareCode}`, { + method: 'PUT', + headers: { ...AUTH, 'Content-Type': 'video/mp4' }, + body: new Uint8Array([0, 1, 2, 3, 4]), + }); + await SELF.fetch(`${BASE}/api/metadata/${shareCode}`, { + method: 'POST', + headers: { ...AUTH, 'Content-Type': 'application/json' }, + body: JSON.stringify({ segments: [] }), + }); + return shareCode; +} + +describe('GET /api/videos', () => { + it('rejects with no auth -> 401', async () => { + const res = await SELF.fetch(`${BASE}/api/videos`); + expect(res.status).toBe(401); + }); + + it('returns 200 and a videos array with valid bearer token', async () => { + const res = await SELF.fetch(`${BASE}/api/videos`, { headers: AUTH }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(Array.isArray(data.videos)).toBe(true); + }); + + it('returns completed videos newest-first and omits sensitive fields', async () => { + const code1 = await createAndCompleteShare({ title: 'Older Video' }); + const code2 = await createAndCompleteShare({ title: 'Newer Video' }); + + // Force distinct timestamps so ordering is deterministic in the test env + // (SQLite datetime('now') has second precision; both rows may land the same second) + await env.DB.prepare("UPDATE videos SET created_at = datetime('now', '-10 seconds') WHERE share_code = ?") + .bind(code1).run(); + + const res = await SELF.fetch(`${BASE}/api/videos`, { headers: AUTH }); + expect(res.status).toBe(200); + const { videos } = await res.json(); + + // Both videos should appear + const codes = videos.map(v => v.share_code); + expect(codes).toContain(code1); + expect(codes).toContain(code2); + + // Newer should be first + const idx1 = codes.indexOf(code1); + const idx2 = codes.indexOf(code2); + expect(idx2).toBeLessThan(idx1); + + // Sensitive fields must not be present on any row + for (const v of videos) { + expect(v).not.toHaveProperty('password_hash'); + expect(v).not.toHaveProperty('password_salt'); + expect(v).not.toHaveProperty('id'); + } + + // Required fields present + const video = videos.find(v => v.share_code === code2); + expect(video).toBeDefined(); + expect(typeof video.title).toBe('string'); + expect(typeof video.duration).toBe('number'); + expect(typeof video.view_count).toBe('number'); + expect(typeof video.is_protected).toBe('number'); + }); + + it('marks password-protected videos with is_protected=1', async () => { + const { sha256Hex } = await import('../src/index.js'); + const clientHash = await sha256Hex('secret123'); + const code = await createAndCompleteShare({ password_hash: clientHash }); + + const res = await SELF.fetch(`${BASE}/api/videos`, { headers: AUTH }); + const { videos } = await res.json(); + const video = videos.find(v => v.share_code === code); + expect(video).toBeDefined(); + expect(video.is_protected).toBe(1); + expect(video).not.toHaveProperty('password_hash'); + }); +}); + +describe('Dashboard login/logout', () => { + it('POST /library/login with wrong password -> 302 to /library/login?error=1 (no session cookie)', async () => { + const form = new FormData(); + form.set('password', 'wrong-password'); + const res = await SELF.fetch(`${BASE}/library/login`, { + method: 'POST', + body: form, + redirect: 'manual', + }); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('/library/login?error=1'); + const setCookie = res.headers.get('Set-Cookie') || ''; + expect(setCookie).not.toContain('voom_session='); + }); + + it('POST /library/login with correct password (API_SECRET) -> 302 to /library with HttpOnly session cookie', async () => { + const form = new FormData(); + form.set('password', 'test-secret'); // matches API_SECRET in test env + const res = await SELF.fetch(`${BASE}/library/login`, { + method: 'POST', + body: form, + redirect: 'manual', + }); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('/library'); + const setCookie = res.headers.get('Set-Cookie') || ''; + expect(setCookie).toContain('voom_session='); + expect(setCookie).toContain('HttpOnly'); + }); + + it('GET /api/videos with session cookie (no bearer) -> 200', async () => { + // Get a valid session token + const token = await expectedSessionToken(env); + const res = await SELF.fetch(`${BASE}/api/videos`, { + headers: { Cookie: `voom_session=${token}` }, + }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(Array.isArray(data.videos)).toBe(true); + }); +}); + +describe('GET /library (dashboard gate)', () => { + it('redirects to /library/login with no cookie', async () => { + const res = await SELF.fetch(`${BASE}/library`, { redirect: 'manual' }); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('/library/login'); + }); + + it('serves the dashboard page with a valid session cookie', async () => { + const token = await expectedSessionToken(env); + // ASSETS serve library.html for /library — this exercises the auth gate passing + const res = await SELF.fetch(`${BASE}/library`, { + headers: { Cookie: `voom_session=${token}` }, + redirect: 'manual', + }); + // In the test environment ASSETS returns the static HTML; status 200 + expect([200, 304]).toContain(res.status); + }); +}); + +it('rejects expired and tampered dashboard sessions', async () => { + const expired = await expectedSessionToken(env, Math.floor(Date.now() / 1000) - 1); + const valid = await expectedSessionToken(env); + const tampered = `${Number(valid.split('.')[0]) + 604800}.${valid.split('.')[1]}`; + for (const token of [expired, tampered, valid.split('.')[1]]) { + const res = await SELF.fetch(`${BASE}/api/videos`, { headers: { Cookie: `voom_session=${token}` } }); + expect(res.status).toBe(401); + } +}); diff --git a/services/recordly-share/worker/test/migration.test.js b/services/recordly-share/worker/test/migration.test.js new file mode 100644 index 000000000..875a6fe39 --- /dev/null +++ b/services/recordly-share/worker/test/migration.test.js @@ -0,0 +1,186 @@ +// Regression tests for ensureSchema against databases that predate schema +// versioning. The v4.1.0 bug: a production DB with tables but no _meta row +// was treated as "fresh", so CREATE TABLE IF NOT EXISTS no-opped, the v2 +// ALTER never ran, and every INSERT touching password_salt threw a 500. +// These tests rebuild the database into each historical shape and verify +// uploads work afterward. +import { describe, it, expect, beforeEach, afterAll } from "vitest"; +import { env, SELF } from "cloudflare:test"; +import { __resetSchemaCacheForTests } from "../src/index.js"; + +const AUTH = { Authorization: "Bearer test-secret" }; +const BASE = "https://share.test"; + +// The videos table exactly as production had it before migration 0006 +// (base schema + migrations 0002–0005): no password_salt column. +const V1_STATEMENTS = [ + `CREATE TABLE videos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + share_code TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + duration REAL NOT NULL DEFAULT 0, + width INTEGER NOT NULL DEFAULT 0, + height INTEGER NOT NULL DEFAULT 0, + has_webcam INTEGER NOT NULL DEFAULT 0, + file_size INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT NOT NULL, + upload_completed INTEGER NOT NULL DEFAULT 0, + view_count INTEGER NOT NULL DEFAULT 0, + password_hash TEXT, + cta_url TEXT, + cta_text TEXT, + last_notified_view_count INTEGER NOT NULL DEFAULT 0, + is_meeting INTEGER NOT NULL DEFAULT 0, + summary TEXT + )`, + `CREATE TABLE transcript_segments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + start_time REAL NOT NULL, + end_time REAL NOT NULL, + text TEXT NOT NULL, + speaker TEXT + )`, + `CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + emoji TEXT NOT NULL, + client_ip TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + `CREATE TABLE comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + author_name TEXT NOT NULL DEFAULT 'Anonymous', + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + client_ip TEXT NOT NULL DEFAULT '' + )`, + `CREATE TABLE chapters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + timestamp REAL NOT NULL, + title TEXT NOT NULL + )`, +]; + +async function dropAllTables() { + const { results } = await env.DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' + AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND name NOT LIKE '\\_cf\\_%' ESCAPE '\\'`, + ).all(); + for (const { name } of results) { + await env.DB.prepare(`DROP TABLE IF EXISTS "${name}"`).run(); + } +} + +async function createV1Schema() { + for (const sql of V1_STATEMENTS) { + await env.DB.prepare(sql).run(); + } +} + +async function videoColumns() { + const { results } = await env.DB.prepare(`PRAGMA table_info(videos)`).all(); + return results.map((c) => c.name); +} + +async function upload() { + return SELF.fetch(`${BASE}/api/upload`, { + method: "POST", + headers: { ...AUTH, "Content-Type": "application/json" }, + body: JSON.stringify({ title: "Migration Test", duration: 3 }), + }); +} + +describe("schema migration paths", () => { + beforeEach(async () => { + await dropAllTables(); + __resetSchemaCacheForTests(); + }); + + // Leave a clean, fully-migrated database for any test file that runs after. + afterAll(async () => { + await dropAllTables(); + __resetSchemaCacheForTests(); + }); + + it("migrates a pre-versioning database (tables exist, no _meta)", async () => { + await createV1Schema(); + + const res = await upload(); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.shareCode).toBeTruthy(); + + expect(await videoColumns()).toContain("password_salt"); + const meta = await env.DB.prepare( + `SELECT value FROM _meta WHERE key = 'schema_version'`, + ).first(); + expect(meta.value).toBe("5"); + expect(await videoColumns()).toContain("owner_id"); + }); + + it("repairs a database mis-stamped as current without the v2 columns", async () => { + // The exact state v4.1.0 left production in: v1 tables, _meta says 2. + await createV1Schema(); + await env.DB.prepare(`CREATE TABLE _meta (key TEXT PRIMARY KEY, value TEXT)`).run(); + await env.DB.prepare(`INSERT INTO _meta (key, value) VALUES ('schema_version', '2')`).run(); + + const res = await upload(); + expect(res.status).toBe(200); + + expect(await videoColumns()).toContain("password_salt"); + }); + + it("re-running migrations on an already-migrated database is a no-op", async () => { + await createV1Schema(); + expect((await upload()).status).toBe(200); // migrates + + __resetSchemaCacheForTests(); // simulate a new isolate cold start + expect((await upload()).status).toBe(200); // must not throw duplicate-column + }); + + it("bootstraps a truly fresh database with the consolidated schema", async () => { + const res = await upload(); + expect(res.status).toBe(200); + + const cols = await videoColumns(); + expect(cols).toContain("password_salt"); + const tables = await env.DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'password_attempts'`, + ).first(); + expect(tables).toBeTruthy(); + expect( + await env.DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'comment_users'`, + ).first(), + ).toBeTruthy(); + }); + + it("password flow works end-to-end on a migrated pre-versioning database", async () => { + await createV1Schema(); + + const hash = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"; // sha256("hello") + const res = await SELF.fetch(`${BASE}/api/upload`, { + method: "POST", + headers: { ...AUTH, "Content-Type": "application/json" }, + body: JSON.stringify({ title: "Protected", duration: 3, password_hash: hash }), + }); + expect(res.status).toBe(200); + const { shareCode } = await res.json(); + await env.DB.prepare(`UPDATE videos SET upload_completed = 1 WHERE share_code = ?`) + .bind(shareCode) + .run(); + + const verify = await SELF.fetch(`${BASE}/s/${shareCode}/verify-password`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: "hello" }), + }); + expect(verify.status).toBe(200); + }); +}); diff --git a/services/recordly-share/worker/vitest.config.js b/services/recordly-share/worker/vitest.config.js new file mode 100644 index 000000000..d5ce03d0a --- /dev/null +++ b/services/recordly-share/worker/vitest.config.js @@ -0,0 +1,22 @@ +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + // The worker caches `schemaReady` at module level, so per-test storage + // rollback would leave the flag true with the tables gone. Tests create + // their own rows and don't depend on a clean DB. + isolatedStorage: false, + singleWorker: true, + wrangler: { configPath: './wrangler.test.jsonc' }, + miniflare: { + bindings: { + API_SECRET: 'test-secret', + ALLOW_API_SECRET_UPLOADS: 'true', + }, + }, + }, + }, + }, +}); diff --git a/services/recordly-share/worker/web/astro.config.mjs b/services/recordly-share/worker/web/astro.config.mjs new file mode 100644 index 000000000..0c90c7a01 --- /dev/null +++ b/services/recordly-share/worker/web/astro.config.mjs @@ -0,0 +1,12 @@ +import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + output: 'static', + integrations: [react()], + vite: { plugins: [tailwindcss()] }, + build: { + format: 'file', + }, +}); diff --git a/services/recordly-share/worker/web/dist/_astro/LibraryPage.Dwlp7Hh7.js b/services/recordly-share/worker/web/dist/_astro/LibraryPage.Dwlp7Hh7.js new file mode 100644 index 000000000..f03163d80 --- /dev/null +++ b/services/recordly-share/worker/web/dist/_astro/LibraryPage.Dwlp7Hh7.js @@ -0,0 +1 @@ +import{$ as A0,a as u0,b as g0,c as p0,d as O0,e as D0,t as z0,f as I0,g as T0,h as B0,i as U0,j as C,k as X,l as Z0,m as e0,n as H0,o as M0,p as j0,q as $0,r as q0,s as W0,u as Y0,v as f0,w as G0,x as J0,y as K0,z as Q0,A as X0,B as a,C as e1,D as a1,E as a0,F as S,G as V,H as t1,S as l1,I as n1,J as j,K as p,L as x0,M as g,N as r1,O as J,T as s1,P as c1,Q as v0,R as o1,U as i1,V as d1,W as m1,X as h1,Y as w}from"./ShareUI.CpnBv7vU.js";import{r as e,$ as A}from"./index.DeQQz02V.js";let E=typeof document<"u"&&window.visualViewport;function u1(){let t=A0(),[l,r]=e.useState(()=>t?{width:0,height:0}:G());return e.useEffect(()=>{let s=i=>{r(h=>i.width===h.width&&i.height===h.height?h:i)},c=()=>{E&&E.scale>1||s(G())},d,m=i=>{E&&E.scale>1||p0(O0(i))&&(d=requestAnimationFrame(()=>{let h=D0();(!h||!p0(h))&&s({width:document.documentElement.clientWidth,height:document.documentElement.clientHeight})}))};return s(G()),u0()&&g0()&&window.addEventListener("blur",m,!0),E?E.addEventListener("resize",c):window.addEventListener("resize",c),()=>{cancelAnimationFrame(d),u0()&&g0()&&window.removeEventListener("blur",m,!0),E?E.removeEventListener("resize",c):window.removeEventListener("resize",c)}},[]),l}function G(){return{width:E?Math.min(E.width*E.scale,document.documentElement.clientWidth):document.documentElement.clientWidth,height:E?E.height*E.scale:document.documentElement.clientHeight}}const t0=z0({defaultVariants:{scroll:"inside",size:"md",variant:"opaque"},slots:{backdrop:"modal__backdrop",body:"modal__body",closeTrigger:"modal__close-trigger",container:"modal__container",dialog:"modal__dialog",footer:"modal__footer",header:"modal__header",heading:"modal__heading",icon:"modal__icon",trigger:"modal__trigger"},variants:{scroll:{inside:{body:"modal__body--scroll-inside",dialog:"modal__dialog--scroll-inside"},outside:{body:"modal__body--scroll-outside",container:"modal__container--scroll-outside",dialog:"modal__dialog--scroll-outside"}},size:{cover:{dialog:"modal__dialog--cover"},full:{container:"modal__container--full",dialog:"modal__dialog--full"},lg:{dialog:"modal__dialog--lg"},md:{dialog:"modal__dialog--md"},sm:{dialog:"modal__dialog--sm"},xs:{dialog:"modal__dialog--xs"}},variant:{blur:{backdrop:"modal__backdrop--blur"},opaque:{backdrop:"modal__backdrop--opaque"},transparent:{backdrop:"modal__backdrop--transparent"}}}});function g1(t,l,r){let{overlayProps:s,underlayProps:c}=I0({...t,isOpen:l.isOpen,onClose:l.close},r);return T0({isDisabled:!l.isOpen}),B0(),e.useEffect(()=>{if(l.isOpen&&r.current)return U0([r.current],{shouldUseInert:!0})},[l.isOpen,r]),{modalProps:C(s),underlayProps:c}}const p1=e.createContext(null),l0=e.createContext(null),f1=e.forwardRef(function(l,r){if(e.useContext(l0))return A.createElement(b0,{...l,modalRef:r},l.children);let{isDismissable:c,isKeyboardDismissDisabled:d,isOpen:m,defaultOpen:i,onOpenChange:h,children:u,isEntering:H,isExiting:v,UNSTABLE_portalContainer:M,shouldCloseOnInteractOutside:f,...L}=l;return A.createElement(y0,{isDismissable:c,isKeyboardDismissDisabled:d,isOpen:m,defaultOpen:i,onOpenChange:h,isEntering:H,isExiting:v,UNSTABLE_portalContainer:M,shouldCloseOnInteractOutside:f},A.createElement(b0,{...L,modalRef:r},u))});function x1(t,l){[t,l]=W0(t,l,p1);let r=e.useContext(X),s=Y0(t),c=t.isOpen!=null||t.defaultOpen!=null||!r?s:r,d=e0(l),m=e.useRef(null),i=f0(d,c.isOpen),h=f0(m,c.isOpen),u=i||h||t.isExiting||!1,H=A0();return!c.isOpen&&!u||H?null:A.createElement(b1,{...t,state:c,isExiting:u,overlayRef:d,modalRef:m})}const y0=e.forwardRef(x1);function b1({UNSTABLE_portalContainer:t,...l}){let r=l.modalRef,{state:s}=l,{modalProps:c,underlayProps:d}=g1(l,s,r),m=H0(l.overlayRef)||l.isEntering||!1,i=M0({...l,defaultClassName:"react-aria-ModalOverlay",values:{isEntering:m,isExiting:l.isExiting,state:s}}),h=u1(),u,H;if(typeof document<"u"){let M=G0(document.body)?document.body:document.scrollingElement||document.documentElement,f=M.getBoundingClientRect().width%1,L=M.getBoundingClientRect().height%1;u=M.scrollWidth-f,H=M.scrollHeight-L}let v={...i.style,"--visual-viewport-width":h.width+"px","--visual-viewport-height":h.height+"px","--page-width":u!==void 0?u+"px":void 0,"--page-height":H!==void 0?H+"px":void 0};return A.createElement(J0,{isExiting:l.isExiting,portalContainer:t},A.createElement(j0.div,{...C($0(l,{global:!0}),d),...i,style:v,ref:l.overlayRef,"data-entering":m||void 0,"data-exiting":l.isExiting||void 0},A.createElement(K0,{values:[[l0,{modalProps:c,modalRef:r,isExiting:l.isExiting,isDismissable:l.isDismissable}],[X,s]]},i.children)))}function b0(t){let{modalProps:l,modalRef:r,isExiting:s,isDismissable:c}=e.useContext(l0),d=e.useContext(X),m=e.useMemo(()=>Z0(t.modalRef,r),[t.modalRef,r]),i=e0(m),h=H0(i),u=M0({...t,defaultClassName:"react-aria-Modal",values:{isEntering:h,isExiting:s,state:d}});return A.createElement(j0.div,{...C($0(t,{global:!0}),l),...u,ref:i,"data-entering":h||void 0,"data-exiting":s||void 0},c&&A.createElement(q0,{onDismiss:d.close}),u.children)}const E1=A.forwardRef(({children:t,...l},r)=>{r=e0(r);let{pressProps:s}=Q0({...l,ref:r}),{focusableProps:c}=X0(l,r),d=A.Children.only(t);e.useEffect(()=>{},[r,l.isDisabled]);let m=parseInt(A.version,10)<19?d.ref:d.props.ref;return A.cloneElement(d,{...C(s,c,d.props),ref:Z0(m,r)})}),A1={variant:"default"},Z=e.createContext({}),E0=({children:t,state:l,...r})=>{const s=e.useMemo(()=>({slots:t0(),placement:void 0}),[]),c=e.useMemo(()=>l?{isOpen:l.isOpen,onOpenChange:l.setOpen}:{},[l]);return a.jsx(Z,{value:s,children:a.jsx(e1,{"data-slot":"modal-root",...C(r,c),children:t})})},Z1=({children:t,className:l,...r})=>{const{slots:s}=e.use(Z);return a.jsx(E1,{children:a.jsx(S.div,{className:V(s?.trigger,l),"data-slot":"modal-trigger",role:"button",...r,children:t})})},H1=({children:t,className:l,isDismissable:r=!0,onClick:s,variant:c,...d})=>{const{slots:m}=e.use(Z),i=e.useMemo(()=>t0({variant:c}),[c]),h=e.useMemo(()=>({slots:{...m,...i}}),[m,i]);return a.jsx(y0,{className:a0(l,i?.backdrop()),"data-slot":"modal-backdrop",isDismissable:r,onClick:u=>{u.stopPropagation(),s?.(u)},...d,children:u=>a.jsxs(Z,{value:h,children:[typeof t=="function"?t(u):t," "]})})},M1=({children:t,className:l,placement:r="auto",scroll:s,size:c,...d})=>{const{slots:m}=e.use(Z),i=e.useMemo(()=>t0({scroll:s,size:c}),[s,c]),h=e.useMemo(()=>({placement:r,slots:{...m,...i}}),[m,r,i]);return a.jsx(f1,{className:a0(l,i?.container()),"data-placement":r,"data-slot":"modal-container",...d,children:u=>a.jsx(Z,{value:h,children:typeof t=="function"?t(u):t})})},j1=({children:t,className:l,...r})=>{const{placement:s,slots:c}=e.use(Z);return a.jsx(l1,{value:A1,children:a.jsx(n1,{className:V(c?.dialog,l),"data-placement":s,"data-slot":"modal-dialog",...r,children:t})})},$1=({children:t,className:l,...r})=>{const{slots:s}=e.use(Z);return a.jsx(S.div,{className:V(s?.header,l),"data-slot":"modal-header",...r,children:t})},v1=({children:t,className:l,...r})=>{const{slots:s}=e.use(Z);return a.jsx(S.div,{className:V(s?.body,l),"data-slot":"modal-body",...r,children:t})},y1=({children:t,className:l,...r})=>{const{slots:s}=e.use(Z);return a.jsx(S.div,{className:V(s?.footer,l),"data-slot":"modal-footer",...r,children:t})},w1=({children:t,className:l,...r})=>{const{slots:s}=e.use(Z);return a.jsx(t1,{className:V(s?.heading,l),"data-slot":"modal-heading",slot:"title",...r,children:t})},V1=({children:t,className:l,...r})=>{const{slots:s}=e.use(Z);return a.jsx(S.div,{className:V(s?.icon,l),"data-slot":"modal-icon",...r,children:t})},L1=({className:t,...l})=>{const{slots:r}=e.use(Z);return a.jsx(a1,{className:a0(t,r?.closeTrigger()),"data-slot":"modal-close-trigger",slot:"close",...l})},b=Object.assign(E0,{Root:E0,Trigger:Z1,Backdrop:H1,Container:M1,Dialog:j1,Header:$1,Icon:V1,Heading:w1,Body:v1,Footer:y1,CloseTrigger:L1}),N1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216.49,104.49l-80,80a12,12,0,0,1-17,0l-80-80a12,12,0,0,1,17-17L128,159l71.51-71.52a12,12,0,0,1,17,17Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,96l-80,80L48,96Z",opacity:"0.2"}),e.createElement("path",{d:"M215.39,92.94A8,8,0,0,0,208,88H48a8,8,0,0,0-5.66,13.66l80,80a8,8,0,0,0,11.32,0l80-80A8,8,0,0,0,215.39,92.94ZM128,164.69,67.31,104H188.69Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M212.24,100.24l-80,80a6,6,0,0,1-8.48,0l-80-80a6,6,0,0,1,8.48-8.48L128,167.51l75.76-75.75a6,6,0,0,1,8.48,8.48Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M210.83,98.83l-80,80a4,4,0,0,1-5.66,0l-80-80a4,4,0,0,1,5.66-5.66L128,170.34l77.17-77.17a4,4,0,1,1,5.66,5.66Z"}))]]),F1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z",opacity:"0.2"}),e.createElement("path",{d:"M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M228.24,76.24l-128,128a6,6,0,0,1-8.48,0l-56-56a6,6,0,0,1,8.48-8.48L96,191.51,219.76,67.76a6,6,0,0,1,8.48,8.48Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M226.83,74.83l-128,128a4,4,0,0,1-5.66,0l-56-56a4,4,0,0,1,5.66-5.66L96,194.34,221.17,69.17a4,4,0,1,1,5.66,5.66Z"}))]]),_1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M160,36A92.09,92.09,0,0,0,79,84.36,68,68,0,1,0,72,220h88a92,92,0,0,0,0-184Zm0,160H72a44,44,0,0,1-1.82-88A91.86,91.86,0,0,0,68,128a12,12,0,0,0,24,0,68,68,0,1,1,68,68Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M240,128a80,80,0,0,1-80,80H72A56,56,0,1,1,85.92,97.74l0,.1A80,80,0,0,1,240,128Z",opacity:"0.2"}),e.createElement("path",{d:"M160,40A88.09,88.09,0,0,0,81.29,88.67,64,64,0,1,0,72,216h88a88,88,0,0,0,0-176Zm0,160H72a48,48,0,0,1,0-96c1.1,0,2.2,0,3.29.11A88,88,0,0,0,72,128a8,8,0,0,0,16,0,72,72,0,1,1,72,72Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M160.06,40A88.1,88.1,0,0,0,81.29,88.67h0A87.48,87.48,0,0,0,72,127.73,8.18,8.18,0,0,1,64.57,136,8,8,0,0,1,56,128a103.66,103.66,0,0,1,5.34-32.92,4,4,0,0,0-4.75-5.18A64.09,64.09,0,0,0,8,152c0,35.19,29.75,64,65,64H160a88.09,88.09,0,0,0,87.93-91.48C246.11,77.54,207.07,40,160.06,40Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M160,42A86.11,86.11,0,0,0,82.43,90.88,62,62,0,1,0,72,214h88a86,86,0,0,0,0-172Zm0,160H72a50,50,0,0,1,0-100,50.67,50.67,0,0,1,5.91.35A85.61,85.61,0,0,0,74,128a6,6,0,0,0,12,0,74,74,0,1,1,74,74Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M160,40A88.09,88.09,0,0,0,81.29,88.67,64,64,0,1,0,72,216h88a88,88,0,0,0,0-176Zm0,160H72a48,48,0,0,1,0-96c1.1,0,2.2,0,3.29.11A88,88,0,0,0,72,128a8,8,0,0,0,16,0,72,72,0,1,1,72,72Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M160,44A84.11,84.11,0,0,0,83.59,93.12,60.71,60.71,0,0,0,72,92a60,60,0,0,0,0,120h88a84,84,0,0,0,0-168Zm0,160H72a52,52,0,1,1,8.55-103.3A83.66,83.66,0,0,0,76,128a4,4,0,0,0,8,0,76,76,0,1,1,76,76Z"}))]]),C1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M144,128a16,16,0,1,1-16-16A16,16,0,0,1,144,128ZM60,112a16,16,0,1,0,16,16A16,16,0,0,0,60,112Zm136,0a16,16,0,1,0,16,16A16,16,0,0,0,196,112Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M240,96v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V96A16,16,0,0,1,32,80H224A16,16,0,0,1,240,96Z",opacity:"0.2"}),e.createElement("path",{d:"M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm56-12a12,12,0,1,0,12,12A12,12,0,0,0,196,116ZM60,116a12,12,0,1,0,12,12A12,12,0,0,0,60,116Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V96A16,16,0,0,0,224,80ZM60,140a12,12,0,1,1,12-12A12,12,0,0,1,60,140Zm68,0a12,12,0,1,1,12-12A12,12,0,0,1,128,140Zm68,0a12,12,0,1,1,12-12A12,12,0,0,1,196,140Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M138,128a10,10,0,1,1-10-10A10,10,0,0,1,138,128ZM60,118a10,10,0,1,0,10,10A10,10,0,0,0,60,118Zm136,0a10,10,0,1,0,10,10A10,10,0,0,0,196,118Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm56-12a12,12,0,1,0,12,12A12,12,0,0,0,196,116ZM60,116a12,12,0,1,0,12,12A12,12,0,0,0,60,116Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M136,128a8,8,0,1,1-8-8A8,8,0,0,1,136,128Zm-76-8a8,8,0,1,0,8,8A8,8,0,0,0,60,120Zm136,0a8,8,0,1,0,8,8A8,8,0,0,0,196,120Z"}))]]),S1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,68H132L105.33,48a20.12,20.12,0,0,0-12-4H40A20,20,0,0,0,20,64V200a20,20,0,0,0,20,20H216.89A19.13,19.13,0,0,0,236,200.89V88A20,20,0,0,0,216,68Zm-4,128H44V68H92l28.8,21.6A12,12,0,0,0,128,92h84Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,88V200.89a7.11,7.11,0,0,1-7.11,7.11H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6L128,80h88A8,8,0,0,1,224,88Z",opacity:"0.2"}),e.createElement("path",{d:"M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232,88V200.89A15.13,15.13,0,0,1,216.89,216H40a16,16,0,0,1-16-16V64A16,16,0,0,1,40,48H93.33a16.12,16.12,0,0,1,9.6,3.2L130.67,72H216A16,16,0,0,1,232,88Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,74H130L101.73,52.8a14,14,0,0,0-8.4-2.8H40A14,14,0,0,0,26,64V200a14,14,0,0,0,14,14H216.89A13.12,13.12,0,0,0,230,200.89V88A14,14,0,0,0,216,74Zm2,126.89a1.11,1.11,0,0,1-1.11,1.11H40a2,2,0,0,1-2-2V64a2,2,0,0,1,2-2H93.33a2,2,0,0,1,1.2.4L124.4,84.8A6,6,0,0,0,128,86h88a2,2,0,0,1,2,2Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,76H129.33l-28.8-21.6a12.05,12.05,0,0,0-7.2-2.4H40A12,12,0,0,0,28,64V200a12,12,0,0,0,12,12H216.89A11.12,11.12,0,0,0,228,200.89V88A12,12,0,0,0,216,76Zm4,124.89a3.12,3.12,0,0,1-3.11,3.11H40a4,4,0,0,1-4-4V64a4,4,0,0,1,4-4H93.33a4,4,0,0,1,2.4.8L125.6,83.2a4,4,0,0,0,2.4.8h88a4,4,0,0,1,4,4Z"}))]]),k1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,76a52,52,0,1,0,52,52A52.06,52.06,0,0,0,128,76Zm0,80a28,28,0,1,1,28-28A28,28,0,0,1,128,156Zm113.86-49.57A12,12,0,0,0,236,98.34L208.21,82.49l-.11-31.31a12,12,0,0,0-4.25-9.12,116,116,0,0,0-38-21.41,12,12,0,0,0-9.68.89L128,37.27,99.83,21.53a12,12,0,0,0-9.7-.9,116.06,116.06,0,0,0-38,21.47,12,12,0,0,0-4.24,9.1l-.14,31.34L20,98.35a12,12,0,0,0-5.85,8.11,110.7,110.7,0,0,0,0,43.11A12,12,0,0,0,20,157.66l27.82,15.85.11,31.31a12,12,0,0,0,4.25,9.12,116,116,0,0,0,38,21.41,12,12,0,0,0,9.68-.89L128,218.73l28.14,15.74a12,12,0,0,0,9.7.9,116.06,116.06,0,0,0,38-21.47,12,12,0,0,0,4.24-9.1l.14-31.34,27.81-15.81a12,12,0,0,0,5.85-8.11A110.7,110.7,0,0,0,241.86,106.43Zm-22.63,33.18-26.88,15.28a11.94,11.94,0,0,0-4.55,4.59c-.54,1-1.11,1.93-1.7,2.88a12,12,0,0,0-1.83,6.31L184.13,199a91.83,91.83,0,0,1-21.07,11.87l-27.15-15.19a12,12,0,0,0-5.86-1.53h-.29c-1.14,0-2.3,0-3.44,0a12.08,12.08,0,0,0-6.14,1.51L93,210.82A92.27,92.27,0,0,1,71.88,199l-.11-30.24a12,12,0,0,0-1.83-6.32c-.58-.94-1.16-1.91-1.7-2.88A11.92,11.92,0,0,0,63.7,155L36.8,139.63a86.53,86.53,0,0,1,0-23.24l26.88-15.28a12,12,0,0,0,4.55-4.58c.54-1,1.11-1.94,1.7-2.89a12,12,0,0,0,1.83-6.31L71.87,57A91.83,91.83,0,0,1,92.94,45.17l27.15,15.19a11.92,11.92,0,0,0,6.15,1.52c1.14,0,2.3,0,3.44,0a12.08,12.08,0,0,0,6.14-1.51L163,45.18A92.27,92.27,0,0,1,184.12,57l.11,30.24a12,12,0,0,0,1.83,6.32c.58.94,1.16,1.91,1.7,2.88A11.92,11.92,0,0,0,192.3,101l26.9,15.33A86.53,86.53,0,0,1,219.23,139.61Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M230.1,108.76,198.25,90.62c-.64-1.16-1.31-2.29-2-3.41l-.12-36A104.61,104.61,0,0,0,162,32L130,49.89c-1.34,0-2.69,0-4,0L94,32A104.58,104.58,0,0,0,59.89,51.25l-.16,36c-.7,1.12-1.37,2.26-2,3.41l-31.84,18.1a99.15,99.15,0,0,0,0,38.46l31.85,18.14c.64,1.16,1.31,2.29,2,3.41l.12,36A104.61,104.61,0,0,0,94,224l32-17.87c1.34,0,2.69,0,4,0L162,224a104.58,104.58,0,0,0,34.08-19.25l.16-36c.7-1.12,1.37-2.26,2-3.41l31.84-18.1A99.15,99.15,0,0,0,230.1,108.76ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z",opacity:"0.2"}),e.createElement("path",{d:"M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm109.94-52.79a8,8,0,0,0-3.89-5.4l-29.83-17-.12-33.62a8,8,0,0,0-2.83-6.08,111.91,111.91,0,0,0-36.72-20.67,8,8,0,0,0-6.46.59L128,41.85,97.88,25a8,8,0,0,0-6.47-.6A111.92,111.92,0,0,0,54.73,45.15a8,8,0,0,0-2.83,6.07l-.15,33.65-29.83,17a8,8,0,0,0-3.89,5.4,106.47,106.47,0,0,0,0,41.56,8,8,0,0,0,3.89,5.4l29.83,17,.12,33.63a8,8,0,0,0,2.83,6.08,111.91,111.91,0,0,0,36.72,20.67,8,8,0,0,0,6.46-.59L128,214.15,158.12,231a7.91,7.91,0,0,0,3.9,1,8.09,8.09,0,0,0,2.57-.42,112.1,112.1,0,0,0,36.68-20.73,8,8,0,0,0,2.83-6.07l.15-33.65,29.83-17a8,8,0,0,0,3.89-5.4A106.47,106.47,0,0,0,237.94,107.21Zm-15,34.91-28.57,16.25a8,8,0,0,0-3,3c-.58,1-1.19,2.06-1.81,3.06a7.94,7.94,0,0,0-1.22,4.21l-.15,32.25a95.89,95.89,0,0,1-25.37,14.3L134,199.13a8,8,0,0,0-3.91-1h-.19c-1.21,0-2.43,0-3.64,0a8.1,8.1,0,0,0-4.1,1l-28.84,16.1A96,96,0,0,1,67.88,201l-.11-32.2a8,8,0,0,0-1.22-4.22c-.62-1-1.23-2-1.8-3.06a8.09,8.09,0,0,0-3-3.06l-28.6-16.29a90.49,90.49,0,0,1,0-28.26L61.67,97.63a8,8,0,0,0,3-3c.58-1,1.19-2.06,1.81-3.06a7.94,7.94,0,0,0,1.22-4.21l.15-32.25a95.89,95.89,0,0,1,25.37-14.3L122,56.87a8,8,0,0,0,4.1,1c1.21,0,2.43,0,3.64,0a8,8,0,0,0,4.1-1l28.84-16.1A96,96,0,0,1,188.12,55l.11,32.2a8,8,0,0,0,1.22,4.22c.62,1,1.23,2,1.8,3.06a8.09,8.09,0,0,0,3,3.06l28.6,16.29A90.49,90.49,0,0,1,222.9,142.12Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M237.94,107.21a8,8,0,0,0-3.89-5.4l-29.83-17-.12-33.62a8,8,0,0,0-2.83-6.08,111.91,111.91,0,0,0-36.72-20.67,8,8,0,0,0-6.46.59L128,41.85,97.88,25a8,8,0,0,0-6.47-.6A111.92,111.92,0,0,0,54.73,45.15a8,8,0,0,0-2.83,6.07l-.15,33.65-29.83,17a8,8,0,0,0-3.89,5.4,106.47,106.47,0,0,0,0,41.56,8,8,0,0,0,3.89,5.4l29.83,17,.12,33.63a8,8,0,0,0,2.83,6.08,111.91,111.91,0,0,0,36.72,20.67,8,8,0,0,0,6.46-.59L128,214.15,158.12,231a7.91,7.91,0,0,0,3.9,1,8.09,8.09,0,0,0,2.57-.42,112.1,112.1,0,0,0,36.68-20.73,8,8,0,0,0,2.83-6.07l.15-33.65,29.83-17a8,8,0,0,0,3.89-5.4A106.47,106.47,0,0,0,237.94,107.21ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,82a46,46,0,1,0,46,46A46.06,46.06,0,0,0,128,82Zm0,80a34,34,0,1,1,34-34A34,34,0,0,1,128,162Zm108-54.4a6,6,0,0,0-2.92-4L202.64,86.22l-.42-.71L202.1,51.2A6,6,0,0,0,200,46.64a110.12,110.12,0,0,0-36.07-20.31,6,6,0,0,0-4.84.45L128.46,43.86h-1L96.91,26.76a6,6,0,0,0-4.86-.44A109.92,109.92,0,0,0,56,46.68a6,6,0,0,0-2.12,4.55l-.16,34.34c-.14.23-.28.47-.41.71L22.91,103.57A6,6,0,0,0,20,107.62a104.81,104.81,0,0,0,0,40.78,6,6,0,0,0,2.92,4l30.42,17.33.42.71.12,34.31A6,6,0,0,0,56,209.36a110.12,110.12,0,0,0,36.07,20.31,6,6,0,0,0,4.84-.45l30.61-17.08h1l30.56,17.1A6.09,6.09,0,0,0,162,230a5.83,5.83,0,0,0,1.93-.32,109.92,109.92,0,0,0,36-20.36,6,6,0,0,0,2.12-4.55l.16-34.34c.14-.23.28-.47.41-.71l30.42-17.29a6,6,0,0,0,2.92-4.05A104.81,104.81,0,0,0,236,107.6Zm-11.25,35.79L195.32,160.1a6.07,6.07,0,0,0-2.28,2.3c-.59,1-1.21,2.11-1.86,3.14a6,6,0,0,0-.91,3.16l-.16,33.21a98.15,98.15,0,0,1-27.52,15.53L133,200.88a6,6,0,0,0-2.93-.77h-.14c-1.24,0-2.5,0-3.74,0a6,6,0,0,0-3.07.76L93.45,217.43a98,98,0,0,1-27.56-15.49l-.12-33.17a6,6,0,0,0-.91-3.16c-.64-1-1.27-2.08-1.86-3.14a6,6,0,0,0-2.27-2.3L31.3,143.4a93,93,0,0,1,0-30.79L60.68,95.9A6.07,6.07,0,0,0,63,93.6c.59-1,1.21-2.11,1.86-3.14a6,6,0,0,0,.91-3.16l.16-33.21A98.15,98.15,0,0,1,93.41,38.56L123,55.12a5.81,5.81,0,0,0,3.07.76c1.24,0,2.5,0,3.74,0a6,6,0,0,0,3.07-.76l29.65-16.56a98,98,0,0,1,27.56,15.49l.12,33.17a6,6,0,0,0,.91,3.16c.64,1,1.27,2.08,1.86,3.14a6,6,0,0,0,2.27,2.3L224.7,112.6A93,93,0,0,1,224.73,143.39Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm109.94-52.79a8,8,0,0,0-3.89-5.4l-29.83-17-.12-33.62a8,8,0,0,0-2.83-6.08,111.91,111.91,0,0,0-36.72-20.67,8,8,0,0,0-6.46.59L128,41.85,97.88,25a8,8,0,0,0-6.47-.6A112.1,112.1,0,0,0,54.73,45.15a8,8,0,0,0-2.83,6.07l-.15,33.65-29.83,17a8,8,0,0,0-3.89,5.4,106.47,106.47,0,0,0,0,41.56,8,8,0,0,0,3.89,5.4l29.83,17,.12,33.62a8,8,0,0,0,2.83,6.08,111.91,111.91,0,0,0,36.72,20.67,8,8,0,0,0,6.46-.59L128,214.15,158.12,231a7.91,7.91,0,0,0,3.9,1,8.09,8.09,0,0,0,2.57-.42,112.1,112.1,0,0,0,36.68-20.73,8,8,0,0,0,2.83-6.07l.15-33.65,29.83-17a8,8,0,0,0,3.89-5.4A106.47,106.47,0,0,0,237.94,107.21Zm-15,34.91-28.57,16.25a8,8,0,0,0-3,3c-.58,1-1.19,2.06-1.81,3.06a7.94,7.94,0,0,0-1.22,4.21l-.15,32.25a95.89,95.89,0,0,1-25.37,14.3L134,199.13a8,8,0,0,0-3.91-1h-.19c-1.21,0-2.43,0-3.64,0a8.08,8.08,0,0,0-4.1,1l-28.84,16.1A96,96,0,0,1,67.88,201l-.11-32.2a8,8,0,0,0-1.22-4.22c-.62-1-1.23-2-1.8-3.06a8.09,8.09,0,0,0-3-3.06l-28.6-16.29a90.49,90.49,0,0,1,0-28.26L61.67,97.63a8,8,0,0,0,3-3c.58-1,1.19-2.06,1.81-3.06a7.94,7.94,0,0,0,1.22-4.21l.15-32.25a95.89,95.89,0,0,1,25.37-14.3L122,56.87a8,8,0,0,0,4.1,1c1.21,0,2.43,0,3.64,0a8.08,8.08,0,0,0,4.1-1l28.84-16.1A96,96,0,0,1,188.12,55l.11,32.2a8,8,0,0,0,1.22,4.22c.62,1,1.23,2,1.8,3.06a8.09,8.09,0,0,0,3,3.06l28.6,16.29A90.49,90.49,0,0,1,222.9,142.12Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,84a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,84Zm0,80a36,36,0,1,1,36-36A36,36,0,0,1,128,164Zm106-56a4,4,0,0,0-2-2.7l-30.89-17.6q-.47-.82-1-1.62L200.1,51.2a3.94,3.94,0,0,0-1.42-3,107.8,107.8,0,0,0-35.41-19.94,4,4,0,0,0-3.23.29L129,45.87h-2l-31-17.36a4,4,0,0,0-3.23-.3,108.05,108.05,0,0,0-35.39,20,4,4,0,0,0-1.41,3l-.16,34.9-1,1.62L23.9,105.3A4,4,0,0,0,22,108a102.76,102.76,0,0,0,0,40,4,4,0,0,0,1.95,2.7l30.89,17.6q.47.83,1,1.62l.12,34.87a3.94,3.94,0,0,0,1.42,3,107.8,107.8,0,0,0,35.41,19.94,4,4,0,0,0,3.23-.29L127,210.13h2l31,17.36a4,4,0,0,0,3.23.3,108.05,108.05,0,0,0,35.39-20,4,4,0,0,0,1.41-3l.16-34.9,1-1.62L232.1,150.7a4,4,0,0,0,2-2.71A102.76,102.76,0,0,0,234,108Zm-7.48,36.67L196.3,161.84a4,4,0,0,0-1.51,1.53c-.61,1.09-1.25,2.17-1.91,3.24a3.92,3.92,0,0,0-.61,2.1l-.16,34.15a99.8,99.8,0,0,1-29.7,16.77l-30.4-17a4.06,4.06,0,0,0-2-.51H130c-1.28,0-2.57,0-3.84,0a4.1,4.1,0,0,0-2.05.51l-30.45,17A100.23,100.23,0,0,1,63.89,202.9l-.12-34.12a3.93,3.93,0,0,0-.61-2.11c-.66-1-1.3-2.14-1.91-3.23a4,4,0,0,0-1.51-1.53L29.49,144.68a94.78,94.78,0,0,1,0-33.34L59.7,94.16a4,4,0,0,0,1.51-1.53c.61-1.09,1.25-2.17,1.91-3.23a4,4,0,0,0,.61-2.11l.16-34.15a99.8,99.8,0,0,1,29.7-16.77l30.4,17a4.1,4.1,0,0,0,2.05.51c1.28,0,2.57,0,3.84,0a4,4,0,0,0,2.05-.51l30.45-17A100.23,100.23,0,0,1,192.11,53.1l.12,34.12a3.93,3.93,0,0,0,.61,2.11c.66,1,1.3,2.14,1.91,3.23a4,4,0,0,0,1.51,1.53l30.25,17.23A94.78,94.78,0,0,1,226.54,144.66Z"}))]]),R1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,28H48A20,20,0,0,0,28,48V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V48A20,20,0,0,0,208,28Zm-4,24v63.72L186.14,97.86a20,20,0,0,0-28.28,0L52,203.72V52ZM85.66,204,172,117.66l32,32V204ZM76,96a20,20,0,1,1,20,20A20,20,0,0,1,76,96Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,40H48a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8h8.69L166.34,106.34a8,8,0,0,1,11.32,0L216,144.69V48A8,8,0,0,0,208,40ZM96,112a16,16,0,1,1,16-16A16,16,0,0,1,96,112Z",opacity:"0.2"}),e.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM48,48H208v77.38l-24.69-24.7a16,16,0,0,0-22.62,0L53.37,208H48ZM208,208H76l96-96,36,36v60ZM96,120A24,24,0,1,0,72,96,24,24,0,0,0,96,120Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,96,88Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM48,48H208v77.38l-24.69-24.7a16,16,0,0,0-22.62,0L53.37,208H48ZM80,96a16,16,0,1,1,16,16A16,16,0,0,1,80,96Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,34H48A14,14,0,0,0,34,48V208a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V48A14,14,0,0,0,208,34ZM46,208V48a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2v82.2l-28.1-28.1a14,14,0,0,0-19.8,0L54.2,210H48A2,2,0,0,1,46,208Zm162,2H71.17l99.41-99.41a2,2,0,0,1,2.83,0L210,147.17V208A2,2,0,0,1,208,210ZM96,118A22,22,0,1,0,74,96,22,22,0,0,0,96,118Zm0-32A10,10,0,1,1,86,96,10,10,0,0,1,96,86Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM48,48H208v77.38l-24.69-24.7a16,16,0,0,0-22.62,0L53.37,208H48ZM208,208H76l96-96,36,36v60ZM96,120A24,24,0,1,0,72,96,24,24,0,0,0,96,120Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,96,88Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36ZM44,208V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4v87l-31.51-31.52a12,12,0,0,0-17,0L55,212H48A4,4,0,0,1,44,208Zm164,4H66.34L169.17,109.17a4,4,0,0,1,5.66,0L212,146.34V208A4,4,0,0,1,208,212ZM96,116A20,20,0,1,0,76,96,20,20,0,0,0,96,116Zm0-32A12,12,0,1,1,84,96,12,12,0,0,1,96,84Z"}))]]),P1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M192,112a80,80,0,1,1-80-80A80,80,0,0,1,192,112Z",opacity:"0.2"}),e.createElement("path",{d:"M229.66,218.34,179.6,168.28a88.21,88.21,0,1,0-11.32,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M168,112a56,56,0,1,1-56-56A56,56,0,0,1,168,112Zm61.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88,88,0,1,1,11.32-11.31l50.06,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M228.24,219.76l-51.38-51.38a86.15,86.15,0,1,0-8.48,8.48l51.38,51.38a6,6,0,0,0,8.48-8.48ZM38,112a74,74,0,1,1,74,74A74.09,74.09,0,0,1,38,112Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M226.83,221.17l-52.7-52.7a84.1,84.1,0,1,0-5.66,5.66l52.7,52.7a4,4,0,0,0,5.66-5.66ZM36,112a76,76,0,1,1,76,76A76.08,76.08,0,0,1,36,112Z"}))]]),O1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M228,128a12,12,0,0,1-12,12H140v76a12,12,0,0,1-24,0V140H40a12,12,0,0,1,0-24h76V40a12,12,0,0,1,24,0v76h76A12,12,0,0,1,228,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z",opacity:"0.2"}),e.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM184,136H136v48a8,8,0,0,1-16,0V136H72a8,8,0,0,1,0-16h48V72a8,8,0,0,1,16,0v48h48a8,8,0,0,1,0,16Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M222,128a6,6,0,0,1-6,6H134v82a6,6,0,0,1-12,0V134H40a6,6,0,0,1,0-12h82V40a6,6,0,0,1,12,0v82h82A6,6,0,0,1,222,128Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M220,128a4,4,0,0,1-4,4H132v84a4,4,0,0,1-8,0V132H40a4,4,0,0,1,0-8h84V40a4,4,0,0,1,8,0v84h84A4,4,0,0,1,220,128Z"}))]]),D1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M124,216a12,12,0,0,1-12,12H48a12,12,0,0,1-12-12V40A12,12,0,0,1,48,28h64a12,12,0,0,1,0,24H60V204h52A12,12,0,0,1,124,216Zm108.49-96.49-40-40a12,12,0,0,0-17,17L195,116H112a12,12,0,0,0,0,24h83l-19.52,19.51a12,12,0,0,0,17,17l40-40A12,12,0,0,0,232.49,119.51Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,56V200a16,16,0,0,1-16,16H48V40H208A16,16,0,0,1,224,56Z",opacity:"0.2"}),e.createElement("path",{d:"M120,216a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V40a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H56V208h56A8,8,0,0,1,120,216Zm109.66-93.66-40-40a8,8,0,0,0-11.32,11.32L204.69,120H112a8,8,0,0,0,0,16h92.69l-26.35,26.34a8,8,0,0,0,11.32,11.32l40-40A8,8,0,0,0,229.66,122.34Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M120,216a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V40a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H56V208h56A8,8,0,0,1,120,216Zm109.66-93.66-40-40A8,8,0,0,0,176,88v32H112a8,8,0,0,0,0,16h64v32a8,8,0,0,0,13.66,5.66l40-40A8,8,0,0,0,229.66,122.34Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M118,216a6,6,0,0,1-6,6H48a6,6,0,0,1-6-6V40a6,6,0,0,1,6-6h64a6,6,0,0,1,0,12H54V210h58A6,6,0,0,1,118,216Zm110.24-92.24-40-40a6,6,0,0,0-8.48,8.48L209.51,122H112a6,6,0,0,0,0,12h97.51l-29.75,29.76a6,6,0,1,0,8.48,8.48l40-40A6,6,0,0,0,228.24,123.76Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M120,216a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V40a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H56V208h56A8,8,0,0,1,120,216Zm109.66-93.66-40-40a8,8,0,0,0-11.32,11.32L204.69,120H112a8,8,0,0,0,0,16h92.69l-26.35,26.34a8,8,0,0,0,11.32,11.32l40-40A8,8,0,0,0,229.66,122.34Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M116,216a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V40a4,4,0,0,1,4-4h64a4,4,0,0,1,0,8H52V212h60A4,4,0,0,1,116,216Zm110.83-90.83-40-40a4,4,0,0,0-5.66,5.66L214.34,124H112a4,4,0,0,0,0,8H214.34l-33.17,33.17a4,4,0,0,0,5.66,5.66l40-40A4,4,0,0,0,226.83,125.17Z"}))]]),z1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,48H180V36A28,28,0,0,0,152,8H104A28,28,0,0,0,76,36V48H40a12,12,0,0,0,0,24h4V208a20,20,0,0,0,20,20H192a20,20,0,0,0,20-20V72h4a12,12,0,0,0,0-24ZM100,36a4,4,0,0,1,4-4h48a4,4,0,0,1,4,4V48H100Zm88,168H68V72H188ZM116,104v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Zm48,0v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56Z",opacity:"0.2"}),e.createElement("path",{d:"M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM112,168a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm0-120H96V40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,50H174V40a22,22,0,0,0-22-22H104A22,22,0,0,0,82,40V50H40a6,6,0,0,0,0,12H50V208a14,14,0,0,0,14,14H192a14,14,0,0,0,14-14V62h10a6,6,0,0,0,0-12ZM94,40a10,10,0,0,1,10-10h48a10,10,0,0,1,10,10V50H94ZM194,208a2,2,0,0,1-2,2H64a2,2,0,0,1-2-2V62H194ZM110,104v64a6,6,0,0,1-12,0V104a6,6,0,0,1,12,0Zm48,0v64a6,6,0,0,1-12,0V104a6,6,0,0,1,12,0Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,52H172V40a20,20,0,0,0-20-20H104A20,20,0,0,0,84,40V52H40a4,4,0,0,0,0,8H52V208a12,12,0,0,0,12,12H192a12,12,0,0,0,12-12V60h12a4,4,0,0,0,0-8ZM92,40a12,12,0,0,1,12-12h48a12,12,0,0,1,12,12V52H92ZM196,208a4,4,0,0,1-4,4H64a4,4,0,0,1-4-4V60H196ZM108,104v64a4,4,0,0,1-8,0V104a4,4,0,0,1,8,0Zm48,0v64a4,4,0,0,1-8,0V104a4,4,0,0,1,8,0Z"}))]]),w0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:N1}));w0.displayName="CaretDownIcon";const K=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:F1}));K.displayName="CheckIcon";const Q=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:_1}));Q.displayName="CloudIcon";const n0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:C1}));n0.displayName="DotsThreeIcon";const r0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:S1}));r0.displayName="FolderSimpleIcon";const V0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:k1}));V0.displayName="GearSixIcon";const L0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:R1}));L0.displayName="ImageSquareIcon";const N0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:P1}));N0.displayName="MagnifyingGlassIcon";const s0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:O1}));s0.displayName="PlusIcon";const F0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:D1}));F0.displayName="SignOutIcon";const _0=e.forwardRef((t,l)=>e.createElement(j,{ref:l,...t,weights:z1}));_0.displayName="TrashIcon";async function I1(){const t=await fetch("/api/videos",{credentials:"include"});if(t.status===401)return window.location.href="/library/login",[];if(!t.ok)throw new Error(`Failed to load videos (${t.status})`);return(await t.json()).videos||[]}async function T1(t){const l=await fetch(`/api/renew/${t}`,{method:"POST",credentials:"include"});return l.ok&&(await l.json()).expiresAt||null}async function B1(t){return(await fetch(`/api/delete/${t}`,{method:"DELETE",credentials:"include"})).ok}function I(t){const l=/[Zz]$|[+-]\d{2}:?\d{2}$/.test(t);return new Date(l?t:t.replace(" ","T")+"Z")}function U1({video:t,folders:l,assignFolder:r,selecting:s,selected:c,toggleSelected:d,busy:m,renew:i,remove:h,copy:u}){const[H,v]=e.useState(!1),M=l.filter(f=>f.codes.includes(t.share_code));return a.jsxs("li",{className:"group min-w-0",children:[a.jsxs(p,{variant:"ghost",isDisabled:m,"aria-label":t.title,onPress:()=>s?d():window.open(`/s/${t.share_code}`,"_blank","noopener,noreferrer"),"aria-pressed":s?c:void 0,className:"relative block h-auto w-full min-w-0 rounded-xl p-0",children:[a.jsx("div",{className:"relative flex aspect-[4/3] w-full items-center justify-center overflow-hidden rounded-xl bg-default/60",children:H?a.jsx(L0,{weight:"fill",className:"size-8 text-muted-foreground/20"}):a.jsx("img",{src:`/thumb/${t.share_code}`,alt:"",loading:"lazy",draggable:!1,onError:()=>v(!0),className:"h-full w-full object-cover"})}),s&&a.jsx("span",{className:`absolute right-2 top-2 flex size-5 items-center justify-center rounded-md ${c?"bg-accent text-white":"bg-background/90"}`,children:c&&a.jsx(K,{className:"size-3.5"})})]}),a.jsxs("div",{className:"relative flex items-start justify-between gap-4 pt-5",children:[a.jsx(x0,{"aria-label":"Your library",className:"!size-[48px] shrink-0 !rounded-full bg-accent/15 text-accent",children:a.jsx(x0.Fallback,{className:"!rounded-full bg-accent/15 text-xs font-medium text-accent",children:"LP"})}),a.jsxs("div",{className:"h-12 min-w-0 flex-1",children:[a.jsx("p",{title:t.title,className:"truncate pr-8 text-[12px] font-medium leading-5",children:t.title}),a.jsxs("div",{className:"mt-1 flex h-6 min-w-0 items-center gap-2",children:[a.jsx("p",{className:"shrink-0 text-[11px] text-muted-foreground",children:I(t.created_at).toLocaleDateString(void 0,{month:"short",day:"numeric"})}),M.map(f=>a.jsx("button",{title:`Remove from ${f.name}`,onClick:()=>r(t.share_code,f.id),className:"max-w-24 truncate rounded-full bg-default/60 px-2 py-1 text-[11px]",children:f.name},f.id)),a.jsxs(g,{children:[a.jsxs(p,{variant:"ghost",size:"sm","aria-label":`Add folder to ${t.title}`,className:"h-6 min-w-0 shrink-0 gap-1.5 rounded-full px-2.5 text-[11px] text-muted-foreground opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus:opacity-100",children:[a.jsx(s0,{className:"size-3"}),"Add folder"]}),a.jsx(g.Popover,{children:a.jsxs(g.Menu,{"aria-label":"Assign recording folder",children:[l.map(f=>a.jsxs(g.Item,{id:f.id,textValue:f.name,onAction:()=>r(t.share_code,f.id),children:[a.jsx(r0,{weight:"fill"}),f.name,f.codes.includes(t.share_code)&&a.jsx(K,{className:"size-3"})]},f.id)),!l.length&&a.jsx(g.Item,{id:"empty",isDisabled:!0,children:"Create a folder in the sidebar"})]})})]})]})]}),a.jsxs(g,{children:[a.jsx(p,{variant:"ghost",isIconOnly:!0,"aria-label":`Options for ${t.title}`,isDisabled:m,className:"absolute right-0 top-5 size-6 min-w-6 text-muted-foreground",children:a.jsx(n0,{weight:"bold",className:"size-5"})}),a.jsx(g.Popover,{children:a.jsxs(g.Menu,{"aria-label":"Recording options",children:[a.jsx(g.Item,{id:"open",onAction:()=>window.open(`/s/${t.share_code}`,"_blank","noopener,noreferrer"),children:"Open recording"}),a.jsx(g.Item,{id:"copy",onAction:u,children:"Copy link"}),a.jsx(g.Item,{id:"renew",onAction:i,children:"Renew share link"}),a.jsx(g.Item,{id:"delete",onAction:h,className:"text-danger",children:"Delete recording"})]})})]})]})]})}function Y1(){const[t,l]=e.useState(!1);return e.useEffect(()=>l(new URLSearchParams(location.search).has("error")),[]),a.jsxs(i1,{children:[a.jsx(d1,{size:28}),a.jsx("h1",{children:"Your library"}),a.jsx("p",{children:"Enter your dashboard password to manage shared recordings."}),a.jsxs("form",{method:"POST",action:"/library/login",className:"gate-form",children:[a.jsxs(m1,{name:"password",isRequired:!0,children:[a.jsx(h1,{children:"Password"}),a.jsx(J,{type:"password",autoComplete:"current-password",autoFocus:!0})]}),t&&a.jsx(v0,{children:"Incorrect password. Please try again."}),a.jsx(p,{type:"submit",children:"Unlock library"})]})]})}function G1(){const[t,l]=e.useState("shared"),[r,s]=e.useState(""),[c,d]=e.useState("all"),[m,i]=e.useState("recent"),[h,u]=e.useState([]),[H,v]=e.useState(!0),[M,f]=e.useState(""),[L,k]=e.useState(null),[R,N]=e.useState(null),c0=e.useCallback(async()=>{v(!0),f("");try{u(await I1())}catch{f("Could not load your recordings. Please try again.")}finally{v(!1)}},[]);e.useEffect(()=>{c0()},[c0]);async function C0(n){k(n);try{const o=await T1(n);if(!o)throw new Error;u(x=>x.map($=>$.share_code===n?{...$,expires_at:o}:$)),w.success("Share link renewed")}catch{w.danger("Could not renew this link. Try again.")}finally{k(null)}}async function S0(n){k(n);try{if(!await B1(n))throw new Error;return u(o=>o.filter(x=>x.share_code!==n)),w.success("Recording deleted"),!0}catch{return w.danger("Could not delete this recording. Try again."),!1}finally{k(null)}}const[y,o0]=e.useState([]),[i0,P]=e.useState(null),[T,B]=e.useState(""),[k0,U]=e.useState(!1),[O,q]=e.useState(!1),[D,F]=e.useState([]),[d0,m0]=e.useState(!1);e.useEffect(()=>{try{const n=JSON.parse(localStorage.getItem("recordly-cloud-folders")||"[]");Array.isArray(n)&&o0(n.filter(o=>!!o&&typeof o.id=="string"&&typeof o.name=="string"&&Array.isArray(o.codes)&&o.codes.every(x=>typeof x=="string")))}catch{}},[]);function z(n){o0(n);try{localStorage.setItem("recordly-cloud-folders",JSON.stringify(n))}catch{w.danger("Folders could not be saved in this browser.")}}function R0(n,o){z(y.map(x=>x.id===o?{...x,codes:x.codes.includes(n)?x.codes.filter($=>$!==n):[...x.codes,n]}:x))}function h0(){T.trim()&&z(y.map(n=>n.id===i0?{...n,name:T.trim()}:n)),P(null)}const W=h.filter(n=>{const o=Date.now()-I(n.created_at).getTime(),x=y.find($=>$.id===t);return n.title.toLocaleLowerCase().includes(r.trim().toLocaleLowerCase())&&(!x||x.codes.includes(n.share_code))&&(c==="all"||o<=(c==="week"?7:30)*864e5)}).sort((n,o)=>m==="name"?n.title.localeCompare(o.title):I(o.created_at).getTime()-I(n.created_at).getTime()),_=L!==null||d0,Y=n=>`h-10 w-full justify-start gap-3 px-3 text-[13px] ${n?"bg-default/50 font-medium":"text-muted-foreground"}`;async function P0(n){try{await navigator.clipboard.writeText(`${location.origin}/s/${n}`),w.success("Link copied")}catch{w.danger("Could not copy the link.")}}return a.jsxs(a.Fragment,{children:[a.jsx(r1,{}),a.jsxs("div",{className:"library-dashboard flex h-full flex-row gap-0 rounded-none bg-background p-0 text-foreground",children:[a.jsxs("aside",{"aria-label":"Library navigation",className:"flex w-48 shrink-0 flex-col bg-transparent px-4 pb-5 pt-12 lg:w-56",children:[a.jsxs("div",{className:"mb-4 flex h-10 items-center gap-2.5 px-3",children:[a.jsx("img",{src:"/icon-64.png",alt:"",className:"size-7 rounded-lg"}),a.jsx("span",{className:"text-[15px] font-semibold tracking-tight",children:"Recordly"})]}),a.jsx("nav",{className:"space-y-1",children:a.jsxs(p,{variant:"ghost",className:Y(t==="shared"),"aria-current":t==="shared"?"page":void 0,onPress:()=>l("shared"),children:[a.jsx(Q,{weight:t==="shared"?"fill":"regular",className:"size-[18px]"}),"Shared"]})}),a.jsxs("div",{className:"mb-2 mt-9 flex items-center justify-between pl-3",children:[a.jsx("h2",{className:"text-[13px] font-semibold tracking-tight text-foreground/80",children:"Folders"}),a.jsx(p,{variant:"ghost",isIconOnly:!0,className:"size-7 min-w-7","aria-label":"New folder",onPress:()=>{let n="Untitled folder",o=1;for(;y.some($=>$.name===n);)n=`Untitled folder ${o++}`;const x=crypto.randomUUID();z([...y,{id:x,name:n,codes:[]}]),P(x),B(n)},children:a.jsx(s0,{className:"size-3.5"})})]}),a.jsx("div",{className:"library-folders min-h-0 flex-1 space-y-1 overflow-y-auto",children:y.map(n=>a.jsxs("div",{className:`group flex h-10 items-center gap-1 rounded-lg px-1 ${t===n.id?"bg-default/70":"hover:bg-default/40"}`,children:[a.jsx(r0,{weight:"fill",className:"mx-2 size-4 shrink-0 text-accent"}),i0===n.id?a.jsx("form",{className:"min-w-0 flex-1",onSubmit:o=>{o.preventDefault(),h0()},children:a.jsx(J,{autoFocus:!0,"aria-label":"Folder name",maxLength:80,value:T,onChange:o=>B(o.target.value),onBlur:h0,onKeyDown:o=>{o.key==="Escape"&&P(null)},className:"h-8 w-full min-w-0 border-0 bg-transparent px-0 text-[13px] shadow-none"})}):a.jsx(p,{variant:"ghost","aria-current":t===n.id?"page":void 0,onPress:()=>l(n.id),className:"h-full min-w-0 flex-1 justify-start rounded-none px-0 text-[13px]",children:a.jsx("span",{className:"truncate",children:n.name})}),a.jsxs(g,{children:[a.jsx(p,{variant:"ghost",isIconOnly:!0,"aria-label":`Options for ${n.name}`,className:"size-6 min-w-6 text-muted-foreground opacity-0 group-hover:opacity-100 focus:opacity-100",children:a.jsx(n0,{className:"size-4"})}),a.jsx(g.Popover,{children:a.jsxs(g.Menu,{"aria-label":"Folder options",children:[a.jsx(g.Item,{id:"rename",onAction:()=>{P(n.id),B(n.name)},children:"Rename"}),a.jsx(g.Item,{id:"remove",onAction:()=>{z(y.filter(o=>o.id!==n.id)),t===n.id&&l("shared")},children:"Remove folder"})]})})]})]},n.id))}),a.jsxs("div",{className:"space-y-1 pt-6",children:[a.jsx(s1,{sidebar:!0}),a.jsxs(p,{variant:"ghost",className:Y(t==="settings"),onPress:()=>l("settings"),children:[a.jsx(V0,{weight:t==="settings"?"fill":"regular",className:"size-[18px]"}),"Settings"]}),a.jsxs(p,{variant:"ghost",className:Y(!1),onPress:()=>location.assign("/library/logout"),children:[a.jsx(F0,{className:"size-[18px]"}),"Log out"]})]})]}),a.jsxs(c1,{className:"my-3 mr-3 flex min-h-0 min-w-0 flex-1 flex-col gap-0 overflow-hidden rounded-2xl bg-background p-0 shadow-sm",children:[t!=="settings"&&a.jsxs(a.Fragment,{children:[a.jsx("header",{className:"flex h-24 shrink-0 items-center gap-5 px-7 pt-5 lg:px-10",children:a.jsxs("div",{className:"relative min-w-0 flex-1",children:[a.jsx(N0,{className:"pointer-events-none absolute left-3.5 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground/60"}),a.jsx(J,{"aria-label":"Search recordings",placeholder:"Search recordings…",value:r,onChange:n=>s(n.target.value),className:"h-11 w-full border-0 bg-default/30 pl-10 shadow-none"})]})}),a.jsxs("div",{className:"flex flex-wrap items-center gap-3 px-7 pb-7 lg:px-10",children:[a.jsxs("div",{className:"flex gap-2","aria-label":"Time filters",children:[[["all","All"],["week","Last 7 days"],["month","Last 30 days"]].map(([n,o])=>a.jsx(p,{variant:c===n?"secondary":"ghost",size:"sm","aria-pressed":c===n,onPress:()=>d(n),className:`h-7 rounded-lg px-3 text-xs ${c===n?"bg-default/60 text-foreground":"text-muted-foreground"}`,children:o},n)),a.jsx(p,{variant:"ghost",isIconOnly:!0,className:"size-7 min-w-7 text-danger","aria-label":"Select recordings to delete","aria-pressed":O,onPress:()=>{q(!O),F([])},children:a.jsx(_0,{weight:"fill",className:"size-4"})})]}),a.jsx("div",{className:"ml-auto",children:a.jsxs(g,{children:[a.jsxs(p,{variant:"ghost",size:"sm","aria-label":"Sort recordings",className:"h-7 gap-2 text-xs text-muted-foreground",children:[m==="name"?"Name":"Last created",a.jsx(w0,{className:"size-3"})]}),a.jsx(g.Popover,{children:a.jsxs(g.Menu,{"aria-label":"Sort recordings",children:[a.jsx(g.Item,{id:"recent",onAction:()=>i("recent"),children:"Last created"}),a.jsx(g.Item,{id:"name",onAction:()=>i("name"),children:"Name"})]})})]})})]}),O&&a.jsxs("div",{className:"flex items-center gap-3 px-7 pb-5 text-xs lg:px-10",children:[a.jsx(p,{variant:"ghost",size:"sm",onPress:()=>F(W.map(n=>n.share_code)),children:"Select all"}),a.jsxs("span",{children:[D.length," selected"]}),a.jsx(p,{variant:"danger",size:"sm",isDisabled:!D.length||_,onPress:()=>N(D),children:"Delete"}),a.jsx(p,{variant:"ghost",size:"sm",onPress:()=>{q(!1),F([])},children:"Cancel"})]})]}),a.jsx("main",{className:"custom-scrollbar min-h-0 flex-1 overflow-y-auto px-7 pb-10 lg:px-10",children:t==="settings"?a.jsxs("div",{className:"space-y-5 py-10",children:[a.jsx("h1",{className:"text-lg font-semibold",children:"Library settings"}),a.jsx("p",{className:"text-sm text-muted",children:"Folders are saved in this browser. Recordings and share links are stored by your sharing service."}),a.jsx(p,{variant:"secondary",onPress:()=>U(!0),children:"How to share a recording"})]}):a.jsxs(a.Fragment,{children:[M&&a.jsx(v0,{children:M}),a.jsx("ul",{"aria-label":"Your recordings",className:"grid grid-cols-[repeat(auto-fill,minmax(min(100%,280px),1fr))] gap-x-7 gap-y-10 lg:gap-x-9",children:H?[0,1,2].map(n=>a.jsx("li",{children:a.jsx(o1,{className:"aspect-[4/3] w-full rounded-xl"})},n)):W.map(n=>a.jsx(U1,{video:n,folders:y,assignFolder:R0,selecting:O,selected:D.includes(n.share_code),toggleSelected:()=>F(o=>o.includes(n.share_code)?o.filter(x=>x!==n.share_code):[...o,n.share_code]),busy:_,copy:()=>{P0(n.share_code)},renew:()=>{C0(n.share_code)},remove:()=>N([n.share_code])},n.share_code))}),!H&&!M&&!W.length&&a.jsxs("div",{className:"flex h-64 flex-col items-center justify-center gap-3 text-sm text-muted-foreground",children:[a.jsx(Q,{weight:"fill",className:"size-8 opacity-40"}),a.jsx("p",{children:h.length?"No recordings match this view.":"No shared recordings yet."}),a.jsx(p,{variant:"secondary",onPress:()=>{s(""),d("all"),l("shared")},children:"Show all recordings"})]})]})})]})]}),a.jsx(b,{isOpen:k0,onOpenChange:U,children:a.jsx(b.Backdrop,{children:a.jsx(b.Container,{children:a.jsxs(b.Dialog,{children:[a.jsx(b.Header,{children:a.jsx(b.Heading,{children:"Share a recording"})}),a.jsx(b.Body,{children:a.jsx("p",{children:"Open a project in Recordly, choose Export, then Create share link. Your shared recording will appear here."})}),a.jsx(b.Footer,{children:a.jsx(p,{onPress:()=>U(!1),children:"Got it"})})]})})})}),a.jsx(b,{isOpen:R!==null,onOpenChange:n=>{!n&&!_&&N(null)},children:a.jsx(b.Backdrop,{children:a.jsx(b.Container,{children:a.jsxs(b.Dialog,{children:[a.jsx(b.Header,{children:a.jsxs(b.Heading,{children:["Delete"," ",R?.length===1?"recording":"recordings","?"]})}),a.jsx(b.Body,{children:a.jsxs("p",{children:["This permanently removes ",R?.length," recording(s) and their share links."]})}),a.jsxs(b.Footer,{children:[a.jsx(p,{variant:"secondary",isDisabled:_,onPress:()=>N(null),children:"Cancel"}),a.jsx(p,{variant:"danger",isDisabled:_,isPending:d0,onPress:async()=>{m0(!0);try{const n=[];for(const o of R||[])await S0(o)||n.push(o);N(n.length?n:null),F(n),q(n.length>0)}finally{m0(!1)}},children:"Delete"})]})]})})})})]})}export{Y1 as LibraryLogin,G1 as default}; diff --git a/services/recordly-share/worker/web/dist/_astro/SharePage.Wj6U9NJ6.js b/services/recordly-share/worker/web/dist/_astro/SharePage.Wj6U9NJ6.js new file mode 100644 index 000000000..34aabd4a6 --- /dev/null +++ b/services/recordly-share/worker/web/dist/_astro/SharePage.Wj6U9NJ6.js @@ -0,0 +1 @@ +import{t as ce,Z as ta,_ as aa,a0 as na,B as r,F as Q,G as O,a1 as ze,a2 as Ge,a3 as ra,a4 as Je,d as Fe,a5 as la,a6 as pt,a7 as _e,a8 as Be,a9 as sa,j as k,aa as ia,ab as oa,A as Ve,ac as ca,ad as he,ae as gt,s as ge,af as da,ag as vt,o as ee,q as te,y as ve,ah as xt,p as J,ai as $t,aj as Pe,ak as xe,al as ua,am as ma,an as fa,u as ha,ao as ba,ap as pa,aq as yt,ar as ga,as as At,at as va,au as xa,m as Ue,v as Et,av as $a,aw as ya,ax as Aa,n as Mt,ay as Qe,az as Vt,aA as Ea,aB as Ma,aC as Va,aD as Pa,$ as wa,aE as Za,aF as ja,aG as La,aH as Sa,aI as Ha,aJ as Ta,aK as Ca,aL as Fa,E as W,aM as ka,l as Na,aN as ue,aO as Ra,aP as Da,aQ as Ka,aR as Ia,aS as Oa,aT as za,aU as _a,aV as Ba,aW as et,aX as Ua,J as R,Y as ie,P as Pt,K as T,aY as wt,aZ as Wa,M as be,X as Ae,L as Ee,a_ as qa,a$ as Xa,Q as Zt,W as ke,O as jt,N as Ya,R as $e,U as Ga,V as Ja,T as Qa,b0 as e0}from"./ShareUI.CpnBv7vU.js";import{r as e,$ as P}from"./index.DeQQz02V.js";const t0=ce({defaultVariants:{color:"default",variant:"secondary"},slots:{base:"chip",label:"chip__label"},variants:{color:{accent:{base:"chip--accent"},danger:{base:"chip--danger"},default:{base:"chip--default"},success:{base:"chip--success"},warning:{base:"chip--warning"}},size:{lg:{base:"chip--lg"},md:{base:"chip--md"},sm:{base:"chip--sm"}},variant:{primary:{base:"chip--primary"},secondary:{base:"chip--secondary"},soft:{base:"chip--soft"},tertiary:{base:"chip--tertiary"}}}}),a0=ce({defaultVariants:{color:"accent",size:"md"},slots:{base:"progress-bar",fill:"progress-bar__fill",output:"progress-bar__output",track:"progress-bar__track"},variants:{color:{accent:{base:"progress-bar--accent"},danger:{base:"progress-bar--danger"},default:{base:"progress-bar--default"},success:{base:"progress-bar--success"},warning:{base:"progress-bar--warning"}},size:{lg:{base:"progress-bar--lg"},md:{base:"progress-bar--md"},sm:{base:"progress-bar--sm"}}}}),n0=ce({defaultVariants:{hideScrollBar:!1,orientation:"vertical",variant:"fade"},slots:{base:"scroll-shadow"},variants:{hideScrollBar:{false:{},true:{base:"scroll-shadow--hide-scrollbar"}},orientation:{horizontal:{base:"scroll-shadow--horizontal"},vertical:{base:"scroll-shadow--vertical"}},variant:{fade:{base:"scroll-shadow--fade"}}}}),r0=ce({slots:{base:"slider",fill:"slider__fill",marks:"slider__marks",output:"slider__output",thumb:"slider__thumb",track:"slider__track"}}),l0=ce({defaultVariants:{align:"center",variant:"primary"},slots:{base:"tabs",scrollNext:"tabs__list-container__scroll-next",scrollPrev:"tabs__list-container__scroll-prev",scroller:"tabs__list-container__scroller",separator:"tabs__separator",tab:"tabs__tab",tabIndicator:"tabs__indicator",tabList:"tabs__list",tabListContainer:"tabs__list-container",tabPanel:"tabs__panel"},variants:{align:{center:{},end:{base:"tabs--align-end"},start:{base:"tabs--align-start"}},variant:{primary:{},secondary:{base:"tabs--secondary"}}}}),s0=ce({base:"textarea",defaultVariants:{fullWidth:!1,variant:"primary"},variants:{fullWidth:{false:"",true:"textarea--full-width"},variant:{primary:"textarea--primary",secondary:"textarea--secondary"}}}),i0=ce({slots:{base:"tooltip",trigger:"tooltip__trigger"}});class tt{constructor(t){this.keyMap=new Map,this.firstKey=null,this.lastKey=null,this.iterable=t;let n=o=>{if(this.keyMap.set(o.key,o),o.childNodes&&o.type==="section")for(let d of o.childNodes)n(d)};for(let o of t)n(o);let l=null,s=0,i=0;for(let[o,d]of this.keyMap)l?(l.nextKey=o,d.prevKey=l.key):(this.firstKey=o,d.prevKey=void 0),d.type==="item"&&(d.index=s++),(d.type==="section"||d.type==="item")&&i++,l=d,l.nextKey=void 0;this._size=i,this.lastKey=l?.key??null}*[Symbol.iterator](){yield*this.iterable}get size(){return this._size}getKeys(){return this.keyMap.keys()}getKeyBefore(t){let n=this.keyMap.get(t);return n?n.prevKey??null:null}getKeyAfter(t){let n=this.keyMap.get(t);return n?n.nextKey??null:null}getFirstKey(){return this.firstKey}getLastKey(){return this.lastKey}getItem(t){return this.keyMap.get(t)??null}at(t){const n=[...this.getKeys()];return this.getItem(n[t])}getChildren(t){return this.keyMap.get(t)?.childNodes||[]}}function o0(a){let{filter:t,layoutDelegate:n}=a,l=ta(a),s=e.useMemo(()=>a.disabledKeys?new Set(a.disabledKeys):new Set,[a.disabledKeys]),i=e.useCallback(u=>t?new tt(t(u)):new tt(u),[t]),o=e.useMemo(()=>({suppressTextValueWarning:a.suppressTextValueWarning}),[a.suppressTextValueWarning]),d=aa(a,i,o),c=e.useMemo(()=>new na(d,l,{layoutDelegate:n}),[d,l,n]);return c0(d,c),{collection:d,disabledKeys:s,selectionManager:c}}function c0(a,t){const n=e.useRef(null);e.useEffect(()=>{if(t.focusedKey!=null&&!a.getItem(t.focusedKey)&&n.current){let l=n.current.getKeyAfter(t.focusedKey),s=null;for(;l!=null;){let i=a.getItem(l);if(i&&i.type==="item"&&!t.isDisabled(l)){s=l;break}l=n.current.getKeyAfter(l)}if(s==null)for(l=n.current.getKeyBefore(t.focusedKey);l!=null;){let i=a.getItem(l);if(i&&i.type==="item"&&!t.isDisabled(l)){s=l;break}l=n.current.getKeyBefore(l)}t.setFocusedKey(s)}n.current=a},[a,t])}const Lt=e.createContext({}),at=({children:a,className:t,color:n,size:l,variant:s,...i})=>{const o=P.useMemo(()=>t0({color:n,size:l,variant:s}),[n,l,s]),d=P.useMemo(()=>typeof a=="string"||typeof a=="number"?r.jsx(St,{children:a}):a,[a]),c=P.useMemo(()=>({slots:o}),[o]);return r.jsx(Lt,{value:c,children:r.jsx(Q.span,{...i,className:O(o.base,t),"data-slot":"chip",children:d})})},St=({children:a,className:t,...n})=>{const{slots:l}=e.use(Lt);return r.jsx(Q.span,{className:O(l?.label,t),"data-slot":"chip-label",...n,children:a})},d0=Object.assign(at,{Root:at,Label:St});function Ht(a){let{onMoveStart:t,onMove:n,onMoveEnd:l}=a,s=e.useRef({didMove:!1,lastPosition:null,id:null}),{addGlobalListener:i,removeGlobalListener:o}=ze(),d=e.useCallback((g,p,A,b)=>{A===0&&b===0||(s.current.didMove||(s.current.didMove=!0,t?.({type:"movestart",pointerType:p,shiftKey:g.shiftKey,metaKey:g.metaKey,ctrlKey:g.ctrlKey,altKey:g.altKey})),n?.({type:"move",pointerType:p,deltaX:A,deltaY:b,shiftKey:g.shiftKey,metaKey:g.metaKey,ctrlKey:g.ctrlKey,altKey:g.altKey}))},[t,n,s]),c=Ge(d),u=e.useCallback((g,p)=>{ra(),s.current.didMove&&l?.({type:"moveend",pointerType:p,shiftKey:g.shiftKey,metaKey:g.metaKey,ctrlKey:g.ctrlKey,altKey:g.altKey})},[l,s]),v=Ge(u);return{moveProps:e.useMemo(()=>{let g={},p=()=>{la(),s.current.didMove=!1};{let b=x=>{if(x.pointerId===s.current.id){let V=x.pointerType||"mouse";c(x,V,x.pageX-(s.current.lastPosition?.pageX??0),x.pageY-(s.current.lastPosition?.pageY??0)),s.current.lastPosition={pageX:x.pageX,pageY:x.pageY}}},h=x=>{if(x.pointerId===s.current.id){let V=x.pointerType||"mouse";v(x,V),s.current.id=null;let Z=Je(Fe(x));o(Z,"pointermove",b,!1),o(Z,"pointerup",h,!1),o(Z,"pointercancel",h,!1)}};g.onPointerDown=x=>{if(x.button===0&&s.current.id==null){p(),x.stopPropagation(),x.preventDefault(),s.current.lastPosition={pageX:x.pageX,pageY:x.pageY},s.current.id=x.pointerId;let V=Je(Fe(x));i(V,"pointermove",b,!1),i(V,"pointerup",h,!1),i(V,"pointercancel",h,!1)}}}let A=(b,h,x)=>{p(),c(b,"keyboard",h,x),v(b,"keyboard")};return g.onKeyDown=b=>{switch(b.key){case"Left":case"ArrowLeft":b.preventDefault(),b.stopPropagation(),A(b,-1,0);break;case"Right":case"ArrowRight":b.preventDefault(),b.stopPropagation(),A(b,1,0);break;case"Up":case"ArrowUp":b.preventDefault(),b.stopPropagation(),A(b,0,-1);break;case"Down":case"ArrowDown":b.preventDefault(),b.stopPropagation(),A(b,0,1);break}},g},[i,o,s])}}const We=new WeakMap;function Ne(a,t){let n=We.get(a);if(!n)throw new Error("Unknown slider state");return`${n.id}-${t}`}function u0(a,t,n){let{labelProps:l,fieldProps:s}=pt(a),i=a.orientation==="vertical";We.set(t,{id:l.id??s.id,"aria-describedby":a["aria-describedby"],"aria-details":a["aria-details"]});let{direction:o}=_e(),{addGlobalListener:d,removeGlobalListener:c}=ze();const u=e.useRef(null),v=o==="rtl",m=e.useRef(null),{moveProps:g}=Ht({onMoveStart(){m.current=null},onMove({deltaX:h,deltaY:x}){if(!n.current)return;let{height:V,width:Z}=n.current.getBoundingClientRect(),L=i?V:Z;m.current==null&&u.current!=null&&(m.current=t.getThumbPercent(u.current)*L);let E=i?x:h;if((i||v)&&(E=-E),m.current+=E,u.current!=null&&n.current){const j=Be(m.current/L,0,1);t.setThumbPercent(u.current,j)}},onMoveEnd(){u.current!=null&&(t.setThumbDragging(u.current,!1),u.current=null)}});let p=e.useRef(void 0),A=(h,x,V,Z)=>{if(n.current&&!a.isDisabled&&t.values.every((L,E)=>!t.isThumbDragging(E))){let{height:L,width:E,top:j,left:C}=n.current.getBoundingClientRect(),I=i?L:E,D=((i?Z:V)-(i?j:C))/I;(o==="rtl"||i)&&(D=1-D);let K=t.getPercentValue(D),$,S=t.values.findIndex(H=>K-H<0);if(S===0)$=S;else if(S===-1)$=t.values.length-1;else{let H=t.values[S-1],w=t.values[S];Math.abs(H-K)=0&&t.isThumbEditable($)?(h.preventDefault(),u.current=$,t.setFocusedThumb($),p.current=x,t.setThumbDragging(u.current,!0),t.setThumbValue($,K),d(window,"mouseup",b,!1),d(window,"touchend",b,!1),d(window,"pointerup",b,!1)):u.current=null}},b=h=>{(h.pointerId??h.changedTouches?.[0].identifier)===p.current&&(u.current!=null&&(t.setThumbDragging(u.current,!1),u.current=null),c(window,"mouseup",b,!1),c(window,"touchend",b,!1),c(window,"pointerup",b,!1))};return"htmlFor"in l&&l.htmlFor&&(delete l.htmlFor,l.onClick=()=>{document.getElementById(Ne(t,0))?.focus(),sa("keyboard")}),{labelProps:l,groupProps:{role:"group",...s},trackProps:k({onMouseDown(h){h.button!==0||h.altKey||h.ctrlKey||h.metaKey||A(h,void 0,h.clientX,h.clientY)},onPointerDown(h){h.pointerType==="mouse"&&(h.button!==0||h.altKey||h.ctrlKey||h.metaKey)||A(h,h.pointerId,h.clientX,h.clientY)},onTouchStart(h){A(h,h.changedTouches[0].identifier,h.changedTouches[0].clientX,h.changedTouches[0].clientY)},style:{position:"relative",touchAction:"none"}},g),outputProps:{htmlFor:t.values.map((h,x)=>Ne(t,x)).join(" "),"aria-live":"off"}}}function m0(a,t){let{index:n=0,isRequired:l,validationState:s,isInvalid:i,trackRef:o,inputRef:d,orientation:c=t.orientation,name:u,form:v}=a,m=a.isDisabled||t.isDisabled,g=c==="vertical",{direction:p}=_e(),{addGlobalListener:A,removeGlobalListener:b}=ze(),h=We.get(t);const{labelProps:x,fieldProps:V}=pt({...a,id:Ne(t,n),"aria-labelledby":`${h.id} ${a["aria-labelledby"]??""}`.trim()}),Z=t.values[n],L=e.useCallback(()=>{d.current&&ia(d.current)},[d]),E=t.focusedThumb===n;e.useEffect(()=>{E&&L()},[E,L]);let j=p==="rtl",C=e.useRef(null),I=w=>{t.setThumbDragging(n,!0),w(),t.setThumbDragging(n,!1)},{keyboardProps:q}=oa({shortcuts:{PageUp:()=>I(()=>{t.incrementThumb(n,t.pageSize)}),PageDown:()=>I(()=>{t.decrementThumb(n,t.pageSize)}),Home:()=>I(()=>{t.setThumbValue(n,t.getThumbMinValue(n))}),End:()=>I(()=>{t.setThumbValue(n,t.getThumbMaxValue(n))})},allowRepeats:!0}),{moveProps:Y}=Ht({onMoveStart(){C.current=null,t.setThumbDragging(n,!0)},onMove({deltaX:w,deltaY:_,pointerType:ae,shiftKey:B}){const{getThumbPercent:ne,setThumbPercent:re,decrementThumb:y,incrementThumb:f,step:M,pageSize:F}=t;if(!o.current)return;let{width:X,height:Se}=o.current.getBoundingClientRect(),Ye=g?Se:X;if(C.current==null&&(C.current=ne(n)*Ye),ae==="keyboard")w>0&&j||w<0&&!j||_>0?y(n,B?F:M):f(n,B?F:M);else{let He=g?_:w;(g||j)&&(He=-He),C.current+=He,re(n,Be(C.current/Ye,0,1))}},onMoveEnd(){t.setThumbDragging(n,!1)}});t.setThumbEditable(n,!m);const{focusableProps:z}=Ve(k(a,{onFocus:()=>t.setFocusedThumb(n),onBlur:()=>t.setFocusedThumb(void 0)}),d);let D=e.useRef(void 0),K=w=>{L(),D.current=w,t.setThumbDragging(n,!0),A(window,"mouseup",$,!1),A(window,"touchend",$,!1),A(window,"pointerup",$,!1)},$=w=>{(w.pointerId??w.changedTouches?.[0].identifier)===D.current&&(L(),t.setThumbDragging(n,!1),b(window,"mouseup",$,!1),b(window,"touchend",$,!1),b(window,"pointerup",$,!1))},S=t.getThumbPercent(n);(g||p==="rtl")&&(S=1-S);let H=m?{}:k(q,Y,{onMouseDown:w=>{w.button!==0||w.altKey||w.ctrlKey||w.metaKey||K()},onPointerDown:w=>{w.button!==0||w.altKey||w.ctrlKey||w.metaKey||K(w.pointerId)},onTouchStart:w=>{K(w.changedTouches[0].identifier)}});return ca(d,t.defaultValues[n],w=>{t.setThumbValue(n,w)}),{inputProps:k(z,V,{type:"range",tabIndex:m?void 0:0,min:t.getThumbMinValue(n),max:t.getThumbMaxValue(n),step:t.step,value:Z,name:u,form:v,disabled:m,"aria-orientation":c,"aria-valuetext":t.getThumbValueLabel(n),"aria-required":l||void 0,"aria-invalid":i||s==="invalid"||void 0,"aria-errormessage":a["aria-errormessage"],"aria-describedby":[h["aria-describedby"],a["aria-describedby"]].filter(Boolean).join(" "),"aria-details":[h["aria-details"],a["aria-details"]].filter(Boolean).join(" "),onChange:w=>{t.setThumbValue(n,parseFloat(Fe(w).value))}}),thumbProps:{...H,style:{position:"absolute",[g?"top":"left"]:`${S*100}%`,transform:"translate(-50%, -50%)",touchAction:"none"}},labelProps:x,isDragging:t.isThumbDragging(n),isDisabled:m,isFocused:E}}const f0=0,h0=100,b0=1;function p0(a){const{isDisabled:t=!1,minValue:n=f0,maxValue:l=h0,numberFormatter:s,step:i=b0,orientation:o="horizontal"}=a;let d=e.useMemo(()=>{let y=(l-n)/10;return y=he(y,0,y+i,i),Math.max(y,i)},[i,l,n]),c=e.useCallback(y=>y?.map((f,M)=>{let F=M===0?n:y[M-1],X=M===y.length-1?l:y[M+1];return he(f,F,X,i)}),[n,l,i]),u=e.useMemo(()=>c(rt(a.value)),[a.value,c]),v=e.useMemo(()=>c(rt(a.defaultValue)??[n]),[a.defaultValue,n,c]),m=lt(a.value,a.defaultValue,a.onChange),g=lt(a.value,a.defaultValue,a.onChangeEnd);const[p,A]=gt(u,v,m);let[b]=e.useState(p);const[h,x]=e.useState(new Array(p.length).fill(!1)),V=e.useRef(new Array(p.length).fill(!0)),[Z,L]=e.useState(void 0),E=e.useRef(p),j=e.useRef(h);let C=y=>{E.current=y,A(y)},I=y=>{j.current=y,x(y)};function q(y){return(y-n)/(l-n)}function Y(y){return y===0?n:p[y-1]}function z(y){return y===p.length-1?l:p[y+1]}function D(y){return V.current[y]}function K(y,f){V.current[y]=f}function $(y,f){if(t||!D(y))return;const M=Y(y),F=z(y);f=he(f,M,F,i);let X=nt(E.current,y,f);C(X)}function S(y,f){if(t||!D(y))return;f&&(E.current=p);const M=j.current[y];j.current=nt(j.current,y,f),I(j.current),g&&M&&!j.current.some(Boolean)&&g(E.current)}let H=e.useRef(null);function w(y=p){switch(typeof y=="number"&&(y=[y]),y.length){case 0:return"";case 1:return s.format(y[0]);case 2:return s.formatRange(y[0],y[1]);default:{let f=y.map(F=>s.format(F)),M=H.current;return(!M||M.resolvedOptions().locale!==s.resolvedOptions().locale)&&(M=H.current=new Intl.ListFormat(s.resolvedOptions().locale,{type:"unit"})),M.format(f)}}}function _(y,f){$(y,B(f))}function ae(y){return Math.round((y-n)/i)*i+n}function B(y){const f=y*(l-n)+n;return Be(ae(f),n,l)}function ne(y,f=1){let M=Math.max(f,i);$(y,he(p[y]+M,n,l,i))}function re(y,f=1){let M=Math.max(f,i);$(y,he(p[y]-M,n,l,i))}return{values:p,defaultValues:a.defaultValue!==void 0?v:b,getThumbValue:y=>p[y],setThumbValue:$,setThumbPercent:_,isThumbDragging:y=>h[y],setThumbDragging:S,focusedThumb:Z,setFocusedThumb:L,getThumbPercent:y=>q(p[y]),getValuePercent:q,getThumbValueLabel:y=>w(p[y]),getFormattedValue:w,getThumbMinValue:Y,getThumbMaxValue:z,getPercentValue:B,isThumbEditable:D,setThumbEditable:K,incrementThumb:ne,decrementThumb:re,step:i,pageSize:d,orientation:o,isDisabled:t}}function nt(a,t,n){return a[t]===n?a:[...a.slice(0,t),n,...a.slice(t+1)]}function rt(a){if(a!=null)return Array.isArray(a)?a:[a]}function lt(a,t,n){return l=>{n?.(typeof a=="number"||typeof t=="number"?l[0]:l)}}const g0=e.createContext(null),we=e.createContext(null),qe=e.createContext(null),Tt=e.createContext(null),v0=e.forwardRef(function(t,n){[t,n]=ge(t,n,g0);let l=e.useRef(null),s=da(t.formatOptions),i=p0({...t,numberFormatter:s}),[o,d]=vt(!t["aria-label"]&&!t["aria-labelledby"]),{groupProps:c,trackProps:u,labelProps:v,outputProps:m}=u0({...t,label:d},i,l),g=ee({...t,values:{orientation:i.orientation,isDisabled:i.isDisabled,state:i},defaultClassName:"react-aria-Slider"}),p=te(t,{global:!0});return delete p.id,P.createElement(ve,{values:[[we,i],[qe,{...u,ref:l}],[Tt,m],[xt,{...v,ref:o}]]},P.createElement(J.div,{...k(p,g,c),ref:n,slot:t.slot||void 0,"data-orientation":i.orientation,"data-disabled":i.isDisabled||void 0}))}),x0=e.forwardRef(function(t,n){[t,n]=ge(t,n,Tt);let{children:l,style:s,className:i,render:o,...d}=t,c=e.useContext(we),u=ee({className:i,style:s,children:l,render:o,defaultChildren:c.getFormattedValue(),defaultClassName:"react-aria-SliderOutput",values:{orientation:c.orientation,isDisabled:c.isDisabled,state:c}});return P.createElement(J.output,{...d,...u,ref:n,"data-orientation":c.orientation||void 0,"data-disabled":c.isDisabled||void 0})}),$0=e.forwardRef(function(t,n){[t,n]=ge(t,n,qe);let l=e.useContext(we),{onHoverStart:s,onHoverEnd:i,onHoverChange:o,...d}=t,{hoverProps:c,isHovered:u}=xe({onHoverStart:s,onHoverEnd:i,onHoverChange:o}),v=ee({...t,defaultClassName:"react-aria-SliderTrack",values:{orientation:l.orientation,isDisabled:l.isDisabled,isHovered:u,state:l}});return P.createElement(J.div,{...k(d,c),...v,ref:n,"data-hovered":u||void 0,"data-orientation":l.orientation||void 0,"data-disabled":l.isDisabled||void 0})}),y0=e.forwardRef(function(t,n){let{inputRef:l=null}=t,s=e.useContext(we),{ref:i}=$t(qe),{index:o=0}=t,d=e.useRef(null),c=l||d,[u,v]=vt(!t["aria-label"]&&!t["aria-labelledby"]),{thumbProps:m,inputProps:g,labelProps:p,isDragging:A,isFocused:b,isDisabled:h}=m0({...t,index:o,trackRef:i,inputRef:c,label:v},s),{focusProps:x,isFocusVisible:V}=Pe(),{hoverProps:Z,isHovered:L}=xe(t),E=ee({...t,defaultClassName:"react-aria-SliderThumb",values:{state:s,isHovered:L,isDragging:A,isFocused:b,isFocusVisible:V,isDisabled:h}}),j=te(t,{global:!0});return delete j.id,P.createElement(J.div,{...k(j,m,Z),...E,ref:n,style:{...m.style,...E.style},"data-hovered":L||void 0,"data-dragging":A||void 0,"data-focused":b||void 0,"data-focus-visible":V||void 0,"data-disabled":h||void 0},P.createElement(ua,null,P.createElement("input",{ref:c,...k(g,x)})),P.createElement(ve,{values:[[xt,{...p,ref:u}]]},E.children))});function A0(a,t){let n=t?.isDisabled,[l,s]=e.useState(!1);return ma(()=>{if(a?.current&&!n){let i=()=>{if(a.current){let d=fa(a.current,{tabbable:!0});s(!!d.nextNode())}};i();let o=new MutationObserver(i);return o.observe(a.current,{subtree:!0,childList:!0,attributes:!0,attributeFilter:["tabIndex","disabled"]}),()=>{o.disconnect()}}}),n?!1:l}const E0=1500,st=500;let le={},M0=0,de=!1,G=null,se=null;function Ct(a={}){let{delay:t=E0,closeDelay:n=st}=a,{isOpen:l,open:s,close:i}=ha(a),[o,d]=e.useState(!1),c=e.useMemo(()=>`${++M0}`,[]),u=e.useRef(null),v=e.useRef(i),m=()=>{le[c]=A},g=()=>{for(let h in le)h!==c&&(le[h](!0,!0),delete le[h])},p=h=>{u.current&&clearTimeout(u.current),u.current=null,g(),m(),d(!!h),de=!0,s(),G&&(clearTimeout(G),G=null),se&&(clearTimeout(se),se=null)},A=(h,x)=>{d(!!x),h||n<=0?(u.current&&clearTimeout(u.current),u.current=null,v.current()):u.current||(u.current=setTimeout(()=>{u.current=null,v.current()},n)),G&&(clearTimeout(G),G=null),de&&(se&&clearTimeout(se),se=setTimeout(()=>{delete le[c],se=null,de=!1},Math.max(st,n)))},b=()=>{g(),m(),!l&&!de?(G&&clearTimeout(G),G=setTimeout(()=>{G=null,de=!0,p(!1)},t)):l||p(!0)};return e.useEffect(()=>{v.current=i},[i]),e.useEffect(()=>()=>{u.current&&clearTimeout(u.current),le[c]&&delete le[c]},[c]),{isOpen:l,shouldSkipAnimation:o,open:h=>{!h&&t>0&&!u.current?b():p(de)},close:A}}const Ft=new WeakMap;function Me(a,t,n){return a?(typeof t=="string"&&(t=t.replace(/\s+/g,"")),`${Ft.get(a)}-${n}-${t}`):""}function V0(a,t,n){let{key:l,isDisabled:s,shouldSelectOnPressUp:i}=a,{selectionManager:o,selectedKey:d}=t,c=l===d,u=s||t.isDisabled||t.selectionManager.isDisabled(l),v=t.collection.getItem(l),{itemProps:m,isPressed:g}=ba({selectionManager:o,key:l,ref:n,isDisabled:u,shouldSelectOnPressUp:i??v?.props.href!=null,linkBehavior:"selection"}),p=Me(t,l,"tab"),A=Me(t,l,"tabpanel"),{tabIndex:b}=m,h=te(v?.props,{labelable:!0});delete h.id;let x=pa(v?.props),{focusableProps:V}=Ve({...v?.props,isDisabled:u},n);return{tabProps:k(h,V,x,m,{id:p,"aria-selected":c,"aria-disabled":u||void 0,"aria-controls":c?A:void 0,tabIndex:u?void 0:b,role:"tab"}),isSelected:c,isDisabled:u,isPressed:g}}function P0(a,t,n){let l=A0(n)?void 0:0;const s=Me(t,a.id??t?.selectedKey,"tabpanel"),i=yt({...a,id:s,"aria-labelledby":Me(t,t?.selectedKey,"tab")});return{tabPanelProps:k(i,{tabIndex:l,role:"tabpanel","aria-describedby":a["aria-describedby"],"aria-details":a["aria-details"]})}}class w0{constructor(t,n,l,s=new Set){this.collection=t,this.flipDirection=n==="rtl",this.disabledKeys=s,this.tabDirection=l==="horizontal"}getKeyLeftOf(t){return this.flipDirection?this.getNextKey(t):this.getPreviousKey(t)}getKeyRightOf(t){return this.flipDirection?this.getPreviousKey(t):this.getNextKey(t)}isDisabled(t){return this.disabledKeys.has(t)||!!this.collection.getItem(t)?.props?.isDisabled}getFirstKey(){let t=this.collection.getFirstKey();return t!=null&&this.isDisabled(t)&&(t=this.getNextKey(t)),t}getLastKey(){let t=this.collection.getLastKey();return t!=null&&this.isDisabled(t)&&(t=this.getPreviousKey(t)),t}getKeyAbove(t){return this.tabDirection?null:this.getPreviousKey(t)}getKeyBelow(t){return this.tabDirection?null:this.getNextKey(t)}getNextKey(t){let n=t;do n=this.collection.getKeyAfter(n),n==null&&(n=this.collection.getFirstKey());while(n!=null&&this.isDisabled(n)&&n!==t);return n}getPreviousKey(t){let n=t;do n=this.collection.getKeyBefore(n),n==null&&(n=this.collection.getLastKey());while(n!=null&&this.isDisabled(n)&&n!==t);return n}}function Z0(a,t,n){let{orientation:l="horizontal",keyboardActivation:s="automatic"}=a,{collection:i,selectionManager:o,disabledKeys:d}=t,{direction:c}=_e(),u=e.useMemo(()=>new w0(i,c,l,d),[i,d,l,c]),{collectionProps:v}=ga({ref:n,selectionManager:o,keyboardDelegate:u,selectOnFocus:s==="automatic",disallowEmptySelection:!0,scrollRef:n,linkBehavior:"selection"}),m=At();Ft.set(t,m);let g=yt({...a,id:m});return{tabListProps:{...k(v,g),role:"tablist","aria-orientation":l,tabIndex:void 0}}}function j0(a){let[t,n]=gt(a.selectedKey,a.defaultSelectedKey??null,a.onSelectionChange),l=e.useMemo(()=>t!=null?[t]:[],[t]),{collection:s,disabledKeys:i,selectionManager:o}=o0({...a,selectionMode:"single",disallowEmptySelection:!0,allowDuplicateSelectionEvents:!0,selectedKeys:l,onSelectionChange:c=>{if(c==="all")return;let u=c.values().next().value??null;u===t&&a.onSelectionChange&&a.onSelectionChange(u),n(u)}}),d=t!=null?s.getItem(t):null;return{collection:s,disabledKeys:i,selectionManager:o,selectedKey:t,setSelectedKey:n,selectedItem:d}}function L0(a){let t=j0({...a,onSelectionChange:a.onSelectionChange?o=>{o!=null&&a.onSelectionChange?.(o)}:void 0,suppressTextValueWarning:!0,defaultSelectedKey:a.defaultSelectedKey??it(a.collection,a.disabledKeys?new Set(a.disabledKeys):new Set)??void 0}),{selectionManager:n,collection:l,selectedKey:s}=t,i=e.useRef(s);return e.useEffect(()=>{let o=s;a.selectedKey==null&&(n.isEmpty||o==null||!l.getItem(o))&&(o=it(l,t.disabledKeys),o!=null&&n.setSelectedKeys([o])),(o!=null&&n.focusedKey==null||!n.isFocused&&o!==i.current)&&n.setFocusedKey(o),i.current=o}),{...t,isDisabled:a.isDisabled||!1}}function it(a,t){let n=null;if(a){for(n=a.getFirstKey();n!=null&&(t.has(n)||a.getItem(n)?.props?.isDisabled)&&n!==a.getLastKey();)n=a.getKeyAfter(n);n!=null&&(t.has(n)||a.getItem(n)?.props?.isDisabled)&&n===a.getLastKey()&&(n=a.getFirstKey())}return n}const Ze=e.createContext(null),oe=e.createContext(null),S0=e.forwardRef(function(t,n){[t,n]=ge(t,n,Ze);let{children:l,orientation:s="horizontal"}=t;return l=e.useMemo(()=>typeof l=="function"?l({orientation:s,defaultChildren:null}):l,[l,s]),P.createElement(va,{content:l},i=>P.createElement(H0,{props:t,collection:i,tabsRef:n}))});function H0({props:a,tabsRef:t,collection:n}){let{orientation:l="horizontal"}=a,s=L0({...a,collection:n,children:void 0}),{focusProps:i,isFocused:o,isFocusVisible:d}=Pe({within:!0}),c=e.useMemo(()=>({orientation:l,isFocusWithin:o,isFocusVisible:d}),[l,o,d]),u=ee({...a,defaultClassName:"react-aria-Tabs",values:c}),v=te(a,{global:!0});return P.createElement(J.div,{...k(v,u,i),ref:t,slot:a.slot||void 0,"data-focused":o||void 0,"data-orientation":l,"data-focus-visible":d||void 0,"data-disabled":s.isDisabled||void 0},P.createElement(ve,{values:[[Ze,a],[oe,s]]},u.children))}const ot=e.forwardRef(function(t,n){return e.useContext(oe)?P.createElement(T0,{props:t,forwardedRef:n}):P.createElement(Aa,t)});function T0({props:a,forwardedRef:t}){let n=e.useContext(oe),{CollectionRoot:l}=e.useContext(Vt),{orientation:s="horizontal",keyboardActivation:i="automatic"}=$t(Ze),o=Ue(t),{tabListProps:d}=Z0({...a,orientation:s,keyboardActivation:i},n,o),c=ee({...a,children:null,defaultClassName:"react-aria-TabList",values:{orientation:s,state:n}}),u=te(a,{global:!0});return delete u.id,P.createElement(J.div,{...k(u,c,d),ref:o,"data-orientation":s||void 0},P.createElement(Va,null,P.createElement(l,{collection:n.collection,persistedKeys:Pa(n.selectionManager.focusedKey)})))}class C0 extends Ma{static{this.type="item"}}const F0=$a(C0,(a,t,n)=>{let l=e.useContext(oe),s=Ue(t),{tabProps:i,isSelected:o,isDisabled:d,isPressed:c}=V0({key:n.key,...a},l,s),{focusProps:u,isFocused:v,isFocusVisible:m}=Pe(),{hoverProps:g,isHovered:p}=xe({isDisabled:d,onHoverStart:a.onHoverStart,onHoverEnd:a.onHoverEnd,onHoverChange:a.onHoverChange}),A=ee({...a,id:void 0,children:n.rendered,defaultClassName:"react-aria-Tab",values:{isSelected:o,isDisabled:d,isFocused:v,isFocusVisible:m,isPressed:c,isHovered:p}}),b=n.props.href?J.a:J.div,h=te(a,{global:!0});return delete h.id,delete h.onClick,P.createElement(b,{...k(h,A,i,u,g),ref:s,"data-selected":o||void 0,"data-disabled":d||void 0,"data-focused":v||void 0,"data-focus-visible":m||void 0,"data-pressed":c||void 0,"data-hovered":p||void 0},P.createElement(ya.Provider,{value:{isSelected:o}},A.children))}),k0=xa(function(t,n){const l=e.useContext(oe);let s=Ue(n),i=l.selectedKey===t.id,[o,d]=e.useState(l.selectedKey!=null?i:null);o==null&&l.selectedKey!=null?d(i):!i&&o&&d(!1);let c=Et(s,i);return!i&&!t.shouldForceMount&&!c?null:P.createElement(N0,{...t,tabPanelRef:s,isInitiallySelected:o||!1,isExiting:c})});function N0(a){let t=e.useContext(oe),{id:n,tabPanelRef:l,isInitiallySelected:s,isExiting:i,...o}=a,{tabPanelProps:d}=P0(a,t,l),{focusProps:c,isFocused:u,isFocusVisible:v}=Pe(),m=t.selectedKey===a.id,g=Mt(l)&&!s,p=ee({...a,defaultClassName:"react-aria-TabPanel",values:{isFocused:u,isFocusVisible:v,isInert:Qe(!m),isEntering:g,isExiting:i,state:t}}),A=te(o,{global:!0});delete A.id;let b=m?k(A,d,c,p):k(A,p);return P.createElement(J.div,{...b,ref:l,"data-focused":u||void 0,"data-focus-visible":v||void 0,inert:Qe(!m||a.inert),"data-inert":m?void 0:"true","data-entering":g||void 0,"data-exiting":i||void 0},P.createElement(ve,{values:[[Ze,null],[oe,null]]},P.createElement(Vt.Provider,{value:Ea},p.children)))}const Re=P.createContext(null);function R0(a){let{children:t}=a,n=e.useContext(Re),[l,s]=e.useState(0),i=e.useMemo(()=>({parent:n,modalCount:l,addModal(){s(o=>o+1),n&&n.addModal()},removeModal(){s(o=>o-1),n&&n.removeModal()}}),[n,l]);return P.createElement(Re.Provider,{value:i},t)}function D0(){let a=e.useContext(Re);return{modalProviderProps:{"aria-hidden":a&&a.modalCount>0?!0:void 0}}}function K0(a){let{modalProviderProps:t}=D0();return P.createElement("div",{"data-overlay-container":!0,...a,...t})}function I0(a){return P.createElement(R0,null,P.createElement(K0,a))}function O0(a){let t=wa(),{portalContainer:n=t?null:document.body,...l}=a,{getContainer:s}=Za();if(!a.portalContainer&&s&&(n=s()),P.useEffect(()=>{if(n?.closest("[data-overlay-container]"))throw new Error("An OverlayContainer must not be inside another container. Please change the portalContainer prop.")},[n]),!n)return null;let i=P.createElement(I0,l);return ja.createPortal(i,n)}function z0(a,t){let n=te(a,{labelable:!0}),{hoverProps:l}=xe({onHoverStart:()=>t?.open(!0),onHoverEnd:()=>t?.close()});return{tooltipProps:k(n,l,{role:"tooltip"})}}function _0(a,t,n){let{isDisabled:l,trigger:s,shouldCloseOnPress:i=!0}=a,o=At(),d=e.useRef(!1),c=e.useRef(!1),u=()=>{(d.current||c.current)&&t.open(c.current)},v=V=>{!d.current&&!c.current&&t.close(V)};e.useEffect(()=>{let V=Z=>{n&&n.current&&Z.key==="Escape"&&(Z.stopPropagation(),t.close(!0))};if(t.isOpen)return document.addEventListener("keydown",V,!0),()=>{document.removeEventListener("keydown",V,!0)}},[n,t]);let m=()=>{s!=="focus"&&(La()==="pointer"?d.current=!0:d.current=!1,u())},g=()=>{s!=="focus"&&(c.current=!1,d.current=!1,v())},p=()=>{i&&(c.current=!1,d.current=!1,v(!0))},A=()=>{Sa()&&(c.current=!0,u())},b=()=>{c.current=!1,d.current=!1,v(!0)},{hoverProps:h}=xe({isDisabled:l,onHoverStart:m,onHoverEnd:g}),{focusableProps:x}=Ve({isDisabled:l,onFocus:A,onBlur:b},n);return{triggerProps:{"aria-describedby":t.isOpen?o:void 0,...k(x,h,{onPointerDown:p,onKeyDown:p}),tabIndex:void 0},tooltipProps:{id:o}}}const pe=e.createContext(null),kt=e.createContext(null);function B0(a){let t=Ct(a),n=e.useRef(null),{triggerProps:l,tooltipProps:s}=_0(a,t,n);return P.createElement(ve,{values:[[pe,t],[kt,{...s,triggerRef:n}]]},P.createElement(Ha,{...l,ref:n},a.children))}const U0=e.forwardRef(function({UNSTABLE_portalContainer:t,...n},l){[n,l]=ge(n,l,kt);let s=e.useContext(pe),i=Ct(n),o=n.isOpen!=null||n.defaultOpen!=null||!s?i:s,d=Et(l,o.isOpen),c=n.isExiting||!o.shouldSkipAnimation&&d||!1;return!o.isOpen&&!c?null:P.createElement(O0,{portalContainer:t},P.createElement(W0,{...n,tooltipRef:l,isExiting:c}))});function W0(a){let t=e.useContext(pe),n=e.useRef(null),{overlayProps:l,arrowProps:s,placement:i,triggerAnchorPoint:o}=Ta({placement:a.placement||"top",targetRef:a.triggerRef,overlayRef:a.tooltipRef,arrowRef:n,offset:a.offset,crossOffset:a.crossOffset,isOpen:t.isOpen,arrowBoundaryOffset:a.arrowBoundaryOffset,shouldFlip:a.shouldFlip,containerPadding:a.containerPadding,onClose:()=>t.close(!0)}),d=Mt(a.tooltipRef,!!i),c=a.isEntering||!t.shouldSkipAnimation&&d||!1,u=ee({...a,defaultClassName:"react-aria-Tooltip",values:{placement:i,isEntering:c,isExiting:a.isExiting,state:t}});a=k(a,l);let{tooltipProps:v}=z0(a,t),m=te(a,{global:!0});return P.createElement(J.div,{...k(m,u,v),ref:a.tooltipRef,style:{...l.style,"--trigger-anchor-point":o?`${o.x}px ${o.y}px`:void 0,...u.style},"data-placement":i??void 0,"data-entering":c||void 0,"data-exiting":a.isExiting||void 0},P.createElement(Ca.Provider,{value:{...s,placement:i,ref:n}},u.children))}const je=e.createContext({}),De=({children:a,className:t,color:n,size:l,...s})=>{const i=P.useMemo(()=>a0({color:n,size:l}),[n,l]);return r.jsx(Fa,{"data-slot":"progress-bar",...s,className:W(t,i.base()),children:o=>r.jsx(je,{value:{slots:i,state:o},children:typeof a=="function"?a(o):a})})};De.displayName="HeroUI.ProgressBar";const Nt=({children:a,className:t,...n})=>{const{slots:l,state:s}=e.use(je);return r.jsx(Q.span,{className:O(l?.output,t),"data-slot":"progress-bar-output",...n,children:a??s?.valueText})};Nt.displayName="HeroUI.ProgressBar.Output";const Rt=({children:a,className:t,...n})=>{const{slots:l}=e.use(je);return r.jsx(Q.div,{className:O(l?.track,t),"data-slot":"progress-bar-track",...n,children:a})};Rt.displayName="HeroUI.ProgressBar.Track";const Dt=({className:a,style:t,...n})=>{const{slots:l,state:s}=e.use(je);return r.jsx(Q.div,{className:O(l?.fill,a),"data-slot":"progress-bar-fill",style:{...t,width:s?.isIndeterminate?void 0:`${s?.percentage??0}%`},...n})};Dt.displayName="HeroUI.ProgressBar.Fill";const Te=Object.assign(De,{Root:De,Output:Nt,Track:Rt,Fill:Dt}),q0=a=>{const{containerRef:t,isEnabled:n,offset:l,onVisibilityChange:s,orientation:i,visibility:o}=a,d=e.useRef(s);e.useEffect(()=>{d.current=s},[s]);const c=e.useRef(null),u=e.useRef(null),v=e.useCallback(()=>{const m=t.current;if(!m)return;const g=i==="vertical",p=g?m.scrollTop:Math.abs(m.scrollLeft),A=g?m.scrollHeight:m.scrollWidth,b=g?m.clientHeight:m.clientWidth,h=p>l,x=p+b+l{u.current=null;const Z=d.current;g?h&&x?(m.dataset.topBottomScroll="true",delete m.dataset.topScroll,delete m.dataset.bottomScroll,Z?.("both")):(m.dataset.topScroll=String(h),m.dataset.bottomScroll=String(x),delete m.dataset.topBottomScroll,Z&&Z(h?"top":x?"bottom":"none")):h&&x?(m.dataset.leftRightScroll="true",delete m.dataset.leftScroll,delete m.dataset.rightScroll,Z?.("both")):(m.dataset.leftScroll=String(h),m.dataset.rightScroll=String(x),delete m.dataset.leftRightScroll,Z&&Z(h?"left":x?"right":"none"))}))},[t,i,l]);e.useEffect(()=>{const m=t.current;if(!m||!n||o!=="auto")return;v(),m.addEventListener("scroll",v,{passive:!0});const g=new ResizeObserver(v);g.observe(m);const p=new MutationObserver(v);return p.observe(m,{attributeFilter:["class","style"],attributes:!0,characterData:!0,childList:!0,subtree:!0}),()=>{m.removeEventListener("scroll",v),g.disconnect(),p.disconnect(),u.current!==null&&(cancelAnimationFrame(u.current),u.current=null),c.current=null}},[t,o,n,v])},Ke=({children:a,className:t,hideScrollBar:n=!1,isEnabled:l=!0,offset:s=0,onVisibilityChange:i,orientation:o="vertical",ref:d,size:c=40,style:u,variant:v="fade",visibility:m="auto",...g})=>{const p=e.useRef(null);q0({containerRef:p,isEnabled:l,offset:s,onVisibilityChange:i,orientation:o,visibility:m}),ka(()=>{const x=p.current;!x||m==="auto"||(delete x.dataset.topScroll,delete x.dataset.bottomScroll,delete x.dataset.topBottomScroll,delete x.dataset.leftScroll,delete x.dataset.rightScroll,delete x.dataset.leftRightScroll,m==="both"?x.dataset[o==="vertical"?"topBottomScroll":"leftRightScroll"]="true":m!=="none"&&(x.dataset[`${m}Scroll`]="true"))},[m,o]);const A=e.useMemo(()=>n0({hideScrollBar:n,orientation:o,variant:v}),[o,n,v]),b={"--scroll-shadow-size":`${c}px`,"--scroll-shadow-offset":`${s}px`,...u},h=e.useMemo(()=>Na(p,d),[p,d]);return r.jsx("div",{ref:h,className:A.base({className:t}),"data-orientation":o,"data-scroll-shadow-mode":l&&m==="auto"?"auto":"manual","data-scroll-shadow-size":c,"data-slot":"scroll-shadow",style:b,...g,children:a})};Ke.displayName="HeroUI.ScrollShadow";const X0=Object.assign(Ke,{Root:Ke}),me=e.createContext({}),ct=({children:a,className:t,orientation:n="horizontal",...l})=>{const s=P.useMemo(()=>r0({}),[]);return r.jsx(v0,{"data-slot":"slider",orientation:n,...l,className:W(t,s.base()),children:i=>r.jsx(me,{value:{slots:s,state:i},children:typeof a=="function"?a(i):a})})},Y0=({children:a,className:t,...n})=>{const{slots:l}=e.use(me);return r.jsx(x0,{className:W(t,l?.output()),"data-slot":"slider-output",...n,children:a?s=>r.jsx(r.Fragment,{children:typeof a=="function"?a(s):a}):({state:s})=>s.values.map((i,o)=>s.getThumbValueLabel(o)).join(" – ")})},G0=({children:a,className:t,...n})=>{const{slots:l,state:s}=e.use(me),{getThumbPercent:i,values:o}=s?.state||{},d=o?.length&&o.length===1,[c,u]=[o?.length&&o.length>1?i?.(0):0,i?.(o?.length?o.length-1:0)].sort(),v=(u-c)*100;return r.jsx($0,{className:W(t,l?.track()),"data-disabled":ue(s?.isDisabled),"data-slot":"slider-track",...d?{"data-fill-start":ue(v>0),"data-fill-end":ue(v==100)}:{"data-fill-start":ue(c==0),"data-fill-end":ue(c*100+v==100)},...n,children:m=>r.jsx(r.Fragment,{children:typeof a=="function"?a(m):a})})},J0=({className:a,style:t,...n})=>{const{slots:l,state:s}=e.use(me),{getThumbPercent:i,orientation:o,values:d}=s?.state||{},[c,u]=[d?.length&&d.length>1?i?.(0):0,i?.(d?.length?d.length-1:0)].sort(),v=o==="vertical";return r.jsx(Q.div,{className:O(l?.fill,a),"data-disabled":ue(s?.isDisabled),"data-slot":"slider-fill",style:{...t,[v?"bottom":"left"]:`${c*100}%`,...v?{height:`${(u-c)*100}%`}:{width:`${(u-c)*100}%`}},...n})},Q0=({children:a,className:t,...n})=>{const{slots:l}=e.use(me);return r.jsx(y0,{className:W(t,l?.thumb()),"data-slot":"slider-thumb",...n,children:s=>r.jsx(r.Fragment,{children:typeof a=="function"?a(s):a})})},e1=({className:a,...t})=>{const{slots:n}=e.use(me);return r.jsx(Q.div,{className:O(n?.marks,a),"data-slot":"slider-marks",...t})},ye=Object.assign(ct,{Root:ct,Output:Y0,Track:G0,Fill:J0,Thumb:Q0,Marks:e1}),fe=e.createContext({}),Kt=za("tabs.listContainer"),dt=({align:a,children:t,className:n,orientation:l="horizontal",variant:s,...i})=>{const o=P.useMemo(()=>l0({align:a,variant:s}),[a,s]),d=P.useMemo(()=>({orientation:l,slots:o}),[l,o]);return r.jsx(fe,{value:d,children:r.jsx(S0,{...i,className:W(n,o.base()),"data-slot":"tabs",orientation:l,children:t})})},t1=({children:a,className:t,render:n,...l})=>r.jsx(Kt.Injector,{...l,className:t,render:n,children:a}),a1=({children:a,className:t,...n})=>{const{orientation:l="horizontal",slots:s}=e.use(fe),i=e.useRef(null),o=l==="vertical",[d,c]=Kt.useSlot(n),u=e.useCallback(p=>{const A=i.current;if(!A)return;const b=o?A.clientHeight:A.clientWidth,h=o?A.scrollHeight:A.scrollWidth,x=Math.max(0,h-b),V=!o&&getComputedStyle(A).direction==="rtl",Z=p*b*.8*(V?-1:1),L=o?A.scrollTop:A.scrollLeft,E=Math.min(V?0:x,Math.max(V?-x:0,L+Z));E!==L&&A.scrollTo({behavior:"smooth",[o?"top":"left"]:E})},[o]);if(!d)return r.jsx(ot,{...c,className:W(t,s?.tabList()),"data-slot":"tabs-list",children:a});const{className:v,render:m,...g}=d;return r.jsx(ot,{...c,className:W(t,s?.tabList()),"data-slot":"tabs-list",render:p=>{const{children:A,className:b,ref:h,...x}=p;return r.jsxs(Q.div,{className:O(s?.tabListContainer,v),"data-slot":"tabs-list-container",render:m,...g,children:[r.jsx(X0,{ref:i,hideScrollBar:!0,className:O(s?.scroller),orientation:l,size:64,children:r.jsx("div",{...x,ref:h,className:b,children:A})}),r.jsx("button",{"aria-label":o?"Scroll tabs up":"Scroll tabs left",className:O(s?.scrollPrev),tabIndex:-1,type:"button",onClick:()=>u(-1),children:o?r.jsx(Da,{}):r.jsx(Ka,{})}),r.jsx("button",{"aria-label":o?"Scroll tabs down":"Scroll tabs right",className:O(s?.scrollNext),tabIndex:-1,type:"button",onClick:()=>u(1),children:o?r.jsx(Ia,{}):r.jsx(Oa,{})})]})},children:a})},n1=({children:a,className:t,...n})=>{const{slots:l}=e.use(fe);return r.jsx(F0,{...n,className:W(t,l?.tab()),"data-slot":"tabs-tab",children:a})},r1=({className:a,...t})=>{const{slots:n}=e.use(fe);return r.jsx(Ra,{className:O(n?.tabIndicator,a),"data-slot":"tabs-indicator",...t})},l1=({children:a,className:t,...n})=>{const{slots:l}=e.use(fe);return r.jsx(k0,{...n,className:W(t,l?.tabPanel()),"data-slot":"tabs-panel",children:a})},s1=({className:a,...t})=>{const{slots:n}=e.use(fe);return r.jsx(Q.span,{"aria-hidden":"true",className:O(n?.separator,a),"data-slot":"tabs-separator",...t})},U=Object.assign(dt,{Root:dt,ListContainer:t1,List:a1,Tab:n1,Indicator:r1,Separator:s1,Panel:l1}),ut=({className:a,fullWidth:t,variant:n,...l})=>{const s=e.use(_a),i=n??s?.variant;return r.jsx(Ba,{"data-slot":"textarea",className:W(a,s0({fullWidth:t,variant:i})),...l})},i1=Object.assign(ut,{Root:ut}),mt=a=>{if(!a)return;const t=a.trim(),n=parseFloat(t);if(!Number.isNaN(n))return t.endsWith("ms")?n:t.endsWith("s")?n*1e3:n},Xe=e.createContext({}),o1=({children:a,shouldSkipAnimation:t})=>{const n=e.use(pe);return!n||t?a:r.jsx(pe,{value:{...n,shouldSkipAnimation:!1},children:a})},ft=({children:a,closeDelay:t,delay:n,shouldSkipAnimation:l=!1,...s})=>{const i=P.useMemo(()=>i0(),[]),o=et("--tooltip-delay"),d=et("--tooltip-close-delay"),c=n??mt(o),u=t??mt(d),v=P.useMemo(()=>({slots:i}),[i]);return r.jsx(Xe,{value:v,children:r.jsx(B0,{closeDelay:u,"data-slot":"tooltip-root",delay:c,...s,children:r.jsx(o1,{shouldSkipAnimation:l,children:a})})})},c1=({children:a,className:t,offset:n,showArrow:l=!1,...s})=>{const{slots:i}=e.use(Xe),o=n||(l?7:3);return r.jsx(U0,{...s,className:W(t,i?.base()),offset:o,children:a})},d1=({children:a,className:t,...n})=>{const l=r.jsx("svg",{"data-slot":"overlay-arrow",fill:"none",height:"12",viewBox:"0 0 12 12",width:"12",xmlns:"http://www.w3.org/2000/svg",children:r.jsx("path",{d:"M0 0C5.48483 8 6.5 8 12 0Z"})}),s=P.isValidElement(a)?P.cloneElement(a,{"data-slot":"overlay-arrow"}):l;return r.jsx(Ua,{"data-slot":"tooltip-arrow",...n,className:t,children:s})},u1=({children:a,className:t,...n})=>{const{slots:l}=e.use(Xe),s=e.useRef(null),{focusableProps:i}=Ve({},s);return r.jsx(Q.div,{ref:s,className:O(l?.trigger,t),"data-slot":"tooltip-trigger",role:"button",...k(i,n),children:a})},ht=Object.assign(ft,{Root:ft,Trigger:u1,Content:c1,Arrow:d1}),m1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M244,56v48a12,12,0,0,1-12,12H184a12,12,0,1,1,0-24H201.1l-19-17.38c-.13-.12-.26-.24-.38-.37A76,76,0,1,0,127,204h1a75.53,75.53,0,0,0,52.15-20.72,12,12,0,0,1,16.49,17.45A99.45,99.45,0,0,1,128,228h-1.37A100,100,0,1,1,198.51,57.06L220,76.72V56a12,12,0,0,1,24,0Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,128a88,88,0,1,1-88-88A88,88,0,0,1,216,128Z",opacity:"0.2"}),e.createElement("path",{d:"M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1-5.66-13.66l17-17-10.55-9.65-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,1,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60l10.93,10L226.34,50.3A8,8,0,0,1,240,56Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M238,56v48a6,6,0,0,1-6,6H184a6,6,0,0,1,0-12h32.55l-30.38-27.8c-.06-.06-.12-.13-.19-.19a82,82,0,1,0-1.7,117.65,6,6,0,0,1,8.24,8.73A93.46,93.46,0,0,1,128,222h-1.28A94,94,0,1,1,194.37,61.4L226,90.35V56a6,6,0,1,1,12,0Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M236,56v48a4,4,0,0,1-4,4H184a4,4,0,0,1,0-8h37.7L187.53,68.69l-.13-.12a84,84,0,1,0-1.75,120.51,4,4,0,0,1,5.5,5.82A91.43,91.43,0,0,1,128,220h-1.26A92,92,0,1,1,193,62.84l35,32.05V56a4,4,0,1,1,8,0Z"}))]]),f1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M228,128a100,100,0,0,1-98.66,100H128a99.39,99.39,0,0,1-68.62-27.29,12,12,0,0,1,16.48-17.45,76,76,0,1,0-1.57-109c-.13.13-.25.25-.39.37L54.89,92H72a12,12,0,0,1,0,24H24a12,12,0,0,1-12-12V56a12,12,0,0,1,24,0V76.72L57.48,57.06A100,100,0,0,1,228,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,128a88,88,0,1,1-88-88A88,88,0,0,1,216,128Z",opacity:"0.2"}),e.createElement("path",{d:"M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L60.63,81.29l17,17A8,8,0,0,1,72,112H24a8,8,0,0,1-8-8V56A8,8,0,0,1,29.66,50.3L49.31,70,60.25,60A96,96,0,0,1,224,128Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M222,128a94,94,0,0,1-92.74,94H128a93.43,93.43,0,0,1-64.5-25.65,6,6,0,1,1,8.24-8.72A82,82,0,1,0,70,70l-.19.19L39.44,98H72a6,6,0,0,1,0,12H24a6,6,0,0,1-6-6V56a6,6,0,0,1,12,0V90.34L61.63,61.4A94,94,0,0,1,222,128Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M220,128a92,92,0,0,1-90.77,92H128a91.47,91.47,0,0,1-63.13-25.1,4,4,0,1,1,5.5-5.82A84,84,0,1,0,68.6,68.57l-.13.12L34.3,100H72a4,4,0,0,1,0,8H24a4,4,0,0,1-4-4V56a4,4,0,0,1,8,0V94.89l35-32A92,92,0,0,1,220,128Z"}))]]),h1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M220,48V96a12,12,0,0,1-24,0V77l-35.51,35.52a12,12,0,0,1-17-17L179,60H160a12,12,0,0,1,0-24h48A12,12,0,0,1,220,48ZM95.51,143.51,60,179V160a12,12,0,0,0-24,0v48a12,12,0,0,0,12,12H96a12,12,0,0,0,0-24H77l35.52-35.51a12,12,0,0,0-17-17ZM208,148a12,12,0,0,0-12,12v19l-35.51-35.52a12,12,0,0,0-17,17L179,196H160a12,12,0,0,0,0,24h48a12,12,0,0,0,12-12V160A12,12,0,0,0,208,148ZM77,60H96a12,12,0,0,0,0-24H48A12,12,0,0,0,36,48V96a12,12,0,0,0,24,0V77l35.51,35.52a12,12,0,0,0,17-17Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,48V208H48V48Z",opacity:"0.2"}),e.createElement("path",{d:"M216,48V96a8,8,0,0,1-16,0V67.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L188.69,56H160a8,8,0,0,1,0-16h48A8,8,0,0,1,216,48ZM98.34,146.34,56,188.69V160a8,8,0,0,0-16,0v48a8,8,0,0,0,8,8H96a8,8,0,0,0,0-16H67.31l42.35-42.34a8,8,0,0,0-11.32-11.32ZM208,152a8,8,0,0,0-8,8v28.69l-42.34-42.35a8,8,0,0,0-11.32,11.32L188.69,200H160a8,8,0,0,0,0,16h48a8,8,0,0,0,8-8V160A8,8,0,0,0,208,152ZM67.31,56H96a8,8,0,0,0,0-16H48a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31l42.34,42.35a8,8,0,0,0,11.32-11.32Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M109.66,146.34a8,8,0,0,1,0,11.32L83.31,184l18.35,18.34A8,8,0,0,1,96,216H48a8,8,0,0,1-8-8V160a8,8,0,0,1,13.66-5.66L72,172.69l26.34-26.35A8,8,0,0,1,109.66,146.34ZM83.31,72l18.35-18.34A8,8,0,0,0,96,40H48a8,8,0,0,0-8,8V96a8,8,0,0,0,13.66,5.66L72,83.31l26.34,26.35a8,8,0,0,0,11.32-11.32ZM208,40H160a8,8,0,0,0-5.66,13.66L172.69,72,146.34,98.34a8,8,0,0,0,11.32,11.32L184,83.31l18.34,18.35A8,8,0,0,0,216,96V48A8,8,0,0,0,208,40Zm3.06,112.61a8,8,0,0,0-8.72,1.73L184,172.69l-26.34-26.35a8,8,0,0,0-11.32,11.32L172.69,184l-18.35,18.34A8,8,0,0,0,160,216h48a8,8,0,0,0,8-8V160A8,8,0,0,0,211.06,152.61Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M214,48V96a6,6,0,0,1-12,0V62.48l-45.76,45.76a6,6,0,0,1-8.48-8.48L193.52,54H160a6,6,0,0,1,0-12h48A6,6,0,0,1,214,48ZM99.76,147.76,54,193.52V160a6,6,0,0,0-12,0v48a6,6,0,0,0,6,6H96a6,6,0,0,0,0-12H62.48l45.76-45.76a6,6,0,0,0-8.48-8.48ZM208,154a6,6,0,0,0-6,6v33.52l-45.76-45.76a6,6,0,0,0-8.48,8.48L193.52,202H160a6,6,0,0,0,0,12h48a6,6,0,0,0,6-6V160A6,6,0,0,0,208,154ZM62.48,54H96a6,6,0,0,0,0-12H48a6,6,0,0,0-6,6V96a6,6,0,0,0,12,0V62.48l45.76,45.76a6,6,0,0,0,8.48-8.48Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,48V96a8,8,0,0,1-16,0V67.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L188.69,56H160a8,8,0,0,1,0-16h48A8,8,0,0,1,216,48ZM98.34,146.34,56,188.69V160a8,8,0,0,0-16,0v48a8,8,0,0,0,8,8H96a8,8,0,0,0,0-16H67.31l42.35-42.34a8,8,0,0,0-11.32-11.32ZM208,152a8,8,0,0,0-8,8v28.69l-42.34-42.35a8,8,0,0,0-11.32,11.32L188.69,200H160a8,8,0,0,0,0,16h48a8,8,0,0,0,8-8V160A8,8,0,0,0,208,152ZM67.31,56H96a8,8,0,0,0,0-16H48a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31l42.34,42.35a8,8,0,0,0,11.32-11.32Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M212,48V96a4,4,0,0,1-8,0V57.66l-49.17,49.17a4,4,0,0,1-5.66-5.66L198.34,52H160a4,4,0,0,1,0-8h48A4,4,0,0,1,212,48ZM101.17,149.17,52,198.34V160a4,4,0,0,0-8,0v48a4,4,0,0,0,4,4H96a4,4,0,0,0,0-8H57.66l49.17-49.17a4,4,0,0,0-5.66-5.66ZM208,156a4,4,0,0,0-4,4v38.34l-49.17-49.17a4,4,0,0,0-5.66,5.66L198.34,204H160a4,4,0,0,0,0,8h48a4,4,0,0,0,4-4V160A4,4,0,0,0,208,156ZM57.66,52H96a4,4,0,0,0,0-8H48a4,4,0,0,0-4,4V96a4,4,0,0,0,8,0V57.66l49.17,49.17a4,4,0,0,0,5.66-5.66Z"}))]]),b1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M168.49,199.51a12,12,0,0,1-17,17l-80-80a12,12,0,0,1,0-17l80-80a12,12,0,0,1,17,17L97,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M160,48V208L80,128Z",opacity:"0.2"}),e.createElement("path",{d:"M163.06,40.61a8,8,0,0,0-8.72,1.73l-80,80a8,8,0,0,0,0,11.32l80,80A8,8,0,0,0,168,208V48A8,8,0,0,0,163.06,40.61ZM152,188.69,91.31,128,152,67.31Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M164.24,203.76a6,6,0,1,1-8.48,8.48l-80-80a6,6,0,0,1,0-8.48l80-80a6,6,0,0,1,8.48,8.48L88.49,128Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M162.83,205.17a4,4,0,0,1-5.66,5.66l-80-80a4,4,0,0,1,0-5.66l80-80a4,4,0,1,1,5.66,5.66L85.66,128Z"}))]]),p1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M184.49,136.49l-80,80a12,12,0,0,1-17-17L159,128,87.51,56.49a12,12,0,1,1,17-17l80,80A12,12,0,0,1,184.49,136.49Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M176,128,96,208V48Z",opacity:"0.2"}),e.createElement("path",{d:"M181.66,122.34l-80-80A8,8,0,0,0,88,48V208a8,8,0,0,0,13.66,5.66l80-80A8,8,0,0,0,181.66,122.34ZM104,188.69V67.31L164.69,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M181.66,133.66l-80,80A8,8,0,0,1,88,208V48a8,8,0,0,1,13.66-5.66l80,80A8,8,0,0,1,181.66,133.66Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M180.24,132.24l-80,80a6,6,0,0,1-8.48-8.48L167.51,128,91.76,52.24a6,6,0,0,1,8.48-8.48l80,80A6,6,0,0,1,180.24,132.24Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M178.83,130.83l-80,80a4,4,0,0,1-5.66-5.66L170.34,128,93.17,50.83a4,4,0,0,1,5.66-5.66l80,80A4,4,0,0,1,178.83,130.83Z"}))]]),g1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,20A108,108,0,0,0,31.85,177.23L21,209.66A20,20,0,0,0,46.34,235l32.43-10.81A108,108,0,1,0,128,20Zm0,192a84,84,0,0,1-42.06-11.27,12,12,0,0,0-6-1.62,12.1,12.1,0,0,0-3.8.62l-29.79,9.93,9.93-29.79a12,12,0,0,0-1-9.81A84,84,0,1,1,128,212Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,128A96,96,0,0,1,79.93,211.11h0L42.54,223.58a8,8,0,0,1-10.12-10.12l12.47-37.39h0A96,96,0,1,1,224,128Z",opacity:"0.2"}),e.createElement("path",{d:"M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-6.54-.67L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232,128A104,104,0,0,1,79.12,219.82L45.07,231.17a16,16,0,0,1-20.24-20.24l11.35-34.05A104,104,0,1,1,232,128Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,26A102,102,0,0,0,38.35,176.69L26.73,211.56a14,14,0,0,0,17.71,17.71l34.87-11.62A102,102,0,1,0,128,26Zm0,192a90,90,0,0,1-45.06-12.08,6.09,6.09,0,0,0-3-.81,6.2,6.2,0,0,0-1.9.31L40.65,217.88a2,2,0,0,1-2.53-2.53L50.58,178a6,6,0,0,0-.5-4.91A90,90,0,1,1,128,218Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-6.54-.67L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,28A100,100,0,0,0,40.53,176.5l-11.9,35.69a12,12,0,0,0,15.18,15.18l35.69-11.9A100,100,0,1,0,128,28Zm0,192a92,92,0,0,1-46.07-12.35,4.05,4.05,0,0,0-2-.54,3.93,3.93,0,0,0-1.27.21L41.28,219.78a4,4,0,0,1-5.06-5.06l12.46-37.38a4,4,0,0,0-.33-3.27A92,92,0,1,1,128,220Z"}))]]),v1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm68-84a12,12,0,0,1-12,12H128a12,12,0,0,1-12-12V72a12,12,0,0,1,24,0v44h44A12,12,0,0,1,196,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),e.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm56,112H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48a8,8,0,0,1,0,16Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,26A102,102,0,1,0,230,128,102.12,102.12,0,0,0,128,26Zm0,192a90,90,0,1,1,90-90A90.1,90.1,0,0,1,128,218Zm62-90a6,6,0,0,1-6,6H128a6,6,0,0,1-6-6V72a6,6,0,0,1,12,0v50h50A6,6,0,0,1,190,128Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm60-92a4,4,0,0,1-4,4H128a4,4,0,0,1-4-4V72a4,4,0,0,1,8,0v52h52A4,4,0,0,1,188,128Z"}))]]),x1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,44H32A20,20,0,0,0,12,64V192a20,20,0,0,0,20,20H224a20,20,0,0,0,20-20V64A20,20,0,0,0,224,44Zm-4,144H36V68H220ZM52,128a44,44,0,0,1,66-38.11,12,12,0,0,1-12,20.78,20,20,0,1,0,0,34.66,12,12,0,1,1,12,20.77A44,44,0,0,1,52,128Zm80,0a44,44,0,0,1,66-38.11,12,12,0,0,1-12,20.78,20,20,0,1,0,0,34.66,12,12,0,1,1,12,20.77A44,44,0,0,1,132,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232,64V192a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H224A8,8,0,0,1,232,64Z",opacity:"0.2"}),e.createElement("path",{d:"M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192ZM118.93,151.71A8,8,0,0,1,116,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,118.93,151.71Zm80,0A8,8,0,0,1,196,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,198.93,151.71Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48ZM116,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58,8,8,0,0,1,8,13.85Zm80,0a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58,8,8,0,0,1,8,13.85Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,50H32A14,14,0,0,0,18,64V192a14,14,0,0,0,14,14H224a14,14,0,0,0,14-14V64A14,14,0,0,0,224,50Zm2,142a2,2,0,0,1-2,2H32a2,2,0,0,1-2-2V64a2,2,0,0,1,2-2H224a2,2,0,0,1,2,2ZM117.19,152.71a6,6,0,0,1-2.19,8.2,38,38,0,1,1,0-65.82,6,6,0,1,1-6,10.38,26,26,0,1,0,0,45.05A6,6,0,0,1,117.19,152.71Zm80,0a6,6,0,0,1-2.19,8.2,38,38,0,1,1,0-65.82,6,6,0,1,1-6,10.38,26,26,0,1,0,0,45.05A6,6,0,0,1,197.19,152.71Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192ZM118.92,151.71A8,8,0,0,1,116,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,118.92,151.71Zm80,0A8,8,0,0,1,196,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,198.92,151.71Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,52H32A12,12,0,0,0,20,64V192a12,12,0,0,0,12,12H224a12,12,0,0,0,12-12V64A12,12,0,0,0,224,52Zm4,140a4,4,0,0,1-4,4H32a4,4,0,0,1-4-4V64a4,4,0,0,1,4-4H224a4,4,0,0,1,4,4ZM115.46,153.71a4,4,0,0,1-1.46,5.47,36,36,0,1,1,0-62.36,4,4,0,0,1-4,6.92,28,28,0,1,0,0,48.52A4,4,0,0,1,115.46,153.71Zm80,0a4,4,0,0,1-1.46,5.47,36,36,0,1,1,0-62.36,4,4,0,0,1-4,6.92,28,28,0,1,0,0,48.52A4,4,0,0,1,195.46,153.71Z"}))]]),$1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M246.81,111.29,158.63,55.12A19.91,19.91,0,0,0,128,71.84v30L54.63,55.12A19.91,19.91,0,0,0,24,71.84V184.16a19.93,19.93,0,0,0,30.63,16.72L128,154.15v30a19.93,19.93,0,0,0,30.63,16.72l88.18-56.17a19.79,19.79,0,0,0,0-33.42ZM48,176.64V79.36L124.38,128Zm104,0V79.36L228.38,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M144,128a7.76,7.76,0,0,1-3.63,6.59L52.18,190.74A7.91,7.91,0,0,1,40,184.15V71.85a7.91,7.91,0,0,1,12.18-6.59l88.19,56.15A7.76,7.76,0,0,1,144,128Zm100.37-6.59L156.18,65.26A7.91,7.91,0,0,0,144,71.85v112.3a7.91,7.91,0,0,0,12.18,6.59l88.19-56.15A7.8,7.8,0,0,0,244.37,121.41Z",opacity:"0.2"}),e.createElement("path",{d:"M248.67,114.66,160.48,58.5A15.91,15.91,0,0,0,136,71.84v37.3L56.48,58.5A15.91,15.91,0,0,0,32,71.84V184.16A15.92,15.92,0,0,0,56.48,197.5L136,146.86v37.3a15.92,15.92,0,0,0,24.48,13.34l88.19-56.16a15.8,15.8,0,0,0,0-26.68ZM48,183.94V72.07L135.82,128Zm104,0V72.07L239.82,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M247.59,116.35,159.41,60.18a14,14,0,0,0-14.22-.46A13.83,13.83,0,0,0,138,71.84v41L55.41,60.18a14,14,0,0,0-14.22-.46A13.83,13.83,0,0,0,34,71.84V184.16a13.83,13.83,0,0,0,7.19,12.12,14,14,0,0,0,14.22-.46L138,143.21v40.95a13.83,13.83,0,0,0,7.19,12.12,14,14,0,0,0,14.22-.46l88.18-56.17a13.79,13.79,0,0,0,0-23.3ZM137.15,129.53,49,185.69a1.9,1.9,0,0,1-2,.06,1.73,1.73,0,0,1-1-1.59V71.84a1.73,1.73,0,0,1,1-1.59,2,2,0,0,1,1-.26,1.87,1.87,0,0,1,1,.32l88.19,56.16a1.8,1.8,0,0,1,0,3.06Zm104,0L153,185.69a1.9,1.9,0,0,1-2,.06,1.73,1.73,0,0,1-1-1.59V71.84a1.73,1.73,0,0,1,1-1.59,2,2,0,0,1,1-.26,1.87,1.87,0,0,1,1,.32l88.19,56.16a1.8,1.8,0,0,1,0,3.06Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M248.67,114.66,160.48,58.5A15.91,15.91,0,0,0,136,71.84v37.3L56.48,58.5A15.91,15.91,0,0,0,32,71.84V184.16A15.92,15.92,0,0,0,56.48,197.5L136,146.86v37.3a15.92,15.92,0,0,0,24.48,13.34l88.19-56.16a15.8,15.8,0,0,0,0-26.68ZM48,183.94V72.07L135.82,128Zm104,0V72.07L239.82,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M246.52,118,158.33,61.87a12,12,0,0,0-12.18-.39A11.66,11.66,0,0,0,140,71.84v44.59L54.33,61.87a12,12,0,0,0-12.18-.39A11.66,11.66,0,0,0,36,71.84V184.16a11.66,11.66,0,0,0,6.15,10.36,12,12,0,0,0,12.18-.39L140,139.57v44.59a11.66,11.66,0,0,0,6.15,10.36,12,12,0,0,0,12.18-.39L246.52,138a11.81,11.81,0,0,0,0-19.94Zm-108.3,13.19L50,187.38a3.91,3.91,0,0,1-4,.13,3.76,3.76,0,0,1-2-3.35V71.84a3.76,3.76,0,0,1,2-3.35,4,4,0,0,1,1.91-.5,3.94,3.94,0,0,1,2.13.63l88.18,56.16a3.8,3.8,0,0,1,0,6.44Zm104,0L154,187.38a3.91,3.91,0,0,1-4,.13,3.76,3.76,0,0,1-2-3.35V71.84a3.76,3.76,0,0,1,2-3.35,4,4,0,0,1,1.91-.5,3.94,3.94,0,0,1,2.13.63l88.18,56.16a3.8,3.8,0,0,1,0,6.44Z"}))]]),y1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M230.14,25.86a20,20,0,0,0-19.57-5.11l-.22.07L18.44,79a20,20,0,0,0-3.06,37.25L99,157l40.71,83.65a19.81,19.81,0,0,0,18,11.38c.57,0,1.15,0,1.73-.07A19.82,19.82,0,0,0,177,237.56L235.18,45.65a1.42,1.42,0,0,0,.07-.22A20,20,0,0,0,230.14,25.86ZM156.91,221.07l-34.37-70.64,46-45.95a12,12,0,0,0-17-17l-46,46L34.93,99.09,210,46Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M223.69,42.18l-58.22,192a8,8,0,0,1-14.92,1.25L108,148,20.58,105.45a8,8,0,0,1,1.25-14.92l192-58.22A8,8,0,0,1,223.69,42.18Z",opacity:"0.2"}),e.createElement("path",{d:"M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14,0-.07-40.06-82.3,48-48a8,8,0,0,0-11.31-11.31l-48,48L24.08,98.25l-.07,0,.14,0L216,40Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M231.4,44.34s0,.1,0,.15l-58.2,191.94a15.88,15.88,0,0,1-14,11.51q-.69.06-1.38.06a15.86,15.86,0,0,1-14.42-9.15L107,164.15a4,4,0,0,1,.77-4.58l57.92-57.92a8,8,0,0,0-11.31-11.31L96.43,148.26a4,4,0,0,1-4.58.77L17.08,112.64a16,16,0,0,1,2.49-29.8l191.94-58.2.15,0A16,16,0,0,1,231.4,44.34Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M225.88,30.12a13.83,13.83,0,0,0-13.7-3.58l-.11,0L20.14,84.77A14,14,0,0,0,18,110.85l85.56,41.64L145.12,238a13.87,13.87,0,0,0,12.61,8c.4,0,.81,0,1.21-.05a13.9,13.9,0,0,0,12.29-10.09l58.2-191.93,0-.11A13.83,13.83,0,0,0,225.88,30.12Zm-8,10.4L159.73,232.43l0,.11a2,2,0,0,1-3.76.26l-40.68-83.58,49-49a6,6,0,1,0-8.49-8.49l-49,49L23.15,100a2,2,0,0,1,.31-3.74l.11,0L215.48,38.08a1.94,1.94,0,0,1,1.92.52A2,2,0,0,1,217.92,40.52Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14,0-.07-40.06-82.3,48-48a8,8,0,0,0-11.31-11.31l-48,48L24.08,98.25l-.07,0,.14,0L216,40Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224.47,31.52a11.87,11.87,0,0,0-11.82-3L20.74,86.67a12,12,0,0,0-1.91,22.38L105,151l41.92,86.15A11.88,11.88,0,0,0,157.74,244c.34,0,.69,0,1,0a11.89,11.89,0,0,0,10.52-8.63l58.21-192,0-.08A11.85,11.85,0,0,0,224.47,31.52Zm-4.62,9.54-58.23,192a4,4,0,0,1-7.48.59l-41.3-84.86,50-50a4,4,0,1,0-5.66-5.66l-50,50-84.9-41.31a3.88,3.88,0,0,1-2.27-4,3.93,3.93,0,0,1,3-3.54L214.9,36.16A3.93,3.93,0,0,1,216,36a4,4,0,0,1,2.79,1.19A3.93,3.93,0,0,1,219.85,41.06Z"}))]]),A1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M200,28H160a20,20,0,0,0-20,20V208a20,20,0,0,0,20,20h40a20,20,0,0,0,20-20V48A20,20,0,0,0,200,28Zm-4,176H164V52h32ZM96,28H56A20,20,0,0,0,36,48V208a20,20,0,0,0,20,20H96a20,20,0,0,0,20-20V48A20,20,0,0,0,96,28ZM92,204H60V52H92Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M208,48V208a8,8,0,0,1-8,8H160a8,8,0,0,1-8-8V48a8,8,0,0,1,8-8h40A8,8,0,0,1,208,48ZM96,40H56a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8H96a8,8,0,0,0,8-8V48A8,8,0,0,0,96,40Z",opacity:"0.2"}),e.createElement("path",{d:"M200,32H160a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H160V48h40ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Zm0,176H56V48H96Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M200,34H160a14,14,0,0,0-14,14V208a14,14,0,0,0,14,14h40a14,14,0,0,0,14-14V48A14,14,0,0,0,200,34Zm2,174a2,2,0,0,1-2,2H160a2,2,0,0,1-2-2V48a2,2,0,0,1,2-2h40a2,2,0,0,1,2,2ZM96,34H56A14,14,0,0,0,42,48V208a14,14,0,0,0,14,14H96a14,14,0,0,0,14-14V48A14,14,0,0,0,96,34Zm2,174a2,2,0,0,1-2,2H56a2,2,0,0,1-2-2V48a2,2,0,0,1,2-2H96a2,2,0,0,1,2,2Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M200,32H160a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H160V48h40ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Zm0,176H56V48H96Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M200,36H160a12,12,0,0,0-12,12V208a12,12,0,0,0,12,12h40a12,12,0,0,0,12-12V48A12,12,0,0,0,200,36Zm4,172a4,4,0,0,1-4,4H160a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4h40a4,4,0,0,1,4,4ZM96,36H56A12,12,0,0,0,44,48V208a12,12,0,0,0,12,12H96a12,12,0,0,0,12-12V48A12,12,0,0,0,96,36Zm4,172a4,4,0,0,1-4,4H56a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H96a4,4,0,0,1,4,4Z"}))]]),E1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,44H40A20,20,0,0,0,20,64V192a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V64A20,20,0,0,0,216,44ZM44,68H212v48H136a12,12,0,0,0-12,12v60H44ZM148,188V140h64v48Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,64v64H136v72H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H216A8,8,0,0,1,224,64Z",opacity:"0.2"}),e.createElement("path",{d:"M216,48H40A16,16,0,0,0,24,64V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,64H216v56H136a8,8,0,0,0-8,8v64H40ZM216,192H144V136h72v56Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,48H40A16,16,0,0,0,24,64V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48Zm0,144H136V128h80v64Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,50H40A14,14,0,0,0,26,64V192a14,14,0,0,0,14,14H216a14,14,0,0,0,14-14V64A14,14,0,0,0,216,50ZM38,192V64a2,2,0,0,1,2-2H216a2,2,0,0,1,2,2v58H136a6,6,0,0,0-6,6v66H40A2,2,0,0,1,38,192Zm178,2H142V134h76v58A2,2,0,0,1,216,194Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,48H40A16,16,0,0,0,24,64V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,64H216v56H136a8,8,0,0,0-8,8v64H40ZM216,192H144V136h72v56Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M216,52H40A12,12,0,0,0,28,64V192a12,12,0,0,0,12,12H216a12,12,0,0,0,12-12V64A12,12,0,0,0,216,52ZM36,192V64a4,4,0,0,1,4-4H216a4,4,0,0,1,4,4v60H136a4,4,0,0,0-4,4v68H40A4,4,0,0,1,36,192Zm180,4H140V132h80v60A4,4,0,0,1,216,196Z"}))]]),M1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M234.49,111.07,90.41,22.94A20,20,0,0,0,60,39.87V216.13a20,20,0,0,0,30.41,16.93l144.08-88.13a19.82,19.82,0,0,0,0-33.86ZM84,208.85V47.15L216.16,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M228.23,134.69,84.15,222.81A8,8,0,0,1,72,216.12V39.88a8,8,0,0,1,12.15-6.69l144.08,88.12A7.82,7.82,0,0,1,228.23,134.69Z",opacity:"0.2"}),e.createElement("path",{d:"M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M240,128a15.74,15.74,0,0,1-7.6,13.51L88.32,229.65a16,16,0,0,1-16.2.3A15.86,15.86,0,0,1,64,216.13V39.87a15.86,15.86,0,0,1,8.12-13.82,16,16,0,0,1,16.2.3L232.4,114.49A15.74,15.74,0,0,1,240,128Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M231.36,116.19,87.28,28.06a14,14,0,0,0-14.18-.27A13.69,13.69,0,0,0,66,39.87V216.13a13.69,13.69,0,0,0,7.1,12.08,14,14,0,0,0,14.18-.27l144.08-88.13a13.82,13.82,0,0,0,0-23.62Zm-6.26,13.38L81,217.7a2,2,0,0,1-2.06,0,1.78,1.78,0,0,1-1-1.61V39.87a1.78,1.78,0,0,1,1-1.61A2.06,2.06,0,0,1,80,38a2,2,0,0,1,1,.31L225.1,126.43a1.82,1.82,0,0,1,0,3.14Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M230.32,117.9,86.24,29.79a11.91,11.91,0,0,0-12.17-.23A11.71,11.71,0,0,0,68,39.89V216.11a11.71,11.71,0,0,0,6.07,10.33,11.91,11.91,0,0,0,12.17-.23L230.32,138.1a11.82,11.82,0,0,0,0-20.2Zm-4.18,13.37L82.06,219.39a4,4,0,0,1-4.07.07,3.77,3.77,0,0,1-2-3.35V39.89a3.77,3.77,0,0,1,2-3.35,4,4,0,0,1,4.07.07l144.08,88.12a3.8,3.8,0,0,1,0,6.54Z"}))]]),V1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M225.7,54.46a20,20,0,0,0-20.33.66L132,101.85v-30a19.91,19.91,0,0,0-30.63-16.72L13.19,111.29a19.79,19.79,0,0,0,0,33.42l88.18,56.17A19.91,19.91,0,0,0,132,184.16v-30l73.37,46.73A19.91,19.91,0,0,0,236,184.16V71.84A19.84,19.84,0,0,0,225.7,54.46ZM108,176.64,31.63,128,108,79.36Zm104,0L135.63,128,212,79.36Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M224,71.85v112.3a7.91,7.91,0,0,1-12.18,6.59l-88.19-56.15a7.8,7.8,0,0,1,0-13.18l88.19-56.15A7.91,7.91,0,0,1,224,71.85ZM107.82,65.26,19.63,121.41a7.8,7.8,0,0,0,0,13.18l88.19,56.15A7.91,7.91,0,0,0,120,184.15V71.85A7.91,7.91,0,0,0,107.82,65.26Z",opacity:"0.2"}),e.createElement("path",{d:"M223.77,58a16,16,0,0,0-16.25.53L128,109.14V71.84A15.91,15.91,0,0,0,103.52,58.5L15.33,114.66a15.8,15.8,0,0,0,0,26.68l88.19,56.16A15.91,15.91,0,0,0,128,184.16v-37.3l79.52,50.64A15.91,15.91,0,0,0,232,184.16V71.84A15.83,15.83,0,0,0,223.77,58ZM112,183.93,24.18,128,112,72.06Zm104,0L128.18,128,216,72.06Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M222.81,59.72a14,14,0,0,0-14.22.46L126,112.79V71.84a13.83,13.83,0,0,0-7.19-12.12,14,14,0,0,0-14.22.46L16.41,116.35a13.79,13.79,0,0,0,0,23.3l88.18,56.17a14,14,0,0,0,14.22.46A13.83,13.83,0,0,0,126,184.16V143.21l82.59,52.61a14,14,0,0,0,14.22.46A13.83,13.83,0,0,0,230,184.16V71.84A13.83,13.83,0,0,0,222.81,59.72ZM114,184.16a1.73,1.73,0,0,1-1,1.59,1.9,1.9,0,0,1-2-.06L22.85,129.53a1.8,1.8,0,0,1,0-3.06L111,70.31a1.87,1.87,0,0,1,1-.32,2,2,0,0,1,1,.26,1.73,1.73,0,0,1,1,1.59Zm104,0a1.73,1.73,0,0,1-1,1.59,1.9,1.9,0,0,1-2-.06l-88.19-56.16a1.8,1.8,0,0,1,0-3.06L215,70.31a1.87,1.87,0,0,1,1-.32,2,2,0,0,1,1,.26,1.73,1.73,0,0,1,1,1.59Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M223.77,58a16,16,0,0,0-16.25.53L128,109.14V71.84A15.91,15.91,0,0,0,103.52,58.5L15.33,114.66a15.8,15.8,0,0,0,0,26.68l88.19,56.16A15.91,15.91,0,0,0,128,184.16v-37.3l79.52,50.64A15.91,15.91,0,0,0,232,184.16V71.84A15.83,15.83,0,0,0,223.77,58ZM112,183.93,24.18,128,112,72.06Zm104,0L128.18,128,216,72.06Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M221.85,61.48a12,12,0,0,0-12.18.39L124,116.43V71.84a11.66,11.66,0,0,0-6.15-10.36,12,12,0,0,0-12.18.39L17.48,118a11.81,11.81,0,0,0,0,19.94l88.19,56.16a12,12,0,0,0,12.18.39A11.66,11.66,0,0,0,124,184.16V139.57l85.67,54.56a12,12,0,0,0,12.18.39A11.66,11.66,0,0,0,228,184.16V71.84A11.66,11.66,0,0,0,221.85,61.48ZM116,184.16a3.76,3.76,0,0,1-2,3.35,3.91,3.91,0,0,1-4-.13L21.78,131.22a3.8,3.8,0,0,1,0-6.44L110,68.62a3.94,3.94,0,0,1,2.13-.63,4,4,0,0,1,1.91.5,3.76,3.76,0,0,1,2,3.35Zm104,0a3.76,3.76,0,0,1-2,3.35,3.91,3.91,0,0,1-4-.13l-88.18-56.16a3.8,3.8,0,0,1,0-6.44L214,68.62a3.94,3.94,0,0,1,2.13-.63,4,4,0,0,1,1.91.5,3.76,3.76,0,0,1,2,3.35Z"}))]]),P1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M157.27,21.22a12,12,0,0,0-12.64,1.31L75.88,76H32A20,20,0,0,0,12,96v64a20,20,0,0,0,20,20H75.88l68.75,53.47A12,12,0,0,0,164,224V32A12,12,0,0,0,157.27,21.22ZM36,100H68v56H36Zm104,99.46L92,162.13V93.87l48-37.33ZM212,128a44,44,0,0,1-11,29.11,12,12,0,1,1-18-15.88,20,20,0,0,0,0-26.43,12,12,0,0,1,18-15.86A43.94,43.94,0,0,1,212,128Zm40,0a83.87,83.87,0,0,1-21.39,56,12,12,0,0,1-17.89-16,60,60,0,0,0,0-80,12,12,0,1,1,17.88-16A83.87,83.87,0,0,1,252,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M80,88v80H32a8,8,0,0,1-8-8V96a8,8,0,0,1,8-8Z",opacity:"0.2"}),e.createElement("path",{d:"M155.51,24.81a8,8,0,0,0-8.42.88L77.25,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H77.25l69.84,54.31A8,8,0,0,0,160,224V32A8,8,0,0,0,155.51,24.81ZM32,96H72v64H32ZM144,207.64,88,164.09V91.91l56-43.55Zm54-106.08a40,40,0,0,1,0,52.88,8,8,0,0,1-12-10.58,24,24,0,0,0,0-31.72,8,8,0,0,1,12-10.58ZM248,128a79.9,79.9,0,0,1-20.37,53.34,8,8,0,0,1-11.92-10.67,64,64,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67A79.83,79.83,0,0,1,248,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M160,32.25V223.69a8.29,8.29,0,0,1-3.91,7.18,8,8,0,0,1-9-.56l-65.57-51A4,4,0,0,1,80,176.16V79.84a4,4,0,0,1,1.55-3.15l65.57-51a8,8,0,0,1,10,.16A8.27,8.27,0,0,1,160,32.25ZM60,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H60a4,4,0,0,0,4-4V84A4,4,0,0,0,60,80Zm126.77,20.84a8,8,0,0,0-.72,11.3,24,24,0,0,1,0,31.72,8,8,0,1,0,12,10.58,40,40,0,0,0,0-52.88A8,8,0,0,0,186.74,100.84Zm40.89-26.17a8,8,0,1,0-11.92,10.66,64,64,0,0,1,0,85.34,8,8,0,1,0,11.92,10.66,80,80,0,0,0,0-106.66Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M154.64,26.61a6,6,0,0,0-6.32.65L77.94,82H32A14,14,0,0,0,18,96v64a14,14,0,0,0,14,14H77.94l70.38,54.74A6,6,0,0,0,158,224V32A6,6,0,0,0,154.64,26.61ZM30,160V96a2,2,0,0,1,2-2H74v68H32A2,2,0,0,1,30,160Zm116,51.73L86,165.07V90.93l60-46.66Zm50.53-108.85a38,38,0,0,1,0,50.24,6,6,0,1,1-9-7.94,26,26,0,0,0,0-34.37,6,6,0,0,1,9-7.93ZM246,128a77.86,77.86,0,0,1-19.86,52,6,6,0,1,1-8.94-8,66,66,0,0,0,0-88,6,6,0,1,1,8.94-8A77.86,77.86,0,0,1,246,128Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M155.51,24.81a8,8,0,0,0-8.42.88L77.25,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H77.25l69.84,54.31A8,8,0,0,0,160,224V32A8,8,0,0,0,155.51,24.81ZM32,96H72v64H32ZM144,207.64,88,164.09V91.91l56-43.55Zm54-106.08a40,40,0,0,1,0,52.88,8,8,0,0,1-12-10.58,24,24,0,0,0,0-31.72,8,8,0,0,1,12-10.58ZM248,128a79.9,79.9,0,0,1-20.37,53.34,8,8,0,0,1-11.92-10.67,64,64,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67A79.83,79.83,0,0,1,248,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M153.76,28.41a4,4,0,0,0-4.22.43L78.63,84H32A12,12,0,0,0,20,96v64a12,12,0,0,0,12,12H78.63l70.91,55.16A4.07,4.07,0,0,0,152,228a3.92,3.92,0,0,0,1.76-.41A4,4,0,0,0,156,224V32A4,4,0,0,0,153.76,28.41ZM28,160V96a4,4,0,0,1,4-4H76v72H32A4,4,0,0,1,28,160Zm120,55.82L84,166V90l64-49.78Zm47-111.61a36,36,0,0,1,0,47.59,4,4,0,1,1-6-5.3,28,28,0,0,0,0-37,4,4,0,0,1,6-5.28ZM244,128a75.88,75.88,0,0,1-19.35,50.67,4,4,0,0,1-6-5.34,68,68,0,0,0,0-90.66,4,4,0,0,1,6-5.34A75.88,75.88,0,0,1,244,128Z"}))]]),w1=new Map([["bold",e.createElement(e.Fragment,null,e.createElement("path",{d:"M56.88,31.93A12,12,0,1,0,39.12,48.07L64.51,76H32A20,20,0,0,0,12,96v64a20,20,0,0,0,20,20H75.88l68.75,53.47A12,12,0,0,0,164,224V185.44l35.12,38.63a12,12,0,0,0,17.76-16.14ZM36,100H68v56H36Zm104,99.46L92,162.13V106.24L140,159Zm-31-134a12,12,0,0,1,2.11-16.84l33.51-26.07A12,12,0,0,1,164,32V94.94a12,12,0,0,1-24,0V56.54l-14.15,11A12,12,0,0,1,109,65.44Zm74,49.35a12,12,0,0,1,18-15.85,44,44,0,0,1,5.55,50.21,12,12,0,0,1-21-11.55A19.67,19.67,0,0,0,188,128,20,20,0,0,0,183,114.79ZM252,128a84.18,84.18,0,0,1-19.11,53.35,12,12,0,1,1-18.53-15.25A60,60,0,0,0,212.73,88a12,12,0,1,1,17.88-16A83.87,83.87,0,0,1,252,128Z"}))],["duotone",e.createElement(e.Fragment,null,e.createElement("path",{d:"M80,88v80H32a8,8,0,0,1-8-8V96a8,8,0,0,1,8-8Z",opacity:"0.2"}),e.createElement("path",{d:"M53.92,34.62A8,8,0,1,0,42.08,45.38L73.55,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H77.25l69.84,54.31A8,8,0,0,0,160,224V175.09l42.08,46.29a8,8,0,1,0,11.84-10.76ZM32,96H72v64H32ZM144,207.64,88,164.09V95.89l56,61.6Zm42-63.77a24,24,0,0,0,0-31.72,8,8,0,1,1,12-10.57,40,40,0,0,1,0,52.88,8,8,0,0,1-12-10.59Zm-80.16-76a8,8,0,0,1,1.4-11.23l39.85-31A8,8,0,0,1,160,32v74.83a8,8,0,0,1-16,0V48.36l-26.94,21A8,8,0,0,1,105.84,67.91ZM248,128a79.9,79.9,0,0,1-20.37,53.34,8,8,0,0,1-11.92-10.67,64,64,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67A79.83,79.83,0,0,1,248,128Z"}))],["fill",e.createElement(e.Fragment,null,e.createElement("path",{d:"M213.92,210.62a8,8,0,1,1-11.84,10.76L160,175.09v48.6a8.29,8.29,0,0,1-3.91,7.18,8,8,0,0,1-9-.56l-65.55-51A4,4,0,0,1,80,176.18V87.09L42.08,45.38A8,8,0,1,1,53.92,34.62Zm-27.21-55.46a8,8,0,0,0,11.29-.7,40,40,0,0,0,0-52.88,8,8,0,1,0-12,10.57,24,24,0,0,1,0,31.72A8,8,0,0,0,186.71,155.16Zm40.92-80.49a8,8,0,1,0-11.92,10.66,64,64,0,0,1,0,85.34,8,8,0,1,0,11.92,10.66,80,80,0,0,0,0-106.66ZM153,119.87a4,4,0,0,0,7-2.7V32.25a8.27,8.27,0,0,0-2.88-6.4,8,8,0,0,0-10-.16L103.83,59.33a4,4,0,0,0-.5,5.85ZM60,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H60a4,4,0,0,0,4-4V84A4,4,0,0,0,60,80Z"}))],["light",e.createElement(e.Fragment,null,e.createElement("path",{d:"M52.44,36A6,6,0,0,0,43.56,44L78,81.94l-.08.06H32A14,14,0,0,0,18,96v64a14,14,0,0,0,14,14H77.94l70.38,54.74A6,6,0,0,0,158,224V169.92L203.56,220a6,6,0,0,0,8.88-8.08ZM30,160V96a2,2,0,0,1,2-2H74v68H32A2,2,0,0,1,30,160Zm116,51.73L86,165.07V90.93l.11-.08L146,156.72Zm41.5-66.53a26,26,0,0,0,0-34.37,6,6,0,1,1,9-7.93,38,38,0,0,1,0,50.24,6,6,0,0,1-9-7.94ZM107.41,66.68a6,6,0,0,1,1.06-8.42l39.85-31A6,6,0,0,1,158,32v74.83a6,6,0,0,1-12,0V44.27L115.83,67.73A6,6,0,0,1,107.41,66.68ZM246,128a77.86,77.86,0,0,1-19.86,52,6,6,0,1,1-8.94-8,66,66,0,0,0,0-88,6,6,0,1,1,8.94-8A77.86,77.86,0,0,1,246,128Z"}))],["regular",e.createElement(e.Fragment,null,e.createElement("path",{d:"M53.92,34.62A8,8,0,1,0,42.08,45.38L73.55,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H77.25l69.84,54.31A8,8,0,0,0,160,224V175.09l42.08,46.29a8,8,0,1,0,11.84-10.76ZM32,96H72v64H32ZM144,207.64,88,164.09V95.89l56,61.6Zm42-63.77a24,24,0,0,0,0-31.72,8,8,0,1,1,12-10.57,40,40,0,0,1,0,52.88,8,8,0,0,1-12-10.59Zm-80.16-76a8,8,0,0,1,1.4-11.23l39.85-31A8,8,0,0,1,160,32v74.83a8,8,0,0,1-16,0V48.36l-26.94,21A8,8,0,0,1,105.84,67.91ZM248,128a79.9,79.9,0,0,1-20.37,53.34,8,8,0,0,1-11.92-10.67,64,64,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67A79.83,79.83,0,0,1,248,128Z"}))],["thin",e.createElement(e.Fragment,null,e.createElement("path",{d:"M51,37.31A4,4,0,0,0,45,42.69L81,82.19,78.63,84H32A12,12,0,0,0,20,96v64a12,12,0,0,0,12,12H78.63l70.91,55.16A4.07,4.07,0,0,0,152,228a3.92,3.92,0,0,0,1.76-.41A4,4,0,0,0,156,224V164.75l49,53.94a4,4,0,1,0,5.92-5.38ZM28,160V96a4,4,0,0,1,4-4H76v72H32A4,4,0,0,1,28,160Zm120,55.82L84,166V90l2.35-1.83L148,156Zm41-69.3a28,28,0,0,0,0-37,4,4,0,1,1,6-5.29,36,36,0,0,1,0,47.59,4,4,0,1,1-6-5.29ZM109,65.45a4,4,0,0,1,.7-5.61l39.85-31A4,4,0,0,1,156,32v74.83a4,4,0,0,1-8,0V40.18l-33.39,26A4,4,0,0,1,109,65.45ZM244,128a75.88,75.88,0,0,1-19.35,50.67,4,4,0,0,1-6-5.34,68,68,0,0,0,0-90.66,4,4,0,0,1,6-5.34A75.88,75.88,0,0,1,244,128Z"}))]]),It=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:m1}));It.displayName="ArrowClockwiseIcon";const Ie=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:f1}));Ie.displayName="ArrowCounterClockwiseIcon";const Ot=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:h1}));Ot.displayName="ArrowsOutIcon";const zt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:b1}));zt.displayName="CaretLeftIcon";const _t=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:p1}));_t.displayName="CaretRightIcon";const Le=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:g1}));Le.displayName="ChatCircleIcon";const Bt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:v1}));Bt.displayName="ClockIcon";const Ut=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:x1}));Ut.displayName="ClosedCaptioningIcon";const Wt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:$1}));Wt.displayName="FastForwardIcon";const qt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:y1}));qt.displayName="PaperPlaneTiltIcon";const Xt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:A1}));Xt.displayName="PauseIcon";const Yt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:E1}));Yt.displayName="PictureInPictureIcon";const Oe=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:M1}));Oe.displayName="PlayIcon";const Gt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:V1}));Gt.displayName="RewindIcon";const Jt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:P1}));Jt.displayName="SpeakerHighIcon";const Qt=e.forwardRef((a,t)=>e.createElement(R,{ref:t,...a,weights:w1}));Qt.displayName="SpeakerSlashIcon";function Z1(a){return"video"in a}function j1(a){return"password_protected"in a}function L1(a){return"expired"in a}async function S1(a){return(await fetch(`/s/${a}/data`,{credentials:"include"})).json()}async function H1(a,t){return(await fetch(`/s/${a}/verify-password`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify({password:t})})).ok}async function T1(a){return(await(await fetch(`/s/${a}/reactions`)).json()).reactions||[]}async function C1(a,t,n){return(await fetch(`/s/${a}/react`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify({timestamp:t,emoji:n})})).ok}async function bt(a,t=1,n=50){return(await fetch(`/s/${a}/comments?page=${t}&limit=${n}`)).json()}async function F1(a,t,n,l){const s=await fetch(`/s/${a}/comment`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify({timestamp:t,author_name:n,text:l})});if(!s.ok)throw new Error("Could not post comment");return(await s.json()).id}function N(a){const t=Math.max(0,Math.floor(a)),n=Math.floor(t/3600),l=Math.floor(t%3600/60),s=t%60;return n>0?`${n}:${String(l).padStart(2,"0")}:${String(s).padStart(2,"0")}`:`${l}:${String(s).padStart(2,"0")}`}function ea(a){return new Date(a+"Z").toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}function k1(a,t,n,l=18){const s=n>0?l/n*t:t*.02,i=[];for(const o of[...a].sort((d,c)=>d.timestamp-c.timestamp)){const d=i.at(-1);d&&o.timestamp-d.members.at(-1).timestampc+u.timestamp,0)/d.members.length):i.push({time:o.timestamp,members:[o]})}return i}function N1(a,t){const n=/(?s&&l.push({text:a.slice(s,o)});const d=Number(i[1]||0)*3600+Number(i[2])*60+Number(i[3]);l.push({text:i[0],...t>0&&d<=t?{time:d}:{}}),s=o+i[0].length}return sk1(a,t,n,28);function D1({data:a,videoRef:t,comments:n,reactions:l,time:s,duration:i,onTime:o,onDuration:d,seek:c}){const u=e.useRef(null),[v,m]=e.useState();e.useEffect(()=>{let f=!0;m(void 0);const M=new Image,F=`/thumb/${a.shareCode}`;return M.onload=()=>{f&&m(F)},M.onerror=()=>{f&&m(void 0)},M.src=F,()=>{f=!1,M.onload=null,M.onerror=null}},[a.shareCode]);const g=e.useRef(null),[p,A]=e.useState(500),[b,h]=e.useState(!1),[x,V]=e.useState(!1),[Z,L]=e.useState(!1),[E,j]=e.useState(1),[C,I]=e.useState(!1),[q,Y]=e.useState(1),[z,D]=e.useState(!0),[K,$]=e.useState(!1),[S,H]=e.useState(!1),w=e.useRef(1),_=a.segments.find(f=>s>=f.start_time&&s{const f=t.current;if(f)if(!f.paused)f.pause();else try{await f.play()}catch{ie.danger("Playback could not start. Try again.")}},[t]),ne=e.useCallback(()=>{const f=t.current;f&&(f.muted||f.volume===0?(f.muted=!1,f.volume=w.current||1):(w.current=f.volume,f.muted=!0))},[t]),re=e.useCallback(async()=>{try{document.fullscreenElement?await document.exitFullscreen():await u.current?.requestFullscreen()}catch{ie.danger("Fullscreen is unavailable in this browser.")}},[]);async function y(){try{document.pictureInPictureElement?await document.exitPictureInPicture():await t.current?.requestPictureInPicture()}catch{ie.danger("Picture in picture is unavailable for this recording.")}}return e.useEffect(()=>{if($(!!document.pictureInPictureEnabled),!g.current)return;const f=new ResizeObserver(([M])=>A(M.contentRect.width));return f.observe(g.current),()=>f.disconnect()},[]),e.useEffect(()=>{function f(M){const F=M.target;if(M.defaultPrevented||M.metaKey||M.ctrlKey||M.altKey||F.closest('input, textarea, select, button, a, [role="slider"], [role="tab"], [role="menuitem"], [contenteditable="true"]'))return;const X=t.current;if(X)switch(M.key.toLowerCase()){case" ":case"k":M.preventDefault(),B();break;case"arrowleft":M.preventDefault(),c(X.currentTime-5);break;case"arrowright":M.preventDefault(),c(X.currentTime+5);break;case"m":ne();break;case"f":re();break;case"c":D(Se=>!Se);break}}return document.addEventListener("keydown",f),()=>document.removeEventListener("keydown",f)},[c,t,B,ne,re]),r.jsxs(Pt,{ref:u,className:"video-shell",children:[r.jsxs("div",{className:"video-stage",children:[r.jsx("video",{ref:t,src:`/v/${a.shareCode}`,poster:v,preload:"metadata",playsInline:!0,onClick:()=>{B()},onPlay:()=>{h(!0),L(!1)},onPause:()=>h(!1),onWaiting:()=>V(!0),onPlaying:()=>V(!1),onCanPlay:()=>V(!1),onError:()=>{H(!0),V(!1)},onTimeUpdate:f=>o(f.currentTarget.currentTime),onVolumeChange:f=>{j(f.currentTarget.volume),I(f.currentTarget.muted)},onRateChange:f=>Y(f.currentTarget.playbackRate),onEnded:()=>{L(!0),h(!1)},onSeeking:()=>L(!1),onLoadedMetadata:f=>{const M=f.currentTarget;d(Number.isFinite(M.duration)?M.duration:0);const F=Number(new URLSearchParams(location.search).get("t"));Number.isFinite(F)&&F>0&&F0&&r.jsx("track",{kind:"captions",src:`/vtt/${a.shareCode}`,srcLang:"en",label:"English"})}),!b&&!S&&!(Z&&ae)&&r.jsx("div",{className:"video-play-overlay",children:r.jsx(T,{isIconOnly:!0,size:"lg","aria-label":Z?"Replay recording":"Play recording",onPress:()=>{B()},children:Z?r.jsx(Ie,{size:24}):r.jsx(Oe,{size:24,weight:"fill"})})}),x&&b&&r.jsx("div",{className:"video-play-overlay",children:r.jsx(wt,{"aria-label":"Buffering video"})}),S&&r.jsxs("div",{className:"video-end-card",children:[r.jsx("p",{children:"We couldn’t load this video."}),r.jsx(T,{variant:"secondary",onPress:()=>{H(!1),t.current?.load()},children:"Try again"})]}),z&&_&&r.jsx("div",{className:"video-caption",children:r.jsxs("span",{children:[_.speaker&&`${_.speaker}: `,_.text]})}),r.jsx("div",{className:"timestamp-reactions","aria-hidden":"true",children:l.filter(f=>s>=f.timestamp&&sr.jsx("span",{className:"timestamp-reaction",style:{left:`${15+M*19%70}%`},children:f.emoji},`${f.created_at}-${f.timestamp}-${M}`))}),Z&&ae&&r.jsxs("div",{className:"video-end-card",children:[r.jsx(Wa,{className:"button button--primary",href:ae,target:"_blank",rel:"noopener noreferrer",children:a.video.cta_text||"Learn more"}),r.jsxs(T,{variant:"secondary",onPress:()=>{c(0),B()},children:[r.jsx(Ie,{size:16}),"Replay"]})]})]}),r.jsxs("div",{className:"video-toolbar",children:[r.jsx("div",{ref:g,className:"timeline-markers","aria-label":"Comments and reactions on the timeline",children:R1([...n.map(f=>({timestamp:f.timestamp,kind:"comment",label:`${f.author_name}: ${f.text}`})),...l.map(f=>({timestamp:f.timestamp,kind:"reaction",emoji:f.emoji,label:`${f.emoji} reaction`})),...a.chapters.map(f=>({timestamp:f.timestamp,kind:"chapter",label:`Chapter: ${f.title}`}))],i,p).map((f,M)=>r.jsxs(ht,{children:[r.jsx(T,{size:"sm",variant:f.members.length>1?"primary":"ghost",className:"timeline-marker",style:{left:`${i?Math.max(2,Math.min(98,f.time/i*100)):2}%`},"aria-label":`${f.members.length>1?`${f.members.length} items`:f.members[0].label} at ${N(f.time)}`,onPress:()=>c(f.members[0].timestamp),children:f.members.length>1?f.members.length:f.members[0].kind==="reaction"?r.jsx("span",{"aria-hidden":"true",children:f.members[0].emoji}):f.members[0].kind==="comment"?r.jsx(Le,{size:12}):"•"}),r.jsx(ht.Content,{children:f.members.map(F=>`${N(F.timestamp)} ${F.label}`).join(" · ")})]},`timeline-${M}`))}),r.jsxs("div",{className:"video-progress",children:[r.jsx(Te,{"aria-label":"Video progress",value:Math.min(s,i||0),minValue:0,maxValue:i||1,size:"sm",valueLabel:`${N(s)} of ${N(i)}`,children:r.jsx(Te.Track,{children:r.jsx(Te.Fill,{})})}),r.jsx("input",{className:"video-seek-input",type:"range","aria-label":"Seek video","aria-valuetext":`${N(s)} of ${N(i)}`,min:0,max:i||1,step:.1,value:Math.min(s,i||0),disabled:!i,onChange:f=>c(Number(f.currentTarget.value))})]}),r.jsxs("div",{className:"video-controls",children:[r.jsx(T,{isIconOnly:!0,size:"sm",variant:"ghost","aria-label":b?"Pause":"Play",onPress:()=>{B()},children:b?r.jsx(Xt,{size:18,weight:"fill"}):r.jsx(Oe,{size:18,weight:"fill"})}),r.jsx(T,{isIconOnly:!0,size:"sm",variant:"ghost",className:"skip-control","aria-label":"Back 5 seconds",onPress:()=>c(s-5),children:r.jsx(Gt,{size:18})}),r.jsx(T,{isIconOnly:!0,size:"sm",variant:"ghost",className:"skip-control","aria-label":"Forward 5 seconds",onPress:()=>c(s+5),children:r.jsx(Wt,{size:18})}),r.jsx(T,{isIconOnly:!0,size:"sm",variant:"ghost","aria-label":C||E===0?"Unmute":"Mute",onPress:ne,children:C||E===0?r.jsx(Qt,{size:18}):r.jsx(Jt,{size:18})}),r.jsx(ye,{className:"volume-control","aria-label":"Volume",minValue:0,maxValue:1,step:.05,value:C?0:E,onChange:f=>{t.current&&(t.current.volume=Number(f),t.current.muted=Number(f)===0,Number(f)&&(w.current=Number(f)))},children:r.jsxs(ye.Track,{children:[r.jsx(ye.Fill,{}),r.jsx(ye.Thumb,{})]})}),r.jsxs("span",{className:"video-time",children:[N(s)," / ",N(i)]}),r.jsx("span",{className:"control-spacer"}),a.segments.length>0&&r.jsx(T,{isIconOnly:!0,size:"sm",variant:z?"secondary":"ghost","aria-label":"Captions","aria-pressed":z,onPress:()=>D(!z),children:r.jsx(Ut,{size:18})}),r.jsxs(be,{children:[r.jsxs(T,{size:"sm",variant:"ghost","aria-label":`Playback speed: ${q}×`,children:[q,"×"]}),r.jsx(be.Popover,{children:r.jsx(be.Menu,{"aria-label":"Playback speed",selectionMode:"single",selectedKeys:new Set([String(q)]),onAction:f=>{t.current&&(t.current.playbackRate=Number(f))},children:[.5,1,1.25,1.5,2].map(f=>r.jsxs(be.Item,{id:String(f),textValue:`${f}×`,children:[r.jsxs(Ae,{children:[f,"×"]}),r.jsx(be.ItemIndicator,{})]},f))})})]}),K&&r.jsx(T,{isIconOnly:!0,size:"sm",variant:"ghost",className:"pip-control","aria-label":"Picture in picture",onPress:()=>{y()},children:r.jsx(Yt,{size:18})}),r.jsx(T,{isIconOnly:!0,size:"sm",variant:"ghost","aria-label":"Fullscreen",onPress:()=>{re()},children:r.jsx(Ot,{size:18})})]})]})]})}function Ce({children:a,label:t}){const n=e.useRef(null),l=e.useRef(null),[s,i]=e.useState(0),[o,d]=e.useState(1),[c,u]=e.useState(0);return e.useLayoutEffect(()=>{const v=n.current,m=l.current;if(!v||!m)return;const g=()=>{const b=v.clientWidth;if(!b)return;const h=b+24,x=Math.max(1,Math.ceil((m.scrollWidth+24-1)/h));u(h),d(x),i(V=>Math.min(V,x-1))},p=new ResizeObserver(g),A=new MutationObserver(g);return p.observe(v),A.observe(m,{childList:!0,subtree:!0,characterData:!0}),g(),()=>{p.disconnect(),A.disconnect()}},[]),r.jsxs("div",{className:"paged-panel","aria-label":t,children:[r.jsx("div",{ref:n,className:"page-viewport",onFocusCapture:v=>{if(!c||!n.current)return;const m=n.current,g=v.target.getBoundingClientRect().left-m.getBoundingClientRect().left+s*c+m.scrollLeft;i(Math.max(0,Math.min(o-1,Math.floor((g+1)/c)))),m.scrollLeft=0},children:r.jsx("div",{ref:l,className:"page-flow",style:{transform:`translateX(-${s*c}px)`},children:a})}),r.jsxs("nav",{hidden:o===1,className:"page-navigation","aria-label":`${t} pages`,children:[r.jsx(T,{size:"sm",variant:"ghost",isIconOnly:!0,"aria-label":`Previous ${t.toLowerCase()} page`,isDisabled:s===0,onPress:()=>i(s-1),children:r.jsx(zt,{size:16})}),r.jsxs("span",{"aria-live":"polite",children:[s+1," / ",o]}),r.jsx(T,{size:"sm",variant:"ghost",isIconOnly:!0,"aria-label":`Next ${t.toLowerCase()} page`,isDisabled:s>=o-1,onPress:()=>i(s+1),children:r.jsx(_t,{size:16})})]})]})}function K1({data:a,time:t,duration:n,comments:l,setComments:s,seek:i,composerRef:o,selectedTab:d,setSelectedTab:c}){const[u,v]=e.useState(""),[m,g]=e.useState(""),[p,A]=e.useState(null),[b,h]=e.useState(!1),[x,V]=e.useState(!0),[Z,L]=e.useState(""),[E,j]=e.useState(1),[C,I]=e.useState(0),[q,Y]=e.useState(!1),z=l.reduce(($,S,H)=>S.timestamp<=t+.3?H:$,-1);e.useEffect(()=>{try{v(localStorage.getItem("recordly-comment-name")||"")}catch{}let $=!1;return bt(a.shareCode).then(S=>{$||(s([...S.comments].sort((H,w)=>H.timestamp-w.timestamp)),I(S.total),V(!1))}).catch(()=>{$||(L("Comments could not be loaded. Reload the page to try again."),V(!1))}),()=>{$=!0}},[a.shareCode,s]);async function D(){Y(!0);try{const $=await bt(a.shareCode,E+1);s(S=>[...new Map([...S,...$.comments].map(H=>[H.id,H])).values()].sort((H,w)=>H.timestamp-w.timestamp||H.id-w.id)),I($.total),j(E+1)}catch{ie.danger("Could not load more comments. Try again.")}finally{Y(!1)}}async function K(){if(b||!u.trim()||!m.trim())return;const $=p??t;h(!0),L("");try{const S=await F1(a.shareCode,$,u.trim(),m.trim());s(H=>[...H,{id:S,timestamp:$,author_name:u.trim(),text:m.trim(),created_at:new Date().toISOString().slice(0,19)}].sort((w,_)=>w.timestamp-_.timestamp)),I(C+1),g(""),A(null);try{localStorage.setItem("recordly-comment-name",u.trim())}catch{}ie.success(`Comment posted at ${N($)}`)}catch{L("Could not post your comment. Your draft is saved here—please try again.")}finally{h(!1)}}return r.jsxs(Pt,{className:"feedback-card",children:[r.jsxs(U,{selectedKey:d,onSelectionChange:$=>c(String($)),className:"feedback-tabs",children:[r.jsx(U.ListContainer,{children:r.jsxs(U.List,{"aria-label":"Recording feedback",children:[r.jsxs(U.Tab,{id:"comments",children:["Comments",C>0&&r.jsx(d0,{size:"sm",variant:"soft",children:C}),r.jsx(U.Indicator,{})]}),a.segments.length>0&&r.jsxs(U.Tab,{id:"transcript",children:["Transcript",r.jsx(U.Indicator,{})]}),(a.video.summary||a.chapters.length>0)&&r.jsxs(U.Tab,{id:"details",children:["Details",r.jsx(U.Indicator,{})]})]})}),r.jsx(U.Panel,{id:"comments",children:r.jsxs(Ce,{label:"Comments",children:[x?r.jsx("div",{className:"empty-feedback",children:r.jsx(wt,{"aria-label":"Loading comments"})}):l.length?l.map(($,S)=>r.jsxs("article",{className:"comment-item","data-active":S===z,children:[r.jsxs("div",{className:"comment-heading",children:[r.jsx(Ee,{size:"sm",children:r.jsx(Ee.Fallback,{children:$.author_name.slice(0,2).toUpperCase()})}),r.jsx("strong",{children:$.author_name}),r.jsx(T,{size:"sm",variant:"ghost","aria-label":`Jump to ${N($.timestamp)}`,onPress:()=>i($.timestamp),children:r.jsx("span",{className:"timestamp",children:N($.timestamp)})})]}),r.jsx("p",{children:N1($.text,n).map((H,w)=>H.time===void 0?H.text:r.jsx(T,{size:"sm",variant:"ghost",className:"inline-timestamp timestamp",onPress:()=>i(H.time),children:H.text},w))}),r.jsx("span",{className:"comment-date",children:ea($.created_at)})]},$.id)):r.jsxs("div",{className:"empty-feedback",children:[r.jsx(Le,{size:28}),r.jsx("h2",{children:"Start the conversation"}),r.jsx("p",{children:"Leave feedback at the exact moment you’re watching."})]}),l.length{D()},children:"Load more comments"})]})}),a.segments.length>0&&r.jsx(U.Panel,{id:"transcript",children:r.jsx(Ce,{label:"Transcript",children:a.segments.map(($,S)=>r.jsxs("article",{className:"transcript-item","data-active":t>=$.start_time&&t<$.end_time,children:[r.jsxs("div",{className:"transcript-heading",children:[r.jsx(T,{size:"sm",variant:"ghost",onPress:()=>i($.start_time),children:r.jsx("span",{className:"timestamp",children:N($.start_time)})}),$.speaker&&r.jsx("span",{children:$.speaker}),r.jsx(T,{size:"sm",variant:"ghost",isIconOnly:!0,className:"copy-timestamp","aria-label":`Copy link to ${N($.start_time)}`,onPress:()=>{Xa($.start_time)},children:r.jsx(qa,{size:15})})]}),r.jsx(T,{variant:"ghost",className:"transcript-seek",onPress:()=>i($.start_time),children:$.text})]},S))})}),(a.video.summary||a.chapters.length>0)&&r.jsx(U.Panel,{id:"details",children:r.jsxs(Ce,{label:"Details",children:[r.jsxs("section",{className:"recording-details",children:[r.jsx("h2",{children:"Recording"}),r.jsx("p",{children:a.video.title})]}),a.video.summary&&r.jsxs("section",{className:"recording-details",children:[r.jsx("h2",{children:"Summary"}),r.jsx("p",{children:a.video.summary})]}),a.chapters.length>0&&r.jsxs("section",{className:"recording-details",children:[r.jsx("h2",{children:"Chapters"}),r.jsx("div",{className:"chapter-list",children:a.chapters.map(($,S)=>r.jsxs(T,{size:"sm",variant:"ghost",onPress:()=>i($.timestamp),children:[r.jsx("span",{className:"timestamp",children:N($.timestamp)}),$.title]},S))})]})]})})]}),r.jsxs("form",{className:"composer",hidden:d!=="comments",onSubmit:$=>{$.preventDefault(),K()},children:[Z&&r.jsx(Zt,{children:Z}),r.jsxs(ke,{name:"author",value:u,onChange:v,isRequired:!0,isDisabled:b,children:[r.jsx(Ae,{children:"Name"}),r.jsx(jt,{maxLength:100,autoComplete:"name",placeholder:"Name"})]}),r.jsxs(ke,{name:"comment",value:m,onChange:$=>{g($),A($.trim()?p??t:null)},isRequired:!0,isDisabled:b,children:[r.jsx(Ae,{children:"Comment"}),r.jsx(i1,{ref:o,maxLength:2e3,placeholder:"Share your thoughts…",onKeyDown:$=>{($.metaKey||$.ctrlKey)&&$.key==="Enter"&&($.preventDefault(),K())}})]}),r.jsxs("div",{className:"composer-footer",children:[r.jsxs(T,{type:"button",size:"sm",variant:"ghost","aria-label":`Comment timestamp ${N(p??t)}. Use current video time`,onPress:()=>A(t),children:[r.jsx(Bt,{size:14}),r.jsx("span",{className:"timestamp",children:N(p??t)})]}),r.jsxs(T,{type:"submit",size:"sm",isPending:b,isDisabled:!u.trim()||!m.trim()||b,children:[r.jsx(qt,{size:15}),b?"Posting…":"Post"]})]})]})]})}const I1=[{emoji:"👍",label:"Like"},{emoji:"❤️",label:"Love"},{emoji:"😂",label:"Laugh"},{emoji:"😮",label:"Surprised"},{emoji:"🔥",label:"Fire"},{emoji:"👏",label:"Applause"}];function O1({data:a}){const t=e.useRef(null),n=e.useRef(null),[l,s]=e.useState(0),[i,o]=e.useState(a.video.duration),[d,c]=e.useState([]),[u,v]=e.useState([]),m=u.reduce((E,j)=>(E[j.emoji]=(E[j.emoji]||0)+1,E),{}),[g,p]=e.useState(null),[A,b]=e.useState("comments"),[h,x]=e.useState("watch"),V=e.useCallback(E=>{const j=t.current;j&&Number.isFinite(j.duration)&&(j.currentTime=Math.max(0,Math.min(j.duration,E)),s(j.currentTime))},[]);e.useEffect(()=>{let E=!1;return T1(a.shareCode).then(j=>{E||v(C=>[...j,...C])}).catch(()=>{}),()=>{E=!0}},[a.shareCode]);async function Z(E){if(g)return;p(E);const j=t.current?.currentTime||0;try{if(!await C1(a.shareCode,j,E))throw new Error;v(C=>[...C,{timestamp:j,emoji:E,created_at:new Date().toISOString()}]),ie.success(`${E} Reaction at ${N(j)}`)}catch{ie.danger("Could not add your reaction. Try again.")}finally{p(null)}}const{video:L}=a;return r.jsxs("div",{className:"recording-app","data-mobile-view":h,children:[h==="feedback"&&r.jsx("div",{className:"mobile-feedback-back",children:r.jsx(T,{size:"sm",variant:"secondary",onPress:()=>x("watch"),children:"Back to video"})}),r.jsxs("div",{className:"share-workspace",children:[r.jsxs("main",{className:"recording-column",children:[r.jsxs("div",{className:"recording-heading viewer-heading",children:[r.jsxs("div",{className:"viewer-identity",children:[r.jsx(Ee,{"aria-label":a.creator?.name||"Recording creator",className:"!size-[48px] shrink-0 !rounded-full bg-accent/15 text-accent",children:r.jsx(Ee.Fallback,{className:"!rounded-full bg-accent/15 text-xs font-medium text-accent",children:a.creator?.name.split(/\s+/).filter(Boolean).slice(0,2).map(E=>E[0]).join("").toUpperCase()||"?"})}),r.jsxs("div",{className:"viewer-caption",children:[r.jsx("h1",{className:"recording-title",title:L.title,children:L.title}),r.jsxs("div",{className:"recording-meta",children:[r.jsx("span",{children:ea(L.created_at)}),r.jsxs("span",{children:[L.view_count.toLocaleString()," ",L.view_count===1?"view":"views"]})]})]})]}),r.jsxs("div",{className:"viewer-heading-actions",children:[r.jsx(Qa,{}),r.jsx(e0,{time:l})]})]}),r.jsx(D1,{data:a,videoRef:t,comments:d,reactions:u,time:l,duration:i,onTime:s,onDuration:o,seek:V}),r.jsxs("div",{className:"reaction-rail",children:[r.jsx("div",{className:"reaction-buttons","aria-label":"Reactions",children:I1.map(({emoji:E,label:j})=>r.jsxs(T,{size:"sm",variant:"ghost","aria-label":`${j}${m[E]?`, ${m[E]} ${m[E]===1?"reaction":"reactions"}`:""}`,isDisabled:g!==null,onPress:()=>{Z(E)},children:[r.jsx("span",{"aria-hidden":"true",children:E}),m[E]>0&&r.jsx("span",{children:m[E]})]},E))}),r.jsxs(T,{className:"mobile-comment-button",size:"sm",variant:"primary",onPress:()=>{b("comments"),x("feedback"),requestAnimationFrame(()=>n.current?.focus({preventScroll:!0}))},children:[r.jsx(Le,{size:17}),"Comment"]})]})]}),r.jsx("aside",{"aria-label":"Recording feedback",children:r.jsx(K1,{data:a,time:l,duration:i,comments:d,setComments:c,seek:V,composerRef:n,selectedTab:A,setSelectedTab:b})})]})]})}function B1(){const[a,t]=e.useState(null),[n,l]=e.useState("loading"),[s,i]=e.useState("Protected recording"),[o,d]=e.useState(""),[c,u]=e.useState(!1),[v,m]=e.useState(""),g=e.useCallback(async()=>{l("loading");try{const A=location.pathname.split("/").filter(Boolean).pop()||"",b=await S1(A);L1(b)?(l("expired"),document.title="Recording unavailable — Recordly"):j1(b)?(l("password"),i(b.title),document.title=`${b.title} — Recordly`):Z1(b)?(t(b),l("recording"),document.title=`${b.video.title} — Recordly`):l("error")}catch{l("error")}},[]);e.useEffect(()=>{g()},[g]);async function p(){if(!c){u(!0),m("");try{const A=location.pathname.split("/").filter(Boolean).pop()||"";await H1(A,o)?await g():m("That password didn’t match. Please try again.")}catch{m("Could not unlock the recording. Please try again.")}finally{u(!1)}}}return r.jsxs(r.Fragment,{children:[r.jsx(Ya,{}),n==="loading"?r.jsx("div",{className:"recording-app",children:r.jsxs("div",{className:"share-workspace","aria-label":"Loading recording","aria-busy":"true",children:[r.jsxs("div",{className:"recording-column",children:[r.jsxs("div",{children:[r.jsx($e,{className:"loading-title"}),r.jsx($e,{className:"loading-meta"})]}),r.jsx($e,{className:"loading-video"})]}),r.jsx($e,{className:"loading-sidebar"})]})}):n==="recording"&&a?r.jsx(O1,{data:a}):r.jsx(Ga,{children:n==="password"?r.jsxs(r.Fragment,{children:[r.jsx(Ja,{size:28}),r.jsx("h1",{children:s}),r.jsx("p",{children:"This recording is password protected."}),r.jsxs("form",{className:"gate-form",onSubmit:A=>{A.preventDefault(),p()},children:[r.jsxs(ke,{value:o,onChange:d,isRequired:!0,isDisabled:c,children:[r.jsx(Ae,{children:"Password"}),r.jsx(jt,{type:"password",autoComplete:"current-password",autoFocus:!0})]}),v&&r.jsx(Zt,{children:v}),r.jsx(T,{type:"submit",isPending:c,isDisabled:!o||c,children:c?"Unlocking…":"Unlock recording"})]})]}):n==="expired"?r.jsxs(r.Fragment,{children:[r.jsx("h1",{children:"Recording unavailable"}),r.jsx("p",{children:"This link has expired or the recording has been removed. Ask the creator for a new link."})]}):r.jsxs(r.Fragment,{children:[r.jsx("h1",{children:"Couldn’t load the recording"}),r.jsx("p",{children:"Check your connection and try again."}),r.jsxs(T,{onPress:()=>{g()},children:[r.jsx(It,{size:16}),"Try again"]})]})})]})}export{B1 as default}; diff --git a/services/recordly-share/worker/web/dist/_astro/ShareUI.CpnBv7vU.js b/services/recordly-share/worker/web/dist/_astro/ShareUI.CpnBv7vU.js new file mode 100644 index 000000000..94d7a46e0 --- /dev/null +++ b/services/recordly-share/worker/web/dist/_astro/ShareUI.CpnBv7vU.js @@ -0,0 +1,12 @@ +import{r as a,$,a as Mc,g as Ac,b as Rc,R as Lc}from"./index.DeQQz02V.js";var fr={exports:{}},Dt={};var Oo;function Ic(){if(Oo)return Dt;Oo=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,o,i){var s=null;if(i!==void 0&&(s=""+i),o.key!==void 0&&(s=""+o.key),"key"in o){i={};for(var l in o)l!=="key"&&(i[l]=o[l])}else i=o;return o=i.ref,{$$typeof:t,type:r,key:s,ref:o!==void 0?o:null,props:i}}return Dt.Fragment=e,Dt.jsx=n,Dt.jsxs=n,Dt}var Vo;function Fc(){return Vo||(Vo=1,fr.exports=Ic()),fr.exports}var E=Fc();const Kc="react-aria-clear-focus",Dc="react-aria-focus";function Yn(t){if(typeof window>"u"||window.navigator==null)return!1;let e=window.navigator.userAgentData?.brands;return Array.isArray(e)&&e.some(n=>t.test(n.brand))||t.test(window.navigator.userAgent)}function no(t){return typeof window<"u"&&window.navigator!=null?t.test(window.navigator.userAgentData?.platform||window.navigator.platform):!1}function je(t){let e=null;return()=>(e==null&&(e=t()),e)}const Me=je(function(){return no(/^Mac/i)}),Nc=je(function(){return no(/^iPhone/i)}),us=je(function(){return no(/^iPad/i)||Me()&&navigator.maxTouchPoints>1}),gt=je(function(){return Nc()||us()}),Oc=je(function(){return Me()||gt()}),pt=je(function(){return Yn(/AppleWebKit/i)&&(gt()||!cs())}),cs=je(function(){return Yn(/Chrome|CriOS|CrMo/i)}),Kn=je(function(){return Yn(/Android/i)}),Vc=je(function(){return Yn(/(Firefox|FxiOS)/i)});function ce(t){if(Bc())t.focus({preventScroll:!0});else{let e=_c(t);t.focus(),Hc(e)}}let fn=null;function Bc(){if(fn==null){fn=!1;try{document.createElement("div").focus({get preventScroll(){return fn=!0,!0}})}catch{}}return fn}function _c(t){let e=t.parentNode,n=[],r=document.scrollingElement||document.documentElement;for(;e instanceof HTMLElement&&e!==r;)(e.offsetHeightt});function tn(){return a.useContext(jc)}function zc(t,e){let n=t.getAttribute("target");return(!n||n==="_self")&&t.origin===location.origin&&!t.hasAttribute("download")&&!e.metaKey&&!e.ctrlKey&&!e.altKey&&!e.shiftKey}function Be(t,e,n=!0){let{metaKey:r,ctrlKey:o,altKey:i,shiftKey:s}=e;!pt()&&Vc()&&window.event?.type?.startsWith("key")&&t.target==="_blank"&&(Me()?r=!0:o=!0);let l=pt()&&Me()&&!us()?new KeyboardEvent("keydown",{keyIdentifier:"Enter",metaKey:r,ctrlKey:o,altKey:i,shiftKey:s}):new MouseEvent("click",{metaKey:r,ctrlKey:o,altKey:i,shiftKey:s,detail:1,bubbles:!0,cancelable:!0});Be.isOpening=n,ce(t),t.dispatchEvent(l),Be.isOpening=!1}Be.isOpening=!1;function Uc(t,e){if(t instanceof HTMLAnchorElement)e(t);else if(t.hasAttribute("data-href")){let n=document.createElement("a");n.href=t.getAttribute("data-href"),t.hasAttribute("data-target")&&(n.target=t.getAttribute("data-target")),t.hasAttribute("data-rel")&&(n.rel=t.getAttribute("data-rel")),t.hasAttribute("data-download")&&(n.download=t.getAttribute("data-download")),t.hasAttribute("data-ping")&&(n.ping=t.getAttribute("data-ping")),t.hasAttribute("data-referrer-policy")&&(n.referrerPolicy=t.getAttribute("data-referrer-policy")),t.appendChild(n),e(n),t.removeChild(n)}}function Zc(t,e){Uc(t,n=>Be(n,e))}function ds(t){const n=tn().useHref(t?.href??"");let r={};if(t)for(let o of["href","target","rel","download","ping","referrerPolicy"])o in t&&t[o]!==void 0&&(r[o]=o==="href"?n:t[o]);return r}function fs(t,e,n,r){!e.isNative&&t.currentTarget instanceof HTMLAnchorElement&&t.currentTarget.href&&!t.isDefaultPrevented()&&zc(t.currentTarget,t)&&n&&(t.preventDefault(),e.open(t.currentTarget,t,n,r))}const J=typeof document<"u"?$.useLayoutEffect:()=>{},ps={prefix:String(Math.round(Math.random()*1e10)),current:0},hs=$.createContext(ps),Wc=$.createContext(!1);let pr=new WeakMap;function Gc(t=!1){let e=a.useContext(hs),n=a.useRef(null);if(n.current===null&&!t){let r=$.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner?.current;if(r){let o=pr.get(r);o==null?pr.set(r,{id:e.current,state:r.memoizedState}):r.memoizedState!==o.state&&(e.current=o.id,pr.delete(r))}n.current=++e.current}return n.current}function qc(t){let e=a.useContext(hs),n=Gc(!!t),r=`react-aria${e.prefix}`;return t||`${r}-${n}`}function Yc(t){let e=$.useId(),[n]=a.useState(nt()),r=n?"react-aria":`react-aria${ps.prefix}`;return t||`${r}-${e}`}const Xc=typeof $.useId=="function"?Yc:qc;function Jc(){return!1}function Qc(){return!0}function ed(t){return()=>{}}function nt(){return typeof $.useSyncExternalStore=="function"?$.useSyncExternalStore(ed,Jc,Qc):a.useContext(Wc)}function td(t){let[e,n]=a.useState(t),r=a.useRef(e),o=a.useRef(null),i=a.useRef(()=>{if(!o.current)return;let l=o.current.next();if(l.done){o.current=null;return}r.current===l.value?i.current():n(l.value)});J(()=>{r.current=e,o.current&&i.current()});let s=a.useCallback(l=>{o.current=l(r.current),i.current()},[i]);return[e,s]}let nd=!!(typeof window<"u"&&window.document&&window.document.createElement),St=new Map,Et;typeof FinalizationRegistry<"u"&&(Et=new FinalizationRegistry(t=>{St.delete(t)}));let hr=new WeakMap;function xe(t){let[e,n]=a.useState(t),r=a.useRef(null),o=Xc(e),i=a.useRef(null),s=hr.get(i);if(Et&&s!==o&&(s!=null&&Et.unregister(i),Et.register(i,o,i),hr.set(i,o)),nd){const l=St.get(o);l&&!l.includes(r)?l.push(r):St.set(o,[r])}return J(()=>{let l=o;return()=>{Et&&(Et.unregister(i),hr.delete(i)),St.delete(l)}},[o]),a.useEffect(()=>{let l=r.current;return l&&n(l),()=>{l&&(r.current=null)}}),o}function rd(t,e){if(t===e)return t;let n=St.get(t);if(n)return n.forEach(o=>o.current=e),e;let r=St.get(e);return r?(r.forEach(o=>o.current=t),t):e}function Ye(t=[]){let e=xe(),[n,r]=td(e),o=a.useCallback(()=>{r(function*(){yield e,yield document.getElementById(e)?e:void 0})},[e,r]);return J(o,[e,o,...t]),n}function ht(...t){return(...e)=>{for(let n of t)typeof n=="function"&&n(...e)}}const ee=t=>od(t)?t.document:id(t)?t:t?.ownerDocument??(typeof document<"u"?document:void 0),he=t=>ee(t)?.defaultView??(typeof window<"u"?window:void 0);function ms(t){return t!==null&&typeof t=="object"&&"nodeType"in t&&typeof t.nodeType=="number"}function od(t){return typeof t=="object"&&t!=null&&"window"in t&&t.window===t}function id(t){return ms(t)&&t.nodeType===9}function Xn(t){return ms(t)&&t.nodeType===11&&"host"in t}function Pt(t,e,n,r){if(n==null||t==null)return()=>{};let o=Array.isArray(t)?t:[t];for(let i of o)i.addEventListener(e,n,r);return()=>{for(let i of o)i.removeEventListener(e,n,r)}}function kn(t,e,n,r){if(t==null)return()=>{};let o=new Array,i=Array.isArray(t)?t:[t];for(let s of i){let l=s.style.getPropertyValue(e),u=s.style.getPropertyPriority(e);s.style.setProperty(e,n,r),o.unshift(()=>{l?s.style.setProperty(e,l,u):s.style.removeProperty(e)})}return()=>{for(let s of o)s()}}let sd=!1;function Ne(){return sd}function Y(t,e){if(!Ne())return e&&t?t.contains(e):!1;if(!t||!e)return!1;let n=e;for(;n!==null;){if(n===t)return!0;typeof n.assignedElements!="function"&&n.assignedSlot?.parentNode?n=n.assignedSlot.parentNode:Xn(n)?n=n.host:n=n.parentNode}return!1}const re=(t=document)=>{if(!Ne())return t.activeElement;let e=t.activeElement;for(;e&&"shadowRoot"in e&&e.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return e};function V(t){if(Ne()&&t.target instanceof Element&&t.target.shadowRoot){if("composedPath"in t)return t.composedPath()[0]??null;if("composedPath"in t.nativeEvent)return t.nativeEvent.composedPath()[0]??null}return t.target}function bs(t,e){if(e===null)return[];e=e??he(t);let n=[e];if(!Ne()||!t||t===e)return n;let r="getRootNode"in e?e.getRootNode():null,o=t.getRootNode()??null;for(;Xn(o)&&o!==r;)n.push(o),o=o.host.getRootNode();return n}function Xe(t){if(!t)return!1;let e=t.getRootNode(),n=he(t);if(!(e instanceof n.Document||e instanceof n.ShadowRoot))return!1;let r=e.activeElement;return r!=null&&t.contains(r)}class ld{constructor(e,n,r,o){this._walkerStack=[],this._currentSetFor=new Set,this._acceptNode=s=>{if(s.nodeType===Node.ELEMENT_NODE){const l=s.shadowRoot;if(l){const u=this._doc.createTreeWalker(l,this.whatToShow,{acceptNode:this._acceptNode});return this._walkerStack.unshift(u),NodeFilter.FILTER_ACCEPT}else{if(typeof this.filter=="function")return this.filter(s);if(this.filter?.acceptNode)return this.filter.acceptNode(s);if(this.filter===null)return NodeFilter.FILTER_ACCEPT}}return NodeFilter.FILTER_SKIP},this._doc=e,this.root=n,this.filter=o??null,this.whatToShow=r??NodeFilter.SHOW_ALL,this._currentNode=n,this._walkerStack.unshift(e.createTreeWalker(n,r,this._acceptNode));const i=n.shadowRoot;if(i){const s=this._doc.createTreeWalker(i,this.whatToShow,{acceptNode:this._acceptNode});this._walkerStack.unshift(s)}}get currentNode(){return this._currentNode}set currentNode(e){if(!Y(this.root,e))throw new Error("Cannot set currentNode to a node that is not contained by the root node.");const n=[];let r=e,o=e;for(this._currentNode=e;r&&r!==this.root;)if(r.nodeType===Node.DOCUMENT_FRAGMENT_NODE){const s=r,l=this._doc.createTreeWalker(s,this.whatToShow,{acceptNode:this._acceptNode});n.push(l),l.currentNode=o,this._currentSetFor.add(l),r=o=s.host}else r=r.parentNode;const i=this._doc.createTreeWalker(this.root,this.whatToShow,{acceptNode:this._acceptNode});n.push(i),i.currentNode=o,this._currentSetFor.add(i),this._walkerStack=n}get doc(){return this._doc}firstChild(){let e=this.currentNode,n=this.nextNode();return Y(e,n)?(n&&(this.currentNode=n),n):(this.currentNode=e,null)}lastChild(){let n=this._walkerStack[0].lastChild();return n&&(this.currentNode=n),n}nextNode(){const e=this._walkerStack[0].nextNode();if(e){if(e.shadowRoot){let r;if(typeof this.filter=="function"?r=this.filter(e):this.filter?.acceptNode&&(r=this.filter.acceptNode(e)),r===NodeFilter.FILTER_ACCEPT)return this.currentNode=e,e;let o=this.nextNode();return o&&(this.currentNode=o),o}return e&&(this.currentNode=e),e}else if(this._walkerStack.length>1){this._walkerStack.shift();let n=this.nextNode();return n&&(this.currentNode=n),n}else return null}previousNode(){const e=this._walkerStack[0];if(e.currentNode===e.root){if(this._currentSetFor.has(e))if(this._currentSetFor.delete(e),this._walkerStack.length>1){this._walkerStack.shift();let r=this.previousNode();return r&&(this.currentNode=r),r}else return null;return null}const n=e.previousNode();if(n){if(n.shadowRoot){let o;if(typeof this.filter=="function"?o=this.filter(n):this.filter?.acceptNode&&(o=this.filter.acceptNode(n)),o===NodeFilter.FILTER_ACCEPT)return n&&(this.currentNode=n),n;let i=this.lastChild();return i&&(this.currentNode=i),i}return n&&(this.currentNode=n),n}else if(this._walkerStack.length>1){this._walkerStack.shift();let r=this.previousNode();return r&&(this.currentNode=r),r}else return null}nextSibling(){return null}previousSibling(){return null}parentNode(){return null}}function gs(t,e,n,r){return Ne()?new ld(t,e,n,r):t.createTreeWalker(e,n,r)}function nn(...t){return t.length===1&&t[0]?t[0]:e=>{let n=!1;const r=t.map(o=>{const i=Bo(o,e);return n||=typeof i=="function",i});if(n)return()=>{r.forEach((o,i)=>{typeof o=="function"?o():Bo(t[i],null)})}}}function Bo(t,e){if(typeof t=="function")return t(e);t!=null&&(t.current=e)}function vs(t){var e,n,r="";if(typeof t=="string"||typeof t=="number")r+=t;else if(typeof t=="object")if(Array.isArray(t)){var o=t.length;for(e=0;e=65&&o.charCodeAt(2)<=90?e[o]=ht(i,s):(o==="className"||o==="UNSAFE_className")&&typeof i=="string"&&typeof s=="string"?e[o]=ad(i,s):o==="id"&&i&&s?e.id=rd(i,s):o==="ref"&&i&&s?e.ref=nn(i,s):e[o]=s!==void 0?s:i}}return e}const ud=new Set(["id"]),cd=new Set(["aria-label","aria-labelledby","aria-describedby","aria-details"]),dd=new Set(["href","hrefLang","target","rel","download","ping","referrerPolicy"]),fd=new Set(["dir","lang","hidden","inert","translate"]),_o=new Set(["onClick","onAuxClick","onContextMenu","onDoubleClick","onMouseDown","onMouseEnter","onMouseLeave","onMouseMove","onMouseOut","onMouseOver","onMouseUp","onTouchCancel","onTouchEnd","onTouchMove","onTouchStart","onPointerDown","onPointerMove","onPointerUp","onPointerCancel","onPointerEnter","onPointerLeave","onPointerOver","onPointerOut","onGotPointerCapture","onLostPointerCapture","onScroll","onWheel","onAnimationStart","onAnimationEnd","onAnimationIteration","onTransitionCancel","onTransitionEnd","onTransitionRun","onTransitionStart"]),pd=/^(data-.*)$/;function de(t,e={}){let{labelable:n,isLink:r,global:o,events:i=o,propNames:s}=e,l={};for(const u in t)Object.prototype.hasOwnProperty.call(t,u)&&(ud.has(u)||n&&cd.has(u)||r&&dd.has(u)||o&&fd.has(u)||i&&(_o.has(u)||u.endsWith("Capture")&&_o.has(u.slice(0,-7)))||s?.has(u)||pd.test(u))&&(l[u]=t[u]);return l}let We=new Map,Kr=new Set;function Ho(){if(typeof window>"u")return;function t(r){return"propertyName"in r}let e=r=>{let o=V(r);if(!t(r)||!o)return;let i=We.get(o);i||(i=new Set,We.set(o,i),o.addEventListener("transitioncancel",n,{once:!0})),i.add(r.propertyName)},n=r=>{let o=V(r);if(!t(r)||!o)return;let i=We.get(o);if(i&&(i.delete(r.propertyName),i.size===0&&(o.removeEventListener("transitioncancel",n),We.delete(o)),We.size===0)){for(let s of Kr)s();Kr.clear()}};document.body.addEventListener("transitionrun",e),document.body.addEventListener("transitionend",n)}typeof document<"u"&&(document.readyState!=="loading"?Ho():document.addEventListener("DOMContentLoaded",Ho));function hd(){for(const[t]of We)"isConnected"in t&&!t.isConnected&&We.delete(t)}function ys(t){requestAnimationFrame(()=>{hd(),We.size===0?t():Kr.add(t)})}function Jn(){let t=a.useRef(new Map),e=a.useCallback((o,i,s,l)=>{let u=l?.once?(...c)=>{t.current.delete(s),s(...c)}:s;t.current.set(s,{type:i,eventTarget:o,fn:u,options:l}),o.addEventListener(i,u,l)},[]),n=a.useCallback((o,i,s,l)=>{let u=t.current.get(s)?.fn||s;o.removeEventListener(i,u,l),t.current.delete(s)},[]),r=a.useCallback(()=>{t.current.forEach((o,i)=>{n(o.eventTarget,o.type,i,o.options)})},[n]);return a.useEffect(()=>r,[r]),{addGlobalListener:e,removeGlobalListener:n,removeAllGlobalListeners:r}}function $s(t,e){let{id:n,"aria-label":r,"aria-labelledby":o}=t;return n=xe(n),o&&r?o=[...new Set([n,...o.trim().split(/\s+/)])].join(" "):o&&(o=o.trim().split(/\s+/).join(" ")),!r&&!o&&e&&(r=e),{id:n,"aria-label":r,"aria-labelledby":o}}function rt(t){const e=a.useRef(null),n=a.useRef(void 0),r=a.useCallback(o=>{if(typeof t=="function"){const i=t,s=i(o);return()=>{typeof s=="function"?s():i(null)}}else if(t)return t.current=o,()=>{t.current=null}},[t]);return a.useMemo(()=>({get current(){return e.current},set current(o){e.current=o,n.current&&(n.current(),n.current=void 0),o!=null&&(n.current=r(o))}}),[r])}const md=$.useInsertionEffect??J;function Ce(t){const e=a.useRef(null);return md(()=>{e.current=t},[t]),a.useCallback((...n)=>{const r=e.current;return r?.(...n)},[])}function jo(t,e){const n=a.useRef(!0),r=a.useRef(null);J(()=>(n.current=!0,()=>{n.current=!1}),[]),J(()=>{n.current?n.current=!1:(!r.current||e.some((o,i)=>!Object.is(o,r[i])))&&t(),r.current=e},e)}function bd(){return typeof window.ResizeObserver<"u"}function Dn(t){const{ref:e,box:n,onResize:r}=t;let o=Ce(r);a.useEffect(()=>{let i=e?.current;if(i)if(bd()){const s=new window.ResizeObserver(l=>{l.length&&o()});return s.observe(i,{box:n}),()=>{i&&s.unobserve(i)}}else return window.addEventListener("resize",o,!1),()=>{window.removeEventListener("resize",o,!1)}},[e,n])}function ro(t,e){J(()=>{if(t&&t.ref&&e)return t.ref.current=e.current,()=>{t.ref&&(t.ref.current=null)}})}function Mt(t,e){if(!t)return!1;let n=window.getComputedStyle(t),r=document.scrollingElement||document.documentElement,o=/(auto|scroll)/.test(n.overflow+n.overflowX+n.overflowY);return t===r&&n.overflow!=="hidden"&&(o=!0),o&&e&&(o=t.scrollHeight!==t.clientHeight||t.scrollWidth!==t.clientWidth),o}function oo(t,e){let n=t;for(Mt(n,e)&&(n=n.parentElement);n&&!Mt(n,e);)n=n.parentElement;return n||document.scrollingElement||document.documentElement}function mr(t,e){let n=[],r=document.scrollingElement||document.documentElement;for(;t&&(Mt(t,e)&&n.push(t),t!==r);)t=t.parentElement;return n}function Bt(t){return Me()?t.metaKey:t.ctrlKey}const gd=new Set(["checkbox","radio","range","color","file","image","button","submit","reset"]);function br(t){return t instanceof HTMLInputElement&&!gd.has(t.type)||t instanceof HTMLTextAreaElement||t instanceof HTMLElement&&t.isContentEditable}let vd=0;const gr=new Map;function yd(t){let[e,n]=a.useState();return J(()=>{if(!t)return;let r=gr.get(t);if(r)n(r.element.id);else{let o=`react-aria-description-${vd++}`;n(o);let i=document.createElement("div");i.id=o,i.style.display="none",i.textContent=t,document.body.appendChild(i),r={refCount:0,element:i},gr.set(t,r)}return r.refCount++,()=>{r&&--r.refCount===0&&(r.element.remove(),gr.delete(t))}},[t]),{"aria-describedby":t?e:void 0}}function _t(t,e,n,r){let o=Ce(n),i=n==null;a.useEffect(()=>{if(!(i||t.current==null))return Pt(t.current,e,o,r)},[t,e,r,i])}function Mn(t,e,n={}){if(t===e)return;let r=e.getBoundingClientRect();$d(t,e,r,n)}function $d(t,e,n,r={}){let{block:o="nearest",inline:i="nearest"}=r,s=t.scrollTop,l=t.scrollLeft,u=t.getBoundingClientRect(),c=window.getComputedStyle(e),d=window.getComputedStyle(t),f=document.scrollingElement||document.documentElement,p=t===f,m=t===f?0:u.top,h=t===f?t.clientHeight:u.bottom,g=t===f?0:u.left,b=t===f?t.clientWidth:u.right,y=parseFloat(c.scrollMarginTop)||0,v=parseFloat(c.scrollMarginBottom)||0,w=parseFloat(c.scrollMarginLeft)||0,x=parseFloat(c.scrollMarginRight)||0,F=parseFloat(d.scrollPaddingTop)||0,K=parseFloat(d.scrollPaddingBottom)||0,P=parseFloat(d.scrollPaddingLeft)||0,S=parseFloat(d.scrollPaddingRight)||0,I=parseFloat(d.borderTopWidth)||0,M=parseFloat(d.borderBottomWidth)||0,O=parseFloat(d.borderLeftWidth)||0,z=parseFloat(d.borderRightWidth)||0,U=n.top-y,C=n.bottom+v,R=n.left-w,H=n.right+x,L=t===f?0:O+z,q=t===f?0:I+M,T=t===f?0:t.offsetWidth-t.clientWidth-L,A=t===f?0:t.offsetHeight-t.clientHeight-q,j=m+(p?0:I)+F,W=h-(p?0:M)-K-A,ne=g+(p?0:O)+P,te=b-(p?0:z)-S;gt()&&pt()||d.direction==="ltr"?te-=T:d.direction==="rtl"&&(ne+=T);let k=UW,Z=Rte;if(k&&o==="start")s+=U-j;else if(k&&o==="center")s+=(U+C)/2-(j+W)/2;else if(k&&o==="end")s+=C-W;else if(k&&o==="nearest"){let oe=U-j,ye=C-W;s+=Math.abs(oe)<=Math.abs(ye)?oe:ye}if(Z&&i==="start")l+=R-ne;else if(Z&&i==="center")l+=(R+H)/2-(ne+te)/2;else if(Z&&i==="end")l+=H-te;else if(Z&&i==="nearest"){let oe=R-ne,ye=H-te;l+=Math.abs(oe)<=Math.abs(ye)?oe:ye}t.scrollTo({left:l,top:s})}function zo(t,e={}){let{containingElement:n}=e;if(t&&t.isConnected){let r=document.scrollingElement||document.documentElement;if(window.getComputedStyle(r).overflow==="hidden"){let{left:i,top:s}=t.getBoundingClientRect(),l=mr(t,!0);for(let d of l)Mn(d,t);let{left:u,top:c}=t.getBoundingClientRect();if(Math.abs(i-u)>1||Math.abs(s-c)>1){l=n?mr(n,!0):[];for(let d of l)Mn(d,n,{block:"center",inline:"center"});for(let d of mr(t,!0))Mn(d,t)}}else{let{left:i,top:s}=t.getBoundingClientRect();t?.scrollIntoView?.({block:"nearest"});let{left:l,top:u}=t.getBoundingClientRect();(Math.abs(i-l)>1||Math.abs(s-u)>1)&&(n?.scrollIntoView?.({block:"center",inline:"center"}),t.scrollIntoView?.({block:"nearest"}))}}}function xs(t){return t.pointerType===""&&t.isTrusted?!0:Kn()&&t.pointerType?t.type==="click"&&t.buttons===1:t.detail===0&&!t.pointerType}function xd(t){return!Kn()&&t.width===0&&t.height===0||Kn()&&t.width===1&&t.height===1&&t.pressure===0&&t.detail===0&&t.pointerType==="mouse"}function Ed(t,e,n){let r=Ce(o=>{n&&!o.defaultPrevented&&n(e)});a.useEffect(()=>{let o=t?.current?.form;return o?.addEventListener("reset",r),()=>{o?.removeEventListener("reset",r)}},[t])}function wd(t,e){let{collection:n,onLoadMore:r,scrollOffset:o=1,direction:i="end"}=t,s=a.useRef(null),l=Ce(u=>{for(let c of u)c.isIntersecting&&r&&r()});J(()=>{if(e.current){const u=100*o,c=i==="start"?`${u}% 0px 0px 0px`:`0px ${u}% ${u}% ${u}%`;s.current=new IntersectionObserver(l,{root:oo(e?.current),rootMargin:c}),s.current.observe(e.current)}return()=>{s.current&&s.current.disconnect()}},[n,e,o,i])}function Cd(t){const e=a.version.split(".");return parseInt(e[0],10)>=19?t:t?"true":void 0}var Qe=Mc();const Sd=Ac(Qe);function Pd(t,e=!0){let[n,r]=a.useState(!0),o=n&&e;return J(()=>{if(o&&t.current&&"getAnimations"in t.current)for(let i of t.current.getAnimations())i instanceof CSSTransition&&i.cancel()},[t,o]),Es(t,o,a.useCallback(()=>r(!1),[])),o}function Td(t,e){let[n,r]=a.useState(e?"open":"closed");switch(n){case"open":e||r("exiting");break;case"closed":case"exiting":e&&r("open");break}let o=n==="exiting";return Es(t,o,a.useCallback(()=>{r(i=>i==="exiting"?"closed":i)},[])),o}function Es(t,e,n){J(()=>{if(e&&t.current){if(!("getAnimations"in t.current)){n();return}let r=t.current.getAnimations();if(r.length===0){n();return}let o=!1;return Promise.allSettled(r.map(i=>i.finished)).then(()=>{o||Qe.flushSync(()=>{n()})}),()=>{o=!0}}},[t,e,n])}const kd=typeof Element<"u"&&"checkVisibility"in Element.prototype;function Md(t){const e=he(t);if(!(t instanceof e.HTMLElement)&&!(t instanceof e.SVGElement))return!1;let{display:n,visibility:r}=t.style,o=n!=="none"&&r!=="hidden"&&r!=="collapse";if(o){const{getComputedStyle:i}=he(t);let{display:s,visibility:l}=i(t);o=s!=="none"&&l!=="hidden"&&l!=="collapse"}return o}function Ad(t,e){return!t.hasAttribute("hidden")&&!t.hasAttribute("data-react-aria-prevent-focus")&&(t.nodeName==="DETAILS"&&e&&e.nodeName!=="SUMMARY"?t.hasAttribute("open"):!0)}function io(t,e){return kd?t.checkVisibility({visibilityProperty:!0})&&!t.closest("[data-react-aria-prevent-focus]"):t.nodeName!=="#comment"&&Md(t)&&Ad(t,e)&&(!t.parentElement||io(t.parentElement,t))}const so=["input:not([disabled]):not([type=hidden])","select:not([disabled])","textarea:not([disabled])","button:not([disabled])","a[href]","area[href]","summary","iframe","object","embed","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable^="false"])',"permission"],Rd=so.join(":not([hidden]),")+",[tabindex]:not([disabled]):not([hidden])";so.push('[tabindex]:not([tabindex="-1"]):not([disabled])');const Ld=so.join(':not([hidden]):not([tabindex="-1"]),');function ws(t,e){return t.matches(Rd)&&!Cs(t)&&(e?.skipVisibilityCheck||io(t))}function lo(t){return t.matches(Ld)&&io(t)&&!Cs(t)}function Cs(t){let e=t;for(;e!=null;){if(e instanceof he(e).HTMLElement&&e.inert)return!0;e=e.parentElement}return!1}function Id(t,e){let n=he(e),r=ee(e);if(r==null||n==null)return;let o,i=`meta[name="${CSS.escape(t)}"], meta[property="${CSS.escape(t)}"]`,s=r.querySelector(i);return s&&s instanceof n.HTMLMetaElement&&(t==="csp-nonce"&&s.nonce&&(o??=s.nonce||void 0),s.content&&(o??=s.content||void 0)),t==="csp-nonce"&&(o??=n.__webpack_nonce__||globalThis.__webpack_nonce__||void 0),o}let Uo=new WeakMap;function Ss(t){let e=ee(t),n=Uo.get(e);return n??=Id("csp-nonce",e),n!==void 0&&Uo.set(e,n),n}function Wt(t,e=-1/0,n=1/0){return Math.min(Math.max(t,e),n)}function pn(t,e){let n=t,r=0,o=e.toString(),i=o.toLowerCase().indexOf("e-");if(i>0)r=Math.abs(Math.floor(Math.log10(Math.abs(e))))+i;else{let s=o.indexOf(".");s>=0&&(r=o.length-s)}if(r>0){let s=Math.pow(10,r);n=Math.round(n*s)/s}return n}function Y1(t,e,n,r){e=Number(e),n=Number(n);let o=(t-(isNaN(e)?0:e))%r,i=pn(Math.abs(o)*2>=r?t+Math.sign(o)*(r-Math.abs(o)):t-o,r);return isNaN(e)?!isNaN(n)&&i>n&&(i=Math.floor(pn(n/r,r))*r):in&&(i=e+Math.floor(pn((n-e)/r,r))*r),i=pn(i,r),i}function Fd(t,e){const{ref:n,render:r,...o}=e,i=a.useRef(null),s=a.useMemo(()=>nn(n,i),[n,i]);J(()=>{},[t,r]);const l={...o,ref:s};return r?r(l,void 0):E.jsx(t,{...l})}const Zo={},ge=new Proxy({},{get(t,e){if(typeof e!="string")return;let n=Zo[e];return n||(n=Fd.bind(null,e),Zo[e]=n),n}});var Kd=Array.isArray,Qn=t=>{if(!t&&t!==0&&t!==0n)return"";if(typeof t=="string")return t;if(typeof t=="number")return t!==t?"":""+t;if(typeof t=="bigint")return""+t;let e="";if(Kd(t)){const n=t.length;for(let r=0;rtypeof t!="string"||!t?t:t.replace(Dd," ").trim(),Od=t=>{const e=t.length;if(e===0)return!1;const n=t.charCodeAt(0),r=t.charCodeAt(e-1);if(n===32||r===32||n>=9&&n<=13||n===160||r>=9&&r<=13||r===160)return!0;for(let o=0;o=9&&i<=13||i===160||i===32&&o+1{const e=Qn(t);if(e)return Od(e)?Nd(e):e},Nr=t=>t===!1?"false":t===!0?"true":t===0?"0":t,Pe=t=>{if(!t||typeof t!="object")return!0;for(const e in t)return!1;return!0},Ps=(t,e)=>{if(t===e)return!0;if(!t||!e)return!1;const n=t,r=e,o=Object.keys(n),i=Object.keys(r);if(o.length!==i.length)return!1;for(let s=0;s{const n=t;for(const r in e)if(Object.hasOwn(e,r)){const o=e[r];r in n?n[r]=zt(n[r],o):n[r]=o}return t},Ts=(t,e)=>{for(let n=0;n{const e=[];Ts(t,e);const n=[];for(let r=0;r{const n=t,r=e,o={};for(const i in n){const s=n[i];if(i in r){const l=r[i];Dr(s)||Dr(l)?o[i]=Or(l,s):typeof s=="object"&&typeof l=="object"&&s&&l?o[i]=ao(s,l):o[i]=l+" "+s}else o[i]=s}for(const i in r)i in n||(o[i]=r[i]);return o},Bd={twMerge:!0,twMergeConfig:{}},ks=256,_d=128,mt=Symbol("tv-cache-miss"),Hd=t=>t?.class!=null&&t.class!==""||t?.className!=null&&t.className!=="",Wo=t=>{if(t===void 0)return"";if(t===null)return"null";if(typeof t=="string")return t;if(typeof t=="boolean")return t?"true":"false";if(typeof t=="number")return t===0?"0":String(t);if(typeof t=="bigint")return String(t);const e=Nr(t),n=typeof e;if(n==="string"||n==="number"||n==="boolean"||n==="bigint")return String(e);if(n==="object")try{return JSON.stringify(e)}catch{return null}return null},$t=(t,e)=>{if(e===void 0)return t;if(e===null)return t+"null";const n=typeof e;if(n==="string"||n==="number"||n==="boolean"||n==="bigint")return t+String(e);if(Array.isArray(e))return t+e.join("\0");try{return t+JSON.stringify(e)}catch{return t+"?"}},Ms=(t,e,n,r)=>{let o="";const i=Object.create(null);for(let l=0;l1&&s.sort();for(let l=0;l{let n="";for(let r=0;r{let e=new Map,n=null;return{get(r){if(e.has(r))return e.get(r);if(n?.has(r)){const o=n.get(r);return e.set(r,o),o}return mt},set(r,o){e.size>=t&&(n=e,e=new Map),e.set(r,o)}}},jd=(t=ks)=>{const e=Rs(t);return{get(n){return e.get(n)},set(n,r){e.set(n,r)}}},zd=(t=_d)=>{let e=new Map,n=null,r=0;return{get(o,i){const s=e.get(o);if(s){const l=s.get(i);if(l!==void 0||s.has(i))return l}if(n){const l=n.get(o);if(l){const u=l.get(i);if(u!==void 0||l.has(i)){let c=e.get(o);return c||(c=new Map,e.set(o,c)),c.has(i)||r++,c.set(i,u),u}}}return mt},set(o,i,s){r>=t&&(n=e,e=new Map,r=0);let l=e.get(o);l||(l=new Map,e.set(o,l)),l.has(i)||r++,l.set(i,s)}}},uo=(t,e)=>{let n=null;return(r,o)=>{if(!Hd(o))return r;const i=o.class,s=o.className;if(i!=null&&i!==""&&typeof i!="string"||s!=null&&s!==""&&typeof s!="string")return t(e,r,i,s);n??=zd();const l=r??"",u=(typeof i=="string"?i:"")+"\0"+(typeof s=="string"?s:""),c=n.get(l,u);if(c!==mt)return c;const d=t(e,r,i,s);return n.set(l,u,d),d}};function Ud(){let t=null,e={},n=!1;return{get cachedTwMerge(){return t},set cachedTwMerge(r){t=r},get cachedTwMergeConfig(){return e},set cachedTwMergeConfig(r){e=r},get didTwMergeConfigChange(){return n},set didTwMergeConfigChange(r){n=r},reset(){t=null,e={},n=!1}}}var $e=Ud(),Zd=t=>{!Pe(t.twMergeConfig)&&!Ps(t.twMergeConfig,$e.cachedTwMergeConfig)&&($e.didTwMergeConfigChange=!0,$e.cachedTwMergeConfig=t.twMergeConfig)},Wd=(t,e)=>{const n=[];for(let r=0;r{if(!Array.isArray(t)||t.length===0)return[];const e=[];for(let n=0;n{if(!Array.isArray(t)||t.length===0)return[];const e=[];for(let n=0;n{const e={};for(let n=0;n{const{extend:n=null,slots:r={},variants:o={},compoundVariants:i=[],compoundSlots:s=[],defaultVariants:l={}}=t,u={...Bd,...e},c=t.slots!==void 0,d=n?.base?zt(n.base,t?.base):t?.base,f=n?.variants&&!Pe(n.variants)?ao(o,n.variants):o,p=n?.defaultVariants&&!Pe(n.defaultVariants)?{...n.defaultVariants,...l}:l;Zd(u);const m=!n?.slots||Pe(n.slots),h=c?m&&n?.base?zt(t?.base,n.base):typeof t?.base=="string"||t?.base==null?t.base:zt(t.base):void 0,g=c?{base:h,...r}:{},b=m?g:Vd({...n?.slots},Pe(g)?{base:t?.base}:g),y=!n?.compoundVariants||Pe(n.compoundVariants)?i:Or(n?.compoundVariants,i),v=!n?.compoundSlots||Pe(n.compoundSlots)?s:Or(n?.compoundSlots,s),w=Object.keys(f),x=y&&!Array.isArray(y)?new TypeError(`The "compoundVariants" prop must be an array. Received: ${typeof y}`):v&&!Array.isArray(v)?new TypeError(`The "compoundSlots" prop must be an array. Received: ${typeof v}`):null,F=c||!m?"slots":w.length===0?"plain":"variants";return{config:u,extend:n,base:d,variants:f,defaultVariants:p,slots:b,compoundVariants:y,compoundSlots:v,compiledVariants:null,compiledCompoundVariants:null,compiledCompoundSlots:null,compiledCompoundSlotsBySlot:null,deferredError:x,mode:F,slotKeys:null,variantKeys:w}},Ls=t=>(t.compiledVariants!==null||(t.compiledVariants=Wd(t.variants,t.variantKeys),t.compiledCompoundVariants=Gd(t.compoundVariants),t.compiledCompoundSlots=qd(t.compoundSlots),t.compiledCompoundSlotsBySlot=Yd(t.compiledCompoundSlots),t.slotKeys=t.slots&&typeof t.slots=="object"?Object.keys(t.slots):[]),t),Vr=[],Is=[],Jd=[],Qd=[],ef=[],Fs=(t,e,n)=>{const r={};for(const o in t)r[o]=t[o];if(e)for(const o in e)e[o]!==void 0&&(r[o]=e[o]);if(n)for(const o in n)n[o]!==void 0&&(r[o]=n[o]);return r},hn=t=>t==null||t===!1,tf=(t,e)=>{if(!Array.isArray(t))return t===e||hn(t)&&hn(e);for(let n=0;n{if(t.isEmpty)return null;const o=r?.[t.key]??n?.[t.key];if(o===null)return null;const i=Nr(o);if(typeof i=="object")return null;const s=e?.[t.key],l=i??Nr(s);return t.values[l||"false"]},co=(t,e)=>{const{conditionKeys:n,source:r}=t;for(let o=0;o{typeof n=="string"?e==="base"&&t.push(n):n&&typeof n=="object"&&n[e]&&t.push(n[e])},nf=(t,e,n)=>{const r=Is;r.length=0;for(let o=0;o{const i=Is;i.length=0;for(let s=0;s{const n=Jd;n.length=0;for(let r=0;r{const r=Qd;r.length=0;for(let o=0;o{const n=ef;n.length=0;for(let r=0;r{const{base:n,config:r}=t;let o=mt;const i=uo(e,r);return(s=>(o===mt&&(o=e(r,n)),i(o,s)))},uf=(t,e)=>{const{base:n,config:r,defaultVariants:o,deferredError:i,variantKeys:s}=t;let l=t.compiledCompoundVariants,u=t.compiledVariants,c=Vr,d=null;const f=uo(e,r);let p=1;const m=h=>{const g=l.length>0?of(l,Fs(o,h)):void 0;return e(r,n,nf(u,o,h),g)};return(h=>{if(i)throw i;(u===null||l===null)&&(Ls(t),u=t.compiledVariants,l=t.compiledCompoundVariants,c=t.compiledCompoundSlots??Vr);let g;if(p>0)p--,g=m(h);else{d??=jd();const b=Ms(s,o,h);if(b!==null){const y=l.length>0||c.length>0?As(l,c):"",v=b+"#"+y,w=d.get(v);w!==mt?g=w:(g=m(h),d.set(v,g))}else g=m(h)}return f(g,h)})},cf=(t,e)=>{const{config:n,defaultVariants:r,deferredError:o,slots:i,variantKeys:s}=t;let l=null,u=null,c=null,d=null,f=!1,p=null,m=null,h=1;const g=()=>{if(c!==null)return;(t.compiledVariants===null||t.compiledCompoundVariants===null||t.compiledCompoundSlots===null||t.compiledCompoundSlotsBySlot===null||t.slotKeys===null)&&Ls(t);const y=t.compiledVariants;l=t.compiledCompoundVariants,u=t.compiledCompoundSlots;const v=t.compiledCompoundSlotsBySlot;c=t.slotKeys,f=l.length>0||u.length>0,p=uo(e,n);const w=new Array(c.length);for(let x=0;x{const I=f?Fs(r,P,S):void 0,M=I?sf(F,l,I):void 0,O=I?lf(K,I):void 0;return e(n,i[F],rf(F,y,r,P,S),M,O)}}d=w},b=y=>{const v=c,w=d,x=p,F={};for(let K=0;K{if(I==null)return S;let M=!1;for(const O in I)if(!(O==="class"||O==="className")&&I[O]!==void 0){M=!0;break}return x(M?P(y,I):S,I)}}return F};return(y=>{if(o)throw o;if(g(),h>0)return h--,b(y);const v=Ms(s,r,y);if(v===null)return b(y);const w=f?As(l,u):"",x=v+"#"+w;m??=Rs();const F=m.get(x);if(F!==mt)return F;const K=b(y);return m.set(x,K),K})},df=(t,e)=>{if(t.mode==="plain")return af(t,e);let n;return(r=>(n??=t.mode==="slots"?cf(t,e):uf(t,e),n(r)))},ff=(t,e)=>{t.variantKeys=e.variantKeys,t.extend=e.extend,t.base=e.base,t.slots=e.slots,t.variants=e.variants,t.defaultVariants=e.defaultVariants,t.compoundSlots=e.compoundSlots,t.compoundVariants=e.compoundVariants},pf=t=>{const e=(r,o)=>{const i=Xd(r,o),s=df(i,t);return ff(s,i),s};return{tv:e,createTV:r=>(o,i)=>e(o,i?ao(r,i):r)}},hf=(t,e)=>{const n=t.length,r=e.length,o=new Array(n+r);for(let i=0;i({classGroupId:t,validator:e}),Ds=(t=new Map,e=null,n)=>({nextPart:t,validators:e,classGroupId:n}),Nn="-",qo=[],bf="arbitrary..",gf=t=>{const e=yf(t),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=t;return{getClassGroupId:s=>{if(s[0]==="["&&s[s.length-1]==="]")return vf(s);const l=s.split(Nn),u=l[0]===""&&l.length>1?1:0;return Ns(l,u,e)},getConflictingClassGroupIds:(s,l)=>{if(l){const u=r[s],c=n[s];return u?c?hf(c,u):u:c||qo}return n[s]||qo}}},Ns=(t,e,n)=>{if(t.length-e===0)return n.classGroupId;const o=t[e],i=n.nextPart.get(o);if(i){const c=Ns(t,e+1,i);if(c)return c}const s=n.validators;if(s===null)return;const l=e===0?t.join(Nn):t.slice(e).join(Nn),u=s.length;for(let c=0;c{const e=t.slice(1,-1),n=e.indexOf(":");if(n===-1)return;const r=e.slice(0,n);return r?bf+r:void 0},yf=t=>{const{theme:e,classGroups:n}=t;return $f(n,e)},$f=(t,e)=>{const n=Ds();for(const r in t){const o=t[r];fo(o,n,r,e)}return n},fo=(t,e,n,r)=>{const o=t.length;for(let i=0;i{if(typeof t=="string"){Ef(t,e,n);return}if(typeof t=="function"){wf(t,e,n,r);return}Cf(t,e,n,r)},Ef=(t,e,n)=>{const r=t===""?e:Os(e,t);r.classGroupId=n},wf=(t,e,n,r)=>{if(Sf(t)){fo(t(r),e,n,r);return}e.validators===null&&(e.validators=[]),e.validators.push(mf(n,t))},Cf=(t,e,n,r)=>{const o=Object.entries(t),i=o.length;for(let s=0;s{let n=t;const r=e.split(Nn),o=r.length;for(let i=0;i"isThemeGetter"in t&&t.isThemeGetter===!0,Pf="!",Tf=58,kf=47,Mf=91,Af=93,Rf=40,Lf=41,Yo=33,If=(t,e,n,r)=>({modifiers:t,hasImportantModifier:e,baseClassName:n,maybePostfixModifierPosition:r,isExternal:void 0}),Xo=t=>{const e=[];let n=0,r=0,o=0,i;const s=t.length;for(let p=0;po?i-o:void 0;return If(e,c,u,f)},Ff=t=>{const e=new Set(t.orderSensitiveModifiers);return n=>{const r=[];let o=[];for(let i=0;i0){o.sort();for(let c=0;c0){o.sort();for(let i=0;i{const e=Ff(t),n=Of(t),{getClassGroupId:r,getConflictingClassGroupIds:o}=gf(t);let i=Object.create(null),s=Object.create(null),l=0,u=new Int32Array(256),c=0,d=new Uint8Array(64),f=!1;const p=w=>{const x=[],F=w.length;let K=-1;f=!1;for(let P=0;P=9&&S<=13?(f=!0,K!==-1&&(x.push(w.slice(K,P)),K=-1)):K===-1&&(K=P)}return K!==-1&&x.push(w.slice(K)),x},m=new Map;let h=0;const g=w=>{let x=m.get(w);if(x===void 0&&(x=h++,m.set(w,x),x>=u.length)){const F=new Int32Array(u.length*2);F.set(u),u=F}return x},b=w=>{const{isExternal:x,modifiers:F,hasImportantModifier:K,baseClassName:P,maybePostfixModifierPosition:S}=Xo(w);if(x)return vr;let I=!!S,M;if(I){const R=P.substring(0,S);M=r(R);const H=M&&n[M]?r(P):void 0;H&&H!==M&&(M=H,I=!1)}else M=r(P);if(!M){if(!I||(M=r(P),!M))return vr;I=!1}const O=F.length===0?"":F.length===1?F[0]:e(F).join(":"),z=K?O+Pf:O,U=o(M,I),C=[];for(let R=0;R{let x=i[w];return x!==void 0||(x=s[w],x===void 0&&(x=b(w)),i[w]=x,++l>Kf&&(l=0,s=i,i=Object.create(null))),x};return{parseClassName:Xo,sortModifiers:e,postfixLookupClassGroupIds:n,getClassGroupId:r,getConflictingClassGroupIds:o,getClassDescriptor:y,mergeClassList:w=>{const x=p(w),F=x.length;if(F===1)return x[0];h>Df&&(m.clear(),h=0,i=Object.create(null),s=Object.create(null),l=0),c=c+1|0,c===0&&(c=1);const K=c;if(F>d.length){let M=d.length;for(;M=0;M-=1){const O=x[M];S+=O.length;const z=y(O);if(z.isExternal){d[M]=1;continue}const U=z.classId;if(u[U]===K){d[M]=0,P=!0;continue}u[U]=K;const C=z.conflictIds;for(let R=0;R{const e=Object.create(null),n=t.postfixLookupClassGroups;if(n)for(let r=0;r{let e,n,r=Object.create(null),o=Object.create(null),i=0;const s=c=>(e=Nf(t()),n=e.mergeClassList,u.mergeString=l,l(c)),l=c=>{let d=r[c];return d!==void 0||(d=o[c],d===void 0&&(d=n(c)),r[c]=d,++i>Vf&&(i=0,o=r,r=Object.create(null))),d},u=(...c)=>u.mergeString(Qn(c));return u.mergeString=s,u},Bf=[],pe=t=>{const e=n=>n[t]||Bf;return e.isThemeGetter=!0,e},Vs=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,Bs=/^\((?:(\w[\w-]*):)?(.+)\)$/i,_f=/^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/,Hf=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,jf=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,zf=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,Uf=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,Zf=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,_s=Number,Wf=Number.isNaN,Gf=Number.isInteger,Ze=t=>_f.test(t),X=t=>!!t&&!Wf(_s(t)),Ke=t=>!!t&&Gf(_s(t)),yr=t=>t.endsWith("%")&&X(t.slice(0,-1)),Oe=t=>Hf.test(t),Hs=()=>!0,qf=t=>jf.test(t)&&!zf.test(t),po=()=>!1,Yf=t=>Uf.test(t),Xf=t=>Zf.test(t),Jf=t=>!B(t)&&!_(t),Qf=t=>t.startsWith("@container")&&(t[10]==="/"&&t[11]!==void 0||t[11]==="s"&&t[16]!==void 0&&t.startsWith("-size/",10)||t[11]==="n"&&t[18]!==void 0&&t.startsWith("-normal/",10)),e0=t=>ot(t,Us,po),B=t=>Vs.test(t),lt=t=>ot(t,Zs,qf),Qo=t=>ot(t,a0,X),t0=t=>ot(t,Gs,Hs),n0=t=>ot(t,Ws,po),ei=t=>ot(t,js,po),r0=t=>ot(t,zs,Xf),mn=t=>ot(t,qs,Yf),_=t=>Bs.test(t),Nt=t=>vt(t,Zs),o0=t=>vt(t,Ws),ti=t=>vt(t,js),i0=t=>vt(t,Us),s0=t=>vt(t,zs),bn=t=>vt(t,qs,!0),l0=t=>vt(t,Gs,!0),ot=(t,e,n)=>{const r=Vs.exec(t);return r?r[1]?e(r[1]):n(r[2]):!1},vt=(t,e,n=!1)=>{const r=Bs.exec(t);return r?r[1]?e(r[1]):n:!1},js=t=>t==="position"||t==="percentage",zs=t=>t==="image"||t==="url",Us=t=>t==="length"||t==="size"||t==="bg-size",Zs=t=>t==="length",a0=t=>t==="number",Ws=t=>t==="family-name",Gs=t=>t==="number"||t==="weight",qs=t=>t==="shadow",$r=()=>{const t=pe("color"),e=pe("font"),n=pe("text"),r=pe("font-weight"),o=pe("tracking"),i=pe("leading"),s=pe("breakpoint"),l=pe("container"),u=pe("spacing"),c=pe("radius"),d=pe("shadow"),f=pe("inset-shadow"),p=pe("text-shadow"),m=pe("drop-shadow"),h=pe("blur"),g=pe("perspective"),b=pe("aspect"),y=pe("ease"),v=pe("animate"),w=()=>["auto","avoid","all","avoid-page","page","left","right","column"],x=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"],F=()=>[...x(),_,B],K=()=>["auto","hidden","clip","visible","scroll"],P=()=>["auto","contain","none"],S=()=>[_,B,u],I=()=>[Ze,"full","auto",...S()],M=()=>[Ke,"none","subgrid",_,B],O=()=>["auto",{span:["full",Ke,_,B]},Ke,_,B],z=()=>[Ke,"auto",_,B],U=()=>["auto","min","max","fr",_,B],C=()=>["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"],R=()=>["start","end","center","stretch","center-safe","end-safe"],H=()=>["auto",...S()],L=()=>[Ze,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...S()],q=()=>[Ze,"screen","full","dvw","lvw","svw","min","max","fit",...S()],T=()=>[Ze,"screen","full","lh","dvh","lvh","svh","min","max","fit",...S()],A=()=>[t,_,B],j=()=>[...x(),ti,ei,{position:[_,B]}],W=()=>["no-repeat",{repeat:["","x","y","space","round"]}],ne=()=>["auto","cover","contain",i0,e0,{size:[_,B]}],te=()=>[yr,Nt,lt],k=()=>["","none","full",c,_,B],Z=()=>["",X,Nt,lt],oe=()=>["solid","dashed","dotted","double"],ye=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],G=()=>[X,yr,ti,ei],Fe=()=>["","none",h,_,B],D=()=>["none",X,_,B],N=()=>["none",X,_,B],ie=()=>[X,_,B],se=()=>[Ze,"full",...S()];return{theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[Oe],breakpoint:[Oe],color:[Hs],container:[Oe],"drop-shadow":[Oe],ease:["in","out","in-out"],font:[Jf],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[Oe],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[Oe],shadow:[Oe],spacing:["px",X],text:[Oe],"text-shadow":[Oe],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",Ze,B,_,b]}],container:["container"],"container-type":[{"@container":["","normal","size",_,B]}],"container-named":[Qf],columns:[{columns:[X,B,_,l]}],"break-after":[{"break-after":w()}],"break-before":[{"break-before":w()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:F()}],overflow:[{overflow:K()}],"overflow-x":[{"overflow-x":K()}],"overflow-y":[{"overflow-y":K()}],overscroll:[{overscroll:P()}],"overscroll-x":[{"overscroll-x":P()}],"overscroll-y":[{"overscroll-y":P()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:I()}],"inset-x":[{"inset-x":I()}],"inset-y":[{"inset-y":I()}],start:[{"inset-s":I(),start:I()}],end:[{"inset-e":I(),end:I()}],"inset-bs":[{"inset-bs":I()}],"inset-be":[{"inset-be":I()}],top:[{top:I()}],right:[{right:I()}],bottom:[{bottom:I()}],left:[{left:I()}],visibility:["visible","invisible","collapse"],z:[{z:[Ke,"auto",_,B]}],basis:[{basis:[Ze,"full","auto",l,...S()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[X,Ze,"auto","initial","none",B]}],grow:[{grow:["",X,_,B]}],shrink:[{shrink:["",X,_,B]}],order:[{order:[Ke,"first","last","none",_,B]}],"grid-cols":[{"grid-cols":M()}],"col-start-end":[{col:O()}],"col-start":[{"col-start":z()}],"col-end":[{"col-end":z()}],"grid-rows":[{"grid-rows":M()}],"row-start-end":[{row:O()}],"row-start":[{"row-start":z()}],"row-end":[{"row-end":z()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":U()}],"auto-rows":[{"auto-rows":U()}],gap:[{gap:S()}],"gap-x":[{"gap-x":S()}],"gap-y":[{"gap-y":S()}],"justify-content":[{justify:[...C(),"normal"]}],"justify-items":[{"justify-items":[...R(),"normal"]}],"justify-self":[{"justify-self":["auto",...R()]}],"align-content":[{content:["normal",...C()]}],"align-items":[{items:[...R(),{baseline:["","last"]}]}],"align-self":[{self:["auto",...R(),{baseline:["","last"]}]}],"place-content":[{"place-content":C()}],"place-items":[{"place-items":[...R(),"baseline"]}],"place-self":[{"place-self":["auto",...R()]}],p:[{p:S()}],px:[{px:S()}],py:[{py:S()}],ps:[{ps:S()}],pe:[{pe:S()}],pbs:[{pbs:S()}],pbe:[{pbe:S()}],pt:[{pt:S()}],pr:[{pr:S()}],pb:[{pb:S()}],pl:[{pl:S()}],m:[{m:H()}],mx:[{mx:H()}],my:[{my:H()}],ms:[{ms:H()}],me:[{me:H()}],mbs:[{mbs:H()}],mbe:[{mbe:H()}],mt:[{mt:H()}],mr:[{mr:H()}],mb:[{mb:H()}],ml:[{ml:H()}],"space-x":[{"space-x":S()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":S()}],"space-y-reverse":["space-y-reverse"],size:[{size:L()}],"inline-size":[{inline:["auto",...q()]}],"min-inline-size":[{"min-inline":["auto",...q()]}],"max-inline-size":[{"max-inline":["none",...q()]}],"block-size":[{block:["auto",...T()]}],"min-block-size":[{"min-block":["auto",...T()]}],"max-block-size":[{"max-block":["none",...T()]}],w:[{w:[l,"screen",...L()]}],"min-w":[{"min-w":[l,"screen","none",...L()]}],"max-w":[{"max-w":[l,"screen","none","prose",{screen:[s]},...L()]}],h:[{h:["screen","lh",...L()]}],"min-h":[{"min-h":["screen","lh","none",...L()]}],"max-h":[{"max-h":["screen","lh",...L()]}],"font-size":[{text:["base",n,Nt,lt]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[r,l0,t0]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",yr,B]}],"font-family":[{font:[o0,n0,e]}],"font-features":[{"font-features":[B]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[o,_,B]}],"line-clamp":[{"line-clamp":[X,"none",_,Qo]}],leading:[{leading:[i,...S()]}],"list-image":[{"list-image":["none",_,B]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",_,B]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:A()}],"text-color":[{text:A()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...oe(),"wavy"]}],"text-decoration-thickness":[{decoration:[X,"from-font","auto",_,lt]}],"text-decoration-color":[{decoration:A()}],"underline-offset":[{"underline-offset":[X,"auto",_,B]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:S()}],"tab-size":[{tab:[Ke,_,B]}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",_,B]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",_,B]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:j()}],"bg-repeat":[{bg:W()}],"bg-size":[{bg:ne()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},Ke,_,B],radial:["",_,B],conic:[Ke,_,B]},s0,r0]}],"bg-color":[{bg:A()}],"gradient-from-pos":[{from:te()}],"gradient-via-pos":[{via:te()}],"gradient-to-pos":[{to:te()}],"gradient-from":[{from:A()}],"gradient-via":[{via:A()}],"gradient-to":[{to:A()}],rounded:[{rounded:k()}],"rounded-s":[{"rounded-s":k()}],"rounded-e":[{"rounded-e":k()}],"rounded-t":[{"rounded-t":k()}],"rounded-r":[{"rounded-r":k()}],"rounded-b":[{"rounded-b":k()}],"rounded-l":[{"rounded-l":k()}],"rounded-ss":[{"rounded-ss":k()}],"rounded-se":[{"rounded-se":k()}],"rounded-ee":[{"rounded-ee":k()}],"rounded-es":[{"rounded-es":k()}],"rounded-tl":[{"rounded-tl":k()}],"rounded-tr":[{"rounded-tr":k()}],"rounded-br":[{"rounded-br":k()}],"rounded-bl":[{"rounded-bl":k()}],"border-w":[{border:Z()}],"border-w-x":[{"border-x":Z()}],"border-w-y":[{"border-y":Z()}],"border-w-s":[{"border-s":Z()}],"border-w-e":[{"border-e":Z()}],"border-w-bs":[{"border-bs":Z()}],"border-w-be":[{"border-be":Z()}],"border-w-t":[{"border-t":Z()}],"border-w-r":[{"border-r":Z()}],"border-w-b":[{"border-b":Z()}],"border-w-l":[{"border-l":Z()}],"divide-x":[{"divide-x":Z()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":Z()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:[...oe(),"hidden","none"]}],"divide-style":[{divide:[...oe(),"hidden","none"]}],"border-color":[{border:A()}],"border-color-x":[{"border-x":A()}],"border-color-y":[{"border-y":A()}],"border-color-s":[{"border-s":A()}],"border-color-e":[{"border-e":A()}],"border-color-bs":[{"border-bs":A()}],"border-color-be":[{"border-be":A()}],"border-color-t":[{"border-t":A()}],"border-color-r":[{"border-r":A()}],"border-color-b":[{"border-b":A()}],"border-color-l":[{"border-l":A()}],"divide-color":[{divide:A()}],"outline-style":[{outline:[...oe(),"none","hidden"]}],"outline-offset":[{"outline-offset":[X,_,B]}],"outline-w":[{outline:["",X,Nt,lt]}],"outline-color":[{outline:A()}],shadow:[{shadow:["","none",d,bn,mn]}],"shadow-color":[{shadow:A()}],"inset-shadow":[{"inset-shadow":["none",f,bn,mn]}],"inset-shadow-color":[{"inset-shadow":A()}],"ring-w":[{ring:Z()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:A()}],"ring-offset-w":[{"ring-offset":[X,lt]}],"ring-offset-color":[{"ring-offset":A()}],"inset-ring-w":[{"inset-ring":Z()}],"inset-ring-color":[{"inset-ring":A()}],"text-shadow":[{"text-shadow":["none",p,bn,mn]}],"text-shadow-color":[{"text-shadow":A()}],opacity:[{opacity:[X,_,B]}],"mix-blend":[{"mix-blend":[...ye(),"plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":ye()}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[X]}],"mask-image-linear-from-pos":[{"mask-linear-from":G()}],"mask-image-linear-to-pos":[{"mask-linear-to":G()}],"mask-image-linear-from-color":[{"mask-linear-from":A()}],"mask-image-linear-to-color":[{"mask-linear-to":A()}],"mask-image-t-from-pos":[{"mask-t-from":G()}],"mask-image-t-to-pos":[{"mask-t-to":G()}],"mask-image-t-from-color":[{"mask-t-from":A()}],"mask-image-t-to-color":[{"mask-t-to":A()}],"mask-image-r-from-pos":[{"mask-r-from":G()}],"mask-image-r-to-pos":[{"mask-r-to":G()}],"mask-image-r-from-color":[{"mask-r-from":A()}],"mask-image-r-to-color":[{"mask-r-to":A()}],"mask-image-b-from-pos":[{"mask-b-from":G()}],"mask-image-b-to-pos":[{"mask-b-to":G()}],"mask-image-b-from-color":[{"mask-b-from":A()}],"mask-image-b-to-color":[{"mask-b-to":A()}],"mask-image-l-from-pos":[{"mask-l-from":G()}],"mask-image-l-to-pos":[{"mask-l-to":G()}],"mask-image-l-from-color":[{"mask-l-from":A()}],"mask-image-l-to-color":[{"mask-l-to":A()}],"mask-image-x-from-pos":[{"mask-x-from":G()}],"mask-image-x-to-pos":[{"mask-x-to":G()}],"mask-image-x-from-color":[{"mask-x-from":A()}],"mask-image-x-to-color":[{"mask-x-to":A()}],"mask-image-y-from-pos":[{"mask-y-from":G()}],"mask-image-y-to-pos":[{"mask-y-to":G()}],"mask-image-y-from-color":[{"mask-y-from":A()}],"mask-image-y-to-color":[{"mask-y-to":A()}],"mask-image-radial":[{"mask-radial":[_,B]}],"mask-image-radial-from-pos":[{"mask-radial-from":G()}],"mask-image-radial-to-pos":[{"mask-radial-to":G()}],"mask-image-radial-from-color":[{"mask-radial-from":A()}],"mask-image-radial-to-color":[{"mask-radial-to":A()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":x()}],"mask-image-conic-pos":[{"mask-conic":[X]}],"mask-image-conic-from-pos":[{"mask-conic-from":G()}],"mask-image-conic-to-pos":[{"mask-conic-to":G()}],"mask-image-conic-from-color":[{"mask-conic-from":A()}],"mask-image-conic-to-color":[{"mask-conic-to":A()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:j()}],"mask-repeat":[{mask:W()}],"mask-size":[{mask:ne()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",_,B]}],filter:[{filter:["","none",_,B]}],blur:[{blur:Fe()}],brightness:[{brightness:[X,_,B]}],contrast:[{contrast:[X,_,B]}],"drop-shadow":[{"drop-shadow":["","none",m,bn,mn]}],"drop-shadow-color":[{"drop-shadow":A()}],grayscale:[{grayscale:["",X,_,B]}],"hue-rotate":[{"hue-rotate":[X,_,B]}],invert:[{invert:["",X,_,B]}],saturate:[{saturate:[X,_,B]}],sepia:[{sepia:["",X,_,B]}],"backdrop-filter":[{"backdrop-filter":["","none",_,B]}],"backdrop-blur":[{"backdrop-blur":Fe()}],"backdrop-brightness":[{"backdrop-brightness":[X,_,B]}],"backdrop-contrast":[{"backdrop-contrast":[X,_,B]}],"backdrop-grayscale":[{"backdrop-grayscale":["",X,_,B]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[X,_,B]}],"backdrop-invert":[{"backdrop-invert":["",X,_,B]}],"backdrop-opacity":[{"backdrop-opacity":[X,_,B]}],"backdrop-saturate":[{"backdrop-saturate":[X,_,B]}],"backdrop-sepia":[{"backdrop-sepia":["",X,_,B]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":S()}],"border-spacing-x":[{"border-spacing-x":S()}],"border-spacing-y":[{"border-spacing-y":S()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",_,B]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[X,"initial",_,B]}],ease:[{ease:["linear","initial",y,_,B]}],delay:[{delay:[X,_,B]}],animate:[{animate:["none",v,_,B]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[g,_,B]}],"perspective-origin":[{"perspective-origin":F()}],rotate:[{rotate:D()}],"rotate-x":[{"rotate-x":D()}],"rotate-y":[{"rotate-y":D()}],"rotate-z":[{"rotate-z":D()}],scale:[{scale:N()}],"scale-x":[{"scale-x":N()}],"scale-y":[{"scale-y":N()}],"scale-z":[{"scale-z":N()}],"scale-3d":["scale-3d"],skew:[{skew:ie()}],"skew-x":[{"skew-x":ie()}],"skew-y":[{"skew-y":ie()}],transform:[{transform:[_,B,"","none","gpu","cpu"]}],"transform-origin":[{origin:F()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:se()}],"translate-x":[{"translate-x":se()}],"translate-y":[{"translate-y":se()}],"translate-z":[{"translate-z":se()}],"translate-none":["translate-none"],zoom:[{zoom:[Ke,_,B]}],accent:[{accent:A()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:A()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",_,B]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scrollbar-thumb-color":[{"scrollbar-thumb":A()}],"scrollbar-track-color":[{"scrollbar-track":A()}],"scrollbar-gutter":[{"scrollbar-gutter":["auto","stable","both"]}],"scrollbar-w":[{scrollbar:["auto","thin","none"]}],"scroll-m":[{"scroll-m":S()}],"scroll-mx":[{"scroll-mx":S()}],"scroll-my":[{"scroll-my":S()}],"scroll-ms":[{"scroll-ms":S()}],"scroll-me":[{"scroll-me":S()}],"scroll-mbs":[{"scroll-mbs":S()}],"scroll-mbe":[{"scroll-mbe":S()}],"scroll-mt":[{"scroll-mt":S()}],"scroll-mr":[{"scroll-mr":S()}],"scroll-mb":[{"scroll-mb":S()}],"scroll-ml":[{"scroll-ml":S()}],"scroll-p":[{"scroll-p":S()}],"scroll-px":[{"scroll-px":S()}],"scroll-py":[{"scroll-py":S()}],"scroll-ps":[{"scroll-ps":S()}],"scroll-pe":[{"scroll-pe":S()}],"scroll-pbs":[{"scroll-pbs":S()}],"scroll-pbe":[{"scroll-pbe":S()}],"scroll-pt":[{"scroll-pt":S()}],"scroll-pr":[{"scroll-pr":S()}],"scroll-pb":[{"scroll-pb":S()}],"scroll-pl":[{"scroll-pl":S()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",_,B]}],fill:[{fill:["none",...A()]}],"stroke-w":[{stroke:[X,Nt,lt,Qo]}],stroke:[{stroke:["none",...A()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{"container-named":["container-type"],overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","inset-bs","inset-be","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pbs","pbe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mbs","mbe","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-bs","border-w-be","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-bs","border-color-be","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mbs","scroll-mbe","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pbs","scroll-pbe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},postfixLookupClassGroups:["container-type"],orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}},u0=(t,{extend:e={},override:n={}})=>(gn(t.theme,n.theme),gn(t.classGroups,n.classGroups),gn(t.conflictingClassGroups,n.conflictingClassGroups),gn(t.conflictingClassGroupModifiers,n.conflictingClassGroupModifiers),Br(t,"postfixLookupClassGroups",n.postfixLookupClassGroups),Br(t,"orderSensitiveModifiers",n.orderSensitiveModifiers),vn(t.theme,e.theme),vn(t.classGroups,e.classGroups),vn(t.conflictingClassGroups,e.conflictingClassGroups),vn(t.conflictingClassGroupModifiers,e.conflictingClassGroupModifiers),_r(t,e,"postfixLookupClassGroups"),_r(t,e,"orderSensitiveModifiers"),t),Br=(t,e,n)=>{n!==void 0&&(t[e]=n)},gn=(t,e)=>{if(e)for(const n in e)Br(t,n,e[n])},vn=(t,e)=>{if(e)for(const n in e)_r(t,e,n)},_r=(t,e,n)=>{const r=e[n];r!==void 0&&(t[n]=t[n]?t[n].concat(r):r)},Ys=t=>Jo(t?typeof t=="function"?()=>t($r()):()=>u0($r(),t):$r),c0=t=>{if(Pe(t))return;const e=t,n={...e.extend??{}};for(const o of["theme","classGroups","conflictingClassGroups","conflictingClassGroupModifiers","postfixLookupClassGroups","orderSensitiveModifiers","cacheSize","prefix","separator","experimentalParseClassName"])e[o]!==void 0&&n[o]===void 0&&(n[o]=e[o]);const r={};if(Object.keys(n).length>0&&(r.extend=n),e.override!=null&&!Pe(e.override)&&(r.override=e.override),!(!r.extend&&!r.override))return r},d0=t=>{const e=c0(t),n=Ys(e);return r=>n.mergeString(r)},An,f0=()=>(An||(An=Ys()),An),p0=()=>((!$e.cachedTwMerge||$e.didTwMergeConfigChange)&&($e.didTwMergeConfigChange=!1,$e.cachedTwMerge=d0($e.cachedTwMergeConfig)),$e.cachedTwMerge),h0=t=>{const e=t?.twMergeConfig;!e||Pe(e)||Ps(e,$e.cachedTwMergeConfig)||($e.cachedTwMergeConfig=e,$e.didTwMergeConfigChange=!0)},m0=t=>Qn(t),b0=$e.reset.bind($e);$e.reset=()=>{An=void 0,b0()};var g0=(t,e)=>{const n=m0(t);return!n||!(e?.twMerge??!0)?n||void 0:n.indexOf(" ")===-1?n:(h0(e),(!!(e?.twMergeConfig&&!Pe(e.twMergeConfig))?p0():f0().mergeString)(n)||void 0)},v0=(t,...e)=>g0(e,t),y0=pf(v0),Ee=y0.tv,$0=zt;const x0=a.createContext({}),E0="__avatar_group_child",w0=Ee({defaultVariants:{status:"default"},slots:{base:"alert",content:"alert__content",description:"alert__description",indicator:"alert__indicator",title:"alert__title"},variants:{status:{accent:{base:"alert--accent"},danger:{base:"alert--danger"},default:{base:"alert--default"},success:{base:"alert--success"},warning:{base:"alert--warning"}}}}),C0=Ee({defaultVariants:{color:"default",size:"md"},slots:{base:"avatar",fallback:"avatar__fallback",image:"avatar__image"},variants:{color:{accent:{fallback:"avatar__fallback--accent"},danger:{fallback:"avatar__fallback--danger"},default:{fallback:"avatar__fallback--default"},success:{fallback:"avatar__fallback--success"},warning:{fallback:"avatar__fallback--warning"}},size:{lg:{base:"avatar--lg"},md:{base:"avatar--md"},sm:{base:"avatar--sm"}},variant:{default:{},soft:{base:"avatar--soft"}}}}),S0=Ee({base:"button",defaultVariants:{fullWidth:!1,isIconOnly:!1,size:"md",variant:"primary"},variants:{fullWidth:{false:"",true:"button--full-width"},isIconOnly:{true:"button--icon-only"},size:{lg:"button--lg",md:"button--md",sm:"button--sm"},variant:{danger:"button--danger","danger-soft":"button--danger-soft",ghost:"button--ghost",outline:"button--outline",primary:"button--primary",secondary:"button--secondary",tertiary:"button--tertiary"}}}),P0=Ee({defaultVariants:{variant:"default"},slots:{base:"card",content:"card__content",description:"card__description",footer:"card__footer",header:"card__header",title:"card__title"},variants:{variant:{default:{base:"card--default"},secondary:{base:"card--secondary"},tertiary:{base:"card--tertiary"},transparent:{base:"card--transparent"}}}}),T0=Ee({base:"close-button",defaultVariants:{variant:"default"},variants:{variant:{default:"close-button--default"}}}),k0=Ee({slots:{menu:"dropdown__menu",popover:"dropdown__popover",root:"dropdown",trigger:"dropdown__trigger"}}),M0=Ee({base:"input",defaultVariants:{fullWidth:!1,variant:"primary"},variants:{fullWidth:{false:"",true:"input--full-width"},variant:{primary:"input--primary",secondary:"input--secondary"}}}),A0=Ee({base:"label",defaultVariants:{isDisabled:!1,isInvalid:!1,isRequired:!1},variants:{isDisabled:{true:"label--disabled"},isInvalid:{true:"label--invalid"},isRequired:{true:"label--required"}}}),R0=Ee({slots:{base:"link",icon:"link__icon"}}),L0=Ee({defaultVariants:{variant:"default"},slots:{indicator:"menu-item__indicator",item:"menu-item",submenuIndicator:"menu-item__indicator menu-item__indicator--submenu"},variants:{variant:{danger:{item:"menu-item--danger"},default:{item:"menu-item--default"}}}}),I0=Ee({base:"menu-section"}),F0=Ee({defaultVariants:{animationType:"shimmer"},slots:{base:"skeleton"},variants:{animationType:{none:"skeleton--none",pulse:"skeleton--pulse",shimmer:"skeleton--shimmer"}}}),K0=Ee({base:"spinner",defaultVariants:{color:"accent",size:"md"},variants:{color:{accent:"spinner--accent",current:"spinner--current",danger:"spinner--danger",success:"spinner--success",warning:"spinner--warning"},size:{lg:"spinner--lg",md:"spinner--md",sm:"spinner--sm",xl:"spinner--xl"}}}),D0=Ee({base:"textfield",defaultVariants:{fullWidth:!1},variants:{fullWidth:{false:"",true:"textfield--full-width"}}}),N0=Ee({defaultVariants:{placement:"bottom",variant:"default"},slots:{action:"toast__action",close:"toast__close-button",content:"toast__content",description:"toast__description",indicator:"toast__indicator",region:"toast-region",title:"toast__title",toast:"toast"},variants:{placement:{bottom:{region:"toast-region--bottom",toast:"toast--bottom"},"bottom end":{region:"toast-region--bottom-end",toast:"toast--bottom-end"},"bottom start":{region:"toast-region--bottom-start",toast:"toast--bottom-start"},top:{region:"toast-region--top",toast:"toast--top"},"top end":{region:"toast-region--top-end",toast:"toast--top-end"},"top start":{region:"toast-region--top-start",toast:"toast--top-start"}},variant:{accent:{toast:"toast--accent"},danger:{toast:"toast--danger"},default:{toast:"toast--default"},success:{toast:"toast--success"},warning:{toast:"toast--warning"}}}}),dt=Symbol("default");function ze({values:t,children:e}){for(let[n,r]of t)e=$.createElement(n.Provider,{value:r},e);return e}function ve(t){let{className:e,style:n,children:r,defaultClassName:o,defaultChildren:i,defaultStyle:s,values:l,render:u}=t;return a.useMemo(()=>{let c,d,f;return typeof e=="function"?c=e({...l,defaultClassName:o}):c=e,typeof n=="function"?d=n({...l,defaultStyle:s||{}}):d=n,typeof r=="function"?f=r({...l,defaultChildren:i}):r==null?f=i:f=r,{className:c??o,style:d||s?{...s,...d}:void 0,children:f??i,"data-rac":"",render:u?p=>u(p,l):void 0}},[e,n,r,o,i,s,l,u])}function ho(t,e){return n=>e(typeof t=="function"?t(n):t,n)}function er(t,e){let n=a.useContext(t);if(e===null)return null;if(n&&typeof n=="object"&&"slots"in n&&n.slots){let r=e||dt;if(!n.slots[r]){let o=new Intl.ListFormat().format(Object.keys(n.slots).map(s=>`"${s}"`)),i=e?`Invalid slot "${e}".`:"A slot prop is required.";throw new Error(`${i} Valid slot names are ${o}.`)}return n.slots[r]}return n}function me(t,e,n){let r=er(n,t.slot)||{},{ref:o,...i}=r,s=rt(a.useMemo(()=>nn(e,o),[e,o])),l=Q(i,t);return"style"in i&&i.style&&"style"in t&&t.style&&(typeof i.style=="function"||typeof t.style=="function"?l.style=u=>{let c=typeof i.style=="function"?i.style(u):i.style,d={...u.defaultStyle,...c},f=typeof t.style=="function"?t.style({...u,defaultStyle:d}):t.style;return{...d,...f}}:l.style={...i.style,...t.style}),[l,s]}function mo(t=!0){let[e,n]=a.useState(t),r=a.useRef(!1),o=a.useCallback(i=>{r.current=!0,n(!!i)},[]);return J(()=>{r.current||n(!1)},[]),[o,e]}function O0(t){const e=/^(data-.*)$/;let n={};for(const r in t)e.test(r)||(n[r]=t[r]);return n}function V0(t,e,n){let{render:r,...o}=e,i=a.useRef(null),s=a.useMemo(()=>nn(n,i),[n,i]);J(()=>{},[t,r]);let l={...o,ref:s};return r?r(l,void 0):$.createElement(t,l)}const ni={},ue=new Proxy({},{get(t,e){if(typeof e!="string")return;let n=ni[e];return n||(n=a.forwardRef(V0.bind(null,e)),ni[e]=n),n}});typeof HTMLTemplateElement<"u"&&(Object.defineProperty(HTMLTemplateElement.prototype,"firstChild",{configurable:!0,enumerable:!0,get:function(){return this.content.firstChild}}),Object.defineProperty(HTMLTemplateElement.prototype,"appendChild",{configurable:!0,enumerable:!0,value:function(t){return this.content.appendChild(t)}}),Object.defineProperty(HTMLTemplateElement.prototype,"removeChild",{configurable:!0,enumerable:!0,value:function(t){return this.content.removeChild(t)}}),Object.defineProperty(HTMLTemplateElement.prototype,"insertBefore",{configurable:!0,enumerable:!0,value:function(t,e){return this.content.insertBefore(t,e)}}));const On=a.createContext(!1);function B0(t){if(a.useContext(On))return $.createElement($.Fragment,null,t.children);let n=$.createElement(On.Provider,{value:!0},t.children);return $.createElement("template",null,n)}function rn(t){let e=(n,r)=>a.useContext(On)?null:t(n,r);return e.displayName=t.displayName||t.name,a.forwardRef(e)}function bo(){return a.useContext(On)}const go=a.createContext({}),_0=rn(function(e,n){[e,n]=me(e,n,go);let{elementType:r="label",...o}=e,i=ue[r];return $.createElement(i,{className:"react-aria-Label",...o,ref:n})});function Xs(t){let{id:e,label:n,"aria-labelledby":r,"aria-label":o,labelElementType:i="label"}=t;e=xe(e);let s=xe(),l={};n&&(r=r?`${s} ${r}`:s,l={id:s,htmlFor:i==="label"?e:void 0});let u=$s({id:e,"aria-label":o,"aria-labelledby":r});return{labelProps:l,fieldProps:u}}const H0=new Set(["Arab","Syrc","Samr","Mand","Thaa","Mend","Nkoo","Adlm","Rohg","Hebr"]),j0=new Set(["ae","ar","arc","bcc","bqi","ckb","dv","fa","glk","he","ku","mzn","nqo","pnb","ps","sd","ug","ur","yi"]);function z0(t){if(Intl.Locale){let n=new Intl.Locale(t).maximize(),r=typeof n.getTextInfo=="function"?n.getTextInfo():n.textInfo;if(r)return r.direction==="rtl";if(n.script)return H0.has(n.script)}let e=t.split("-")[0];return j0.has(e)}const Js=Symbol.for("react-aria.i18n.locale");function Qs(){let t=typeof window<"u"&&window[Js]||typeof navigator<"u"&&(navigator.language||navigator.userLanguage)||"en-US";try{Intl.DateTimeFormat.supportedLocalesOf([t])}catch{t="en-US"}return{locale:t,direction:z0(t)?"rtl":"ltr"}}let Hr=Qs(),Ht=new Set;function ri(){Hr=Qs();for(let t of Ht)t(Hr)}function U0(){let t=nt(),[e,n]=a.useState(Hr);return a.useEffect(()=>(Ht.size===0&&window.addEventListener("languagechange",ri),Ht.add(n),()=>{Ht.delete(n),Ht.size===0&&window.removeEventListener("languagechange",ri)}),[]),t?{locale:typeof window<"u"&&window[Js]||"en-US",direction:"ltr"}:e}const Z0=$.createContext(null);function it(){let t=U0();return a.useContext(Z0)||t}let xr=new Map,jr=!1;try{jr=new Intl.NumberFormat("de-DE",{signDisplay:"exceptZero"}).resolvedOptions().signDisplay==="exceptZero"}catch{}let Vn=!1;try{Vn=new Intl.NumberFormat("de-DE",{style:"unit",unit:"degree"}).resolvedOptions().style==="unit"}catch{}const el={degree:{narrow:{default:"°","ja-JP":" 度","zh-TW":"度","sl-SI":" °"}}};class W0{constructor(e,n={}){this.numberFormatter=G0(e,n),this.options=n}format(e){let n="";if(!jr&&this.options.signDisplay!=null?n=q0(this.numberFormatter,this.options.signDisplay,e):n=this.numberFormatter.format(e),this.options.style==="unit"&&!Vn){let{unit:r,unitDisplay:o="short",locale:i}=this.resolvedOptions();if(!r)return n;let s=el[r]?.[o];n+=s[i]||s.default}return n}formatToParts(e){return this.numberFormatter.formatToParts(e)}formatRange(e,n){if(typeof this.numberFormatter.formatRange=="function")return this.numberFormatter.formatRange(e,n);if(n= start date");return`${this.format(e)} – ${this.format(n)}`}formatRangeToParts(e,n){if(typeof this.numberFormatter.formatRangeToParts=="function")return this.numberFormatter.formatRangeToParts(e,n);if(n= start date");let r=this.numberFormatter.formatToParts(e),o=this.numberFormatter.formatToParts(n);return[...r.map(i=>({...i,source:"startRange"})),{type:"literal",value:" – ",source:"shared"},...o.map(i=>({...i,source:"endRange"}))]}resolvedOptions(){let e=this.numberFormatter.resolvedOptions();return!jr&&this.options.signDisplay!=null&&(e={...e,signDisplay:this.options.signDisplay}),!Vn&&this.options.style==="unit"&&(e={...e,style:"unit",unit:this.options.unit,unitDisplay:this.options.unitDisplay}),e}}function G0(t,e={}){let{numberingSystem:n}=e;if(n&&t.includes("-nu-")&&(t.includes("-u-")||(t+="-u-"),t+=`-nu-${n}`),e.style==="unit"&&!Vn){let{unit:i,unitDisplay:s="short"}=e;if(!i)throw new Error('unit option must be provided with style: "unit"');if(!el[i]?.[s])throw new Error(`Unsupported unit ${i} with unitDisplay = ${s}`);e={...e,style:"decimal"}}let r=t+(e?Object.entries(e).sort((i,s)=>i[0]0||Object.is(n,0):e==="exceptZero"&&(Object.is(n,-0)||Object.is(n,0)?n=Math.abs(n):r=n>0),r){let o=t.format(-n),i=t.format(n),s=o.replace(i,"").replace(/\u200e|\u061C/,"");return[...s].length!==1&&console.warn("@react-aria/i18n polyfill for NumberFormat signDisplay: Unsupported case"),o.replace(i,"!!!").replace(s,"+").replace("!!!",i)}else return t.format(n)}}function Y0(t={}){let{locale:e}=it();return a.useMemo(()=>new W0(e,t),[e,t])}function X0(t){let{value:e=0,minValue:n=0,maxValue:r=100,valueLabel:o,isIndeterminate:i,formatOptions:s={style:"percent"}}=t,l=de(t,{labelable:!0}),{labelProps:u,fieldProps:c}=Xs({...t,labelElementType:"span"});e=Wt(e,n,r);let d=r-n,f=d===0?0:(e-n)/d,p=Y0(s);if(!i&&!o){let m=s.style==="percent"?f:e;o=p.format(m)}return{progressBarProps:Q(l,{...c,"aria-valuenow":i?void 0:e,"aria-valuemin":n,"aria-valuemax":r,"aria-valuetext":i?void 0:o,role:"progressbar"}),labelProps:u}}const tl=a.createContext(null),X1=a.forwardRef(function(e,n){[e,n]=me(e,n,tl);let{value:r=0,minValue:o=0,maxValue:i=100,isIndeterminate:s=!1}=e;r=Wt(r,o,i);let[l,u]=mo(!e["aria-label"]&&!e["aria-labelledby"]),{progressBarProps:c,labelProps:d}=X0({...e,label:u}),f=i-o,p;s||(f===0?p=0:p=(r-o)/f*100);let m=ve({...e,defaultClassName:"react-aria-ProgressBar",values:{percentage:p,valueText:c["aria-valuetext"],isIndeterminate:s}}),h=de(e,{global:!0});return $.createElement(ue.div,{...Q(h,m,c),ref:n,slot:e.slot||void 0},$.createElement(go.Provider,{value:{...d,ref:l,elementType:"span"}},m.children))}),nl=7e3;let xt=null;function oi(t,e="assertive",n=nl){xt?xt.announce(t,e,n):(xt=new J0,(typeof IS_REACT_ACT_ENVIRONMENT=="boolean"?IS_REACT_ACT_ENVIRONMENT:typeof jest<"u")?xt.announce(t,e,n):setTimeout(()=>{xt?.isAttached()&&xt?.announce(t,e,n)},100))}class J0{constructor(){this.node=null,this.assertiveLog=null,this.politeLog=null,typeof document<"u"&&(this.node=document.createElement("div"),this.node.dataset.liveAnnouncer="true",Object.assign(this.node.style,{border:0,clip:"rect(0 0 0 0)",clipPath:"inset(50%)",height:"1px",margin:"-1px",overflow:"hidden",padding:0,position:"absolute",width:"1px",whiteSpace:"nowrap"}),this.assertiveLog=this.createLog("assertive"),this.node.appendChild(this.assertiveLog),this.politeLog=this.createLog("polite"),this.node.appendChild(this.politeLog),document.body.prepend(this.node))}isAttached(){return this.node?.isConnected}createLog(e){let n=document.createElement("div");return n.setAttribute("role","log"),n.setAttribute("aria-live",e),n.setAttribute("aria-relevant","additions"),n}destroy(){this.node&&(document.body.removeChild(this.node),this.node=null)}announce(e,n="assertive",r=nl){if(!this.node)return;let o=document.createElement("div");typeof e=="object"?(o.setAttribute("role","img"),o.setAttribute("aria-labelledby",e["aria-labelledby"])):o.textContent=e,n==="assertive"?this.assertiveLog?.appendChild(o):this.politeLog?.appendChild(o),e!==""&&setTimeout(()=>{o.remove()},r)}clear(e){this.node&&((!e||e==="assertive")&&this.assertiveLog&&(this.assertiveLog.innerHTML=""),(!e||e==="polite")&&this.politeLog&&(this.politeLog.innerHTML=""))}}function vo(t){let e=t;return e.nativeEvent=t,e.isDefaultPrevented=()=>e.defaultPrevented,e.isPropagationStopped=()=>e.cancelBubble,e.persist=()=>{},e}function rl(t,e){Object.defineProperty(t,"target",{value:e}),Object.defineProperty(t,"currentTarget",{value:e})}function ol(t){let e=a.useRef({isFocused:!1,observer:null});return J(()=>{const n=e.current;return()=>{n.observer&&(n.observer.disconnect(),n.observer=null)}},[]),a.useCallback(n=>{let r=V(n);if(r instanceof HTMLButtonElement||r instanceof HTMLInputElement||r instanceof HTMLTextAreaElement||r instanceof HTMLSelectElement){e.current.isFocused=!0;let o=r,i=s=>{if(e.current.isFocused=!1,o.disabled){let l=vo(s);t?.(l)}e.current.observer&&(e.current.observer.disconnect(),e.current.observer=null)};o.addEventListener("focusout",i,{once:!0}),e.current.observer=new MutationObserver(()=>{if(e.current.isFocused&&o.disabled){e.current.observer?.disconnect();let s=o===re()?null:re();o.dispatchEvent(new FocusEvent("blur",{relatedTarget:s})),o.dispatchEvent(new FocusEvent("focusout",{bubbles:!0,relatedTarget:s}))}}),e.current.observer.observe(o,{attributes:!0,attributeFilter:["disabled"]})}},[t])}let Bn=!1;function Q0(t){for(;t&&!ws(t,{skipVisibilityCheck:!0});)t=t.parentElement;let e=he(t),n=re(e.document);if(!n||n===t)return;let r=t?.getRootNode(),o=r!=null&&Xn(r)?r:he(t),i=h=>h===t||h!=null&&Y(t,h),s=h=>h===n||n!=null&&h!=null&&Y(n,h);Bn=!0;let l=!1,u=h=>{(s(V(h))||l)&&h.stopImmediatePropagation()},c=h=>{(s(V(h))||l)&&(h.stopImmediatePropagation(),!t&&!l&&(l=!0,ce(n),p()))},d=h=>{(i(V(h))||l)&&h.stopImmediatePropagation()},f=h=>{(i(V(h))||l)&&(h.stopImmediatePropagation(),l||(l=!0,ce(n),p()))};o.addEventListener("blur",u,!0),o.addEventListener("focusout",c,!0),o.addEventListener("focusin",f,!0),o.addEventListener("focus",d,!0);let p=()=>{cancelAnimationFrame(m),o.removeEventListener("blur",u,!0),o.removeEventListener("focusout",c,!0),o.removeEventListener("focusin",f,!0),o.removeEventListener("focus",d,!0),Bn=!1,l=!1},m=requestAnimationFrame(p);return p}let _e=null;const Gt=new Set;let Ut=new Map,bt=!1,Rn=!1;const ep={Tab:!0,Escape:!0};function tr(t,e){for(let n of Gt)n(t,e)}function tp(t){return!(t.metaKey||!Me()&&t.altKey||t.ctrlKey||t.key==="Control"||t.key==="Shift"||t.key==="Meta")}function _n(t){bt=!0,!Be.isOpening&&tp(t)&&(_e="keyboard",tr("keyboard",t))}function Tt(t){_e="pointer","pointerType"in t&&t.pointerType,(t.type==="mousedown"||t.type==="pointerdown")&&(bt=!0,tr("pointer",t))}function il(t){!Be.isOpening&&xs(t)&&(bt=!0,_e="virtual")}function sl(t){if(Bn)return;let e=V(t),n=he(e),r=ee(e);if(e===n){Rn=!0;return}e===r||!t.isTrusted||(!bt&&!Rn&&(_e="virtual",tr("virtual",t)),bt=!1,Rn=!1)}function ll(){Bn||(bt=!1,Rn=!0)}function Hn(t){if(typeof window>"u"||typeof document>"u")return;const e=he(t),n=ee(t);if(Ut.get(e))return;let r=e.HTMLElement.prototype.focus;Reflect.defineProperty(e.HTMLElement.prototype,"focus",{configurable:!0,writable:!0,value:function(){bt=!0,r.apply(this,arguments)}}),n.addEventListener("keydown",_n,!0),n.addEventListener("keyup",_n,!0),n.addEventListener("click",il,!0),e.addEventListener("focus",sl,!0),e.addEventListener("blur",ll,!1),typeof PointerEvent<"u"&&(n.addEventListener("pointerdown",Tt,!0),n.addEventListener("pointermove",Tt,!0),n.addEventListener("pointerup",Tt,!0)),e.addEventListener("beforeunload",()=>{al(t)},{once:!0}),Ut.set(e,{focus:r})}const al=(t,e)=>{const n=he(t),r=ee(t);e&&r.removeEventListener("DOMContentLoaded",e),Ut.has(n)&&(Reflect.defineProperty(n.HTMLElement.prototype,"focus",{configurable:!0,writable:!0,value:Ut.get(n).focus}),r.removeEventListener("keydown",_n,!0),r.removeEventListener("keyup",_n,!0),r.removeEventListener("click",il,!0),n.removeEventListener("focus",sl,!0),n.removeEventListener("blur",ll,!1),typeof PointerEvent<"u"&&(r.removeEventListener("pointerdown",Tt,!0),r.removeEventListener("pointermove",Tt,!0),r.removeEventListener("pointerup",Tt,!0)),Ut.delete(n))};function np(t){const e=ee(t);let n;return e.readyState!=="loading"?Hn(t):(n=()=>{Hn(t)},e.addEventListener("DOMContentLoaded",n)),()=>al(t,n)}typeof document<"u"&&np();function At(){return _e!=="pointer"}function Je(){return _e}function Ln(t){_e=t,tr(t,null)}function rp(){Hn();let[t,e]=a.useState(_e);return a.useEffect(()=>{let n=()=>{e(_e)};return Gt.add(n),()=>{Gt.delete(n)}},[]),nt()?null:t}const op=new Set(["checkbox","radio","range","color","file","image","button","submit","reset"]);function ip(t,e,n){let r=n?V(n):void 0,o=ee(r),i=he(r);const s=typeof i<"u"?i.HTMLInputElement:HTMLInputElement,l=typeof i<"u"?i.HTMLTextAreaElement:HTMLTextAreaElement,u=typeof i<"u"?i.HTMLElement:HTMLElement,c=typeof i<"u"?i.KeyboardEvent:KeyboardEvent;let d=re(o);return t=t||d instanceof s&&!op.has(d.type)||d instanceof l||d instanceof u&&d.isContentEditable,!(t&&e==="keyboard"&&n instanceof c&&!ep[n.key])}function sp(t={}){let{isTextInput:e,autoFocus:n}=t,[r,o]=a.useState(n||At());return ul(i=>{o(i)},[e],{isTextInput:e}),{isFocusVisible:r}}function ul(t,e,n){Hn(),a.useEffect(()=>{if(n?.enabled===!1)return;let r=(o,i)=>{ip(!!n?.isTextInput,o,i)&&t(At())};return Gt.add(r),()=>{Gt.delete(r)}},e)}function et(t){if(!t.isConnected)return;const e=ee(t);if(Je()==="virtual"){let n=re(e);ys(()=>{const r=re(e);(r===n||r===e.body)&&t.isConnected&&ce(t)})}else ce(t)}function cl(t){let{isDisabled:e,onFocus:n,onBlur:r,onFocusChange:o}=t;const i=a.useCallback(u=>{if(V(u)===u.currentTarget)return r&&r(u),o&&o(!1),!0},[r,o]),s=ol(i),l=a.useCallback(u=>{let c=V(u);const d=ee(c),f=d?re(d):re();c===u.currentTarget&&c===f&&(n&&n(u),o&&o(!0),s(u))},[o,n,s]);return{focusProps:{onFocus:!e&&(n||o||r)?l:void 0,onBlur:!e&&(r||o)?i:void 0}}}function yn(t){if(t)return e=>{let n=!0,r={...e,preventDefault(){e.preventDefault()},isDefaultPrevented(){return e.isDefaultPrevented()},stopPropagation(){n=!0},continuePropagation(){n=!1,typeof e.continuePropagation=="function"&&e.continuePropagation()},isPropagationStopped(){return n}};t(r),n&&!(typeof e.isPropagationStopped=="function"&&e.isPropagationStopped())&&e.stopPropagation()}}const lp=new Set(["shift","alt","control","meta","mod"]),ap=["Alt","Control","Meta","Shift"];function up(t){let e=new Set;return t.alt&&e.add("Alt"),t.shift&&e.add("Shift"),t.ctrl&&e.add("Control"),t.meta&&e.add("Meta"),t.mod&&e.add(Me()?"Meta":"Control"),e}function cp(t){let e=new Set;return t.altKey&&e.add("Alt"),t.ctrlKey&&e.add("Control"),t.metaKey&&e.add("Meta"),t.shiftKey&&e.add("Shift"),e}function dl(t){return ap.filter(e=>t.has(e))}function dp(t){let e=t.split("+").reduce((n,r)=>{let o=r.toLowerCase();return lp.has(o)?o==="shift"?n.shift=!0:o==="alt"?n.alt=!0:o==="control"?n.ctrl=!0:o==="meta"?n.meta=!0:o==="mod"&&(n.mod=!0):n.key=r,n},{shift:!1,alt:!1,ctrl:!1,meta:!1,mod:!1,key:""});if(e.key==="")throw new Error(`Invalid keyboard shortcut: "${t}". Must include exactly one non-modifier key (e.g. "a", "Enter", "ArrowDown"). Combine any of Shift, Alt, Ctrl, Meta, and Mod.`);return e}function fl(t){return t.toLowerCase()}const fp={space:" ",esc:"escape",del:"delete",ins:"insert",left:"arrowleft",right:"arrowright",up:"arrowup",down:"arrowdown",pageup:"pageup",pagedown:"pagedown"};function pp(t){let e=fl(t),n=fp[e];return n??e}function hp(t){let e=dl(up(t)),n=pp(t.key);return e.length>0?`${e.join("+")}+${n}`:n}function mp(t){let e=dl(cp(t)),n=fl(t.key);return(e.length>0?`${e.join("+")}+`:"")+n}function bp(t){let e=new Map;for(let[n,r]of Object.entries(t)){let o=dp(n);e.set(hp(o),r)}return n=>{let r=mp(n),o=e.get(r),i=o?.(n);i===void 0&&o!==void 0?i={shouldContinuePropagation:!1,shouldPreventDefault:!0}:typeof i=="boolean"&&(i={shouldContinuePropagation:!i,shouldPreventDefault:i}),i?.shouldPreventDefault&&n.preventDefault(),(!o||i?.shouldContinuePropagation)&&n.continuePropagation()}}function tt(t){let{shortcuts:e,allowRepeats:n=!1,allowComposing:r=!1}=t,o,i;if(e){let s=bp(e),l=yn(c=>{if(!Y(c.currentTarget,V(c))){c.continuePropagation();return}if(c.nativeEvent?.repeat&&!n||c.nativeEvent?.isComposing&&!r){c.continuePropagation();return}s(c)}),u=yn(c=>{if(!Y(c.currentTarget,V(c))){c.continuePropagation();return}if(c.nativeEvent?.repeat&&!n||c.nativeEvent?.isComposing&&!r){c.continuePropagation();return}c.continuePropagation()});o=t.onKeyDown?ht(t.onKeyDown,l):l,i=t.onKeyUp?ht(t.onKeyUp,u):u}else o=yn(t.onKeyDown),i=yn(t.onKeyUp);return{keyboardProps:t.isDisabled?{}:{onKeyDown:o,onKeyUp:i}}}let qt=$.createContext(null);function gp(t){let e=a.useContext(qt)||{};ro(e,t);let{ref:n,...r}=e;return r}const J1=$.forwardRef(function(e,n){let{children:r,...o}=e,i=rt(n),s={...o,ref:i};return $.createElement(qt.Provider,{value:s},r)});function nr(t,e){let{focusProps:n}=cl(t),{keyboardProps:r}=tt(t),o=Q(n,r),i=gp(e),s=t.isDisabled?{}:i,l=a.useRef(t.autoFocus);a.useEffect(()=>{l.current&&e.current&&et(e.current),l.current=!1},[e]);let u=t.excludeFromTabOrder?-1:0;return t.isDisabled&&(u=void 0),{focusableProps:Q({...o,tabIndex:u},s)}}let Ct="default",zr="",In=new WeakMap;function vp(t){if(gt()&&pt()){if(Ct==="default"){const e=ee(t);zr=e.documentElement.style.webkitUserSelect,e.documentElement.style.webkitUserSelect="none"}Ct="disabled"}else if(t instanceof HTMLElement||t instanceof SVGElement){let e="userSelect"in t.style?"userSelect":"webkitUserSelect";In.set(t,t.style[e]),t.style[e]="none"}}function ii(t){if(gt()&&pt()){if(Ct!=="disabled")return;Ct="restoring",setTimeout(()=>{ys(()=>{if(Ct==="restoring"){const e=ee(t);e.documentElement.style.webkitUserSelect==="none"&&(e.documentElement.style.webkitUserSelect=zr||""),zr="",Ct="default"}})},300)}else if((t instanceof HTMLElement||t instanceof SVGElement)&&t&&In.has(t)){let e=In.get(t),n="userSelect"in t.style?"userSelect":"webkitUserSelect";t.style[n]==="none"&&(t.style[n]=e),t.getAttribute("style")===""&&t.removeAttribute("style"),In.delete(t)}}const Yt=$.createContext({register:()=>{}});Yt.displayName="PressResponderContext";function yp(t){let e=a.useContext(Yt);if(e){let{register:n,ref:r,...o}=e;t=Q(o,t),n()}return ro(e,t.ref),t}class $n{#e;constructor(e,n,r,o){this.#e=!0;const s=(o?.target??r.currentTarget)?.getBoundingClientRect();let l,u=0,c,d=null;r.clientX!=null&&r.clientY!=null&&(c=r.clientX,d=r.clientY),s&&(c!=null&&d!=null?(l=c-s.left,u=d-s.top):(l=s.width/2,u=s.height/2)),this.type=e,this.pointerType=n,this.target=r.currentTarget,this.shiftKey=r.shiftKey,this.metaKey=r.metaKey,this.ctrlKey=r.ctrlKey,this.altKey=r.altKey,this.x=l,this.y=u,this.key=r.key}continuePropagation(){this.#e=!1}get shouldStopPropagation(){return this.#e}}const si=Symbol("linkClicked"),li="react-aria-pressable-style",ai="data-react-aria-pressable";function on(t){let{onPress:e,onPressChange:n,onPressStart:r,onPressEnd:o,onPressUp:i,onClick:s,isDisabled:l,isPressed:u,preventFocusOnPress:c,shouldCancelOnPointerExit:d,allowTextSelectionOnPress:f,ref:p,...m}=yp(t),[h,g]=a.useState(!1),b=a.useRef({isPressed:!1,ignoreEmulatedMouseEvents:!1,didFirePressStart:!1,isTriggeringEvent:!1,activePointerId:null,target:null,isOverTarget:!1,pointerType:null,disposables:[]}),{addGlobalListener:y,removeAllGlobalListeners:v}=Jn(),w=a.useCallback((C,R)=>{let H=b.current;if(l||H.didFirePressStart)return!1;let L=!0;if(H.isTriggeringEvent=!0,r){let q=new $n("pressstart",R,C);r(q),L=q.shouldStopPropagation}return n&&n(!0),H.isTriggeringEvent=!1,H.didFirePressStart=!0,g(!0),L},[l,r,n]),x=a.useCallback((C,R,H=!0)=>{let L=b.current;if(!L.didFirePressStart)return!1;L.didFirePressStart=!1,L.isTriggeringEvent=!0;let q=!0;if(o){let T=new $n("pressend",R,C);o(T),q=T.shouldStopPropagation}if(n&&n(!1),g(!1),e&&H&&!l){let T=new $n("press",R,C);e(T),q&&=T.shouldStopPropagation}return L.isTriggeringEvent=!1,q},[l,o,n,e]),F=Ce(x),K=a.useCallback((C,R)=>{let H=b.current;if(l)return!1;if(i){H.isTriggeringEvent=!0;let L=new $n("pressup",R,C);return i(L),H.isTriggeringEvent=!1,L.shouldStopPropagation}return!0},[l,i]),P=Ce(K),S=a.useCallback(C=>{let R=b.current;if(R.isPressed&&R.target){R.didFirePressStart&&R.pointerType!=null&&x(at(R.target,C),R.pointerType,!1),R.isPressed=!1,R.isOverTarget=!1,R.activePointerId=null,R.pointerType=null,v(),f||ii(R.target);for(let H of R.disposables)H();R.disposables=[]}},[f,v,x]),I=Ce(S);a.useEffect(()=>{l&&b.current.isPressed&&I({currentTarget:b.current.target,shiftKey:!1,ctrlKey:!1,metaKey:!1,altKey:!1})},[l]);let M=a.useCallback(C=>{d&&S(C)},[d,S]),O=a.useCallback(C=>{l||s?.(C)},[l,s]),z=a.useCallback((C,R)=>{if(!l&&s){let H=new MouseEvent("click",C);rl(H,R),s(vo(H))}},[l,s]),U=a.useMemo(()=>{let C=b.current,R={onKeyDown(L){if(Er(L.nativeEvent,L.currentTarget)&&Y(L.currentTarget,V(L))){ui(V(L),L.key)&&L.preventDefault();let q=!0;!C.isPressed&&!L.repeat&&(C.target=L.currentTarget,C.isPressed=!0,C.pointerType="keyboard",q=w(L,"keyboard"));let T=L.currentTarget,A=j=>{Er(j,T)&&!j.repeat&&Y(T,V(j))&&C.target&&P(at(C.target,j),"keyboard")};y(ee(L.currentTarget),"keyup",ht(A,H),!0),q&&L.stopPropagation(),L.metaKey&&Me()&&C.metaKeyEvents?.set(L.key,L.nativeEvent)}else L.key==="Meta"&&(C.metaKeyEvents=new Map)},onClick(L){if(!(L&&!Y(L.currentTarget,V(L)))&&L&&L.button===0&&!C.isTriggeringEvent&&!Be.isOpening){let q=!0;if(l&&L.preventDefault(),!C.ignoreEmulatedMouseEvents&&!C.isPressed&&(C.pointerType==="virtual"||xs(L.nativeEvent))){let T=w(L,"virtual"),A=P(L,"virtual"),j=F(L,"virtual");O(L),q=T&&A&&j}else if(C.isPressed&&C.pointerType!=="keyboard"){let T=C.pointerType||L.nativeEvent.pointerType||"virtual",A=P(at(L.currentTarget,L),T),j=F(at(L.currentTarget,L),T,!0);q=A&&j,C.isOverTarget=!1,O(L),I(L)}C.ignoreEmulatedMouseEvents=!1,q&&L.stopPropagation()}}},H=L=>{if(C.isPressed&&C.target&&Er(L,C.target)){ui(V(L),L.key)&&L.preventDefault();let q=V(L),T=Y(C.target,q);F(at(C.target,L),"keyboard",T),T&&z(L,C.target),v(),L.key!=="Enter"&&yo(C.target)&&Y(C.target,q)&&!L[si]&&(L[si]=!0,Be(C.target,L,!1)),C.isPressed=!1,C.metaKeyEvents?.delete(L.key)}else if(L.key==="Meta"&&C.metaKeyEvents?.size){let q=C.metaKeyEvents;C.metaKeyEvents=void 0;for(let T of q.values())C.target?.dispatchEvent(new KeyboardEvent("keyup",T))}};if(typeof PointerEvent<"u"){R.onPointerDown=T=>{if(T.button!==0||!Y(T.currentTarget,V(T)))return;if(xd(T.nativeEvent)){C.pointerType="virtual";return}C.pointerType=T.pointerType;let A=!0;if(!C.isPressed){C.isPressed=!0,C.isOverTarget=!0,C.activePointerId=T.pointerId,C.target=T.currentTarget,f||vp(C.target),A=w(T,C.pointerType);let j=V(T);"releasePointerCapture"in j&&("hasPointerCapture"in j?j.hasPointerCapture(T.pointerId)&&j.releasePointerCapture(T.pointerId):j.releasePointerCapture(T.pointerId)),y(ee(T.currentTarget),"pointerup",L,!1),y(ee(T.currentTarget),"pointercancel",q,!1)}A&&T.stopPropagation()},R.onMouseDown=T=>{if(Y(T.currentTarget,V(T))&&T.button===0){if(c){let A=Q0(T.target);A&&C.disposables.push(A)}T.stopPropagation()}},R.onPointerUp=T=>{!Y(T.currentTarget,V(T))||C.pointerType==="virtual"||T.button===0&&!C.isPressed&&P(T,C.pointerType||T.pointerType)},R.onPointerEnter=T=>{T.pointerId===C.activePointerId&&C.target&&!C.isOverTarget&&C.pointerType!=null&&(C.isOverTarget=!0,w(at(C.target,T),C.pointerType))},R.onPointerLeave=T=>{T.pointerId===C.activePointerId&&C.target&&C.isOverTarget&&C.pointerType!=null&&(C.isOverTarget=!1,F(at(C.target,T),C.pointerType,!1),M(T))};let L=T=>{if(T.pointerId===C.activePointerId&&C.isPressed&&T.button===0&&C.target){if(Y(C.target,V(T))&&C.pointerType!=null){let A=!1,j=setTimeout(()=>{C.isPressed&&C.target instanceof HTMLElement&&(A?I(T):(ce(C.target),C.target.click()))},80);y(T.currentTarget,"click",()=>A=!0,!0),C.disposables.push(()=>clearTimeout(j))}else I(T);C.isOverTarget=!1}},q=T=>{I(T)};R.onDragStart=T=>{Y(T.currentTarget,V(T))&&I(T)}}return R},[y,l,c,v,f,M,w,O,z]);return a.useEffect(()=>{if(!p)return;const C=ee(p.current);if(!C||!C.head||C.getElementById(li))return;const R=C.createElement("style");R.id=li;let H=Ss(C);H&&(R.nonce=H),R.textContent=` +@layer { + [${ai}] { + touch-action: pan-x pan-y pinch-zoom; + } +} + `.trim(),C.head.prepend(R)},[p]),a.useEffect(()=>{let C=b.current;return()=>{f||ii(C.target??void 0);for(let R of C.disposables)R();C.disposables=[]}},[f]),{isPressed:u||h,pressProps:Q(m,U,{[ai]:!0})}}function yo(t){return t.tagName==="A"&&t.hasAttribute("href")}function Er(t,e){const{key:n,code:r}=t,o=e,i=o.getAttribute("role");return(n==="Enter"||n===" "||n==="Spacebar"||r==="Space")&&!(o instanceof he(o).HTMLInputElement&&!pl(o,n)||o instanceof he(o).HTMLTextAreaElement||o.isContentEditable)&&!((i==="link"||!i&&yo(o))&&n!=="Enter")}function at(t,e){let n=e.clientX,r=e.clientY;return{currentTarget:t,shiftKey:e.shiftKey,ctrlKey:e.ctrlKey,metaKey:e.metaKey,altKey:e.altKey,clientX:n,clientY:r,key:e.key}}function $p(t){return t instanceof HTMLInputElement?!1:t instanceof HTMLButtonElement?t.type!=="submit"&&t.type!=="reset":!yo(t)}function ui(t,e){return Me()&&e==="Enter"?!1:t instanceof HTMLInputElement?e==="Enter"&&(t.type==="checkbox"||t.type==="radio")?!1:!pl(t,e):$p(t)}const xp=new Set(["checkbox","radio","range","color","file","image","button","submit","reset"]);function pl(t,e){return t.type==="checkbox"||t.type==="radio"?e===" ":xp.has(t.type)}function Ep(t,e){let{elementType:n="button",isDisabled:r,onPress:o,onPressStart:i,onPressEnd:s,onPressUp:l,onPressChange:u,preventFocusOnPress:c,allowFocusWhenDisabled:d,onClick:f,href:p,target:m,rel:h,type:g="button"}=t,b;n==="button"?b={type:g,disabled:r,form:t.form,formAction:t.formAction,formEncType:t.formEncType,formMethod:t.formMethod,formNoValidate:t.formNoValidate,formTarget:t.formTarget,name:t.name,value:t.value}:b={role:"button",href:n==="a"&&!r?p:void 0,target:n==="a"?m:void 0,type:n==="input"?g:void 0,disabled:n==="input"?r:void 0,"aria-disabled":!r||n==="input"?void 0:r,rel:n==="a"?h:void 0};let{pressProps:y,isPressed:v}=on({onPressStart:i,onPressEnd:s,onPressChange:u,onPress:o,onPressUp:l,onClick:f,isDisabled:r,preventFocusOnPress:c,ref:e}),{focusableProps:w}=nr(t,e);d&&(w.tabIndex=r?-1:w.tabIndex);let x=Q(w,y,de(t,{labelable:!0}));return{isPressed:v,buttonProps:Q(b,x,{"aria-haspopup":t["aria-haspopup"],"aria-expanded":t["aria-expanded"],"aria-controls":t["aria-controls"],"aria-pressed":t["aria-pressed"],"aria-current":t["aria-current"],"aria-disabled":t["aria-disabled"]})}}function sn(t){let{isDisabled:e,onBlurWithin:n,onFocusWithin:r,onFocusWithinChange:o}=t,i=a.useRef({isFocusWithin:!1}),{addGlobalListener:s,removeAllGlobalListeners:l}=Jn(),u=a.useCallback(f=>{Y(f.currentTarget,V(f))&&i.current.isFocusWithin&&!Y(f.currentTarget,f.relatedTarget)&&(i.current.isFocusWithin=!1,l(),n&&n(f),o&&o(!1))},[n,o,i,l]),c=ol(u),d=a.useCallback(f=>{if(!Y(f.currentTarget,V(f)))return;let p=V(f);const m=ee(p),h=re(m);if(!i.current.isFocusWithin&&h===p){r&&r(f),o&&o(!0),i.current.isFocusWithin=!0,c(f);let g=f.currentTarget;s(m,"focus",b=>{let y=V(b);if(i.current.isFocusWithin&&!Y(g,y)){let v=new m.defaultView.FocusEvent("blur",{relatedTarget:y});rl(v,g);let w=vo(v);u(w)}},{capture:!0})}},[r,o,c,s,u]);return e?{focusWithinProps:{onFocus:void 0,onBlur:void 0}}:{focusWithinProps:{onFocus:d,onBlur:u}}}function Lt(t={}){let{autoFocus:e=!1,isTextInput:n,within:r}=t,o=a.useRef({isFocused:!1,isFocusVisible:e||At()}),[i,s]=a.useState(!1),[l,u]=a.useState(()=>o.current.isFocused&&o.current.isFocusVisible),c=a.useCallback(()=>u(o.current.isFocused&&o.current.isFocusVisible),[]),d=a.useCallback(m=>{o.current.isFocused=m,o.current.isFocusVisible=At(),s(m),c()},[c]);ul(m=>{o.current.isFocusVisible=m,c()},[n,i],{enabled:i,isTextInput:n});let{focusProps:f}=cl({isDisabled:r,onFocusChange:d}),{focusWithinProps:p}=sn({isDisabled:!r,onFocusWithinChange:d});return{isFocused:i,isFocusVisible:l,focusProps:r?p:f}}let Ur=!1,xn=0;function wp(){Ur=!0,setTimeout(()=>{Ur=!1},500)}function ci(t){t.pointerType==="touch"&&wp()}function Cp(){let t=ee(null);if(!(typeof t>"u"))return xn===0&&typeof PointerEvent<"u"&&t.addEventListener("pointerup",ci),xn++,()=>{xn--,!(xn>0)&&typeof PointerEvent<"u"&&t.removeEventListener("pointerup",ci)}}function Ue(t){let{onHoverStart:e,onHoverChange:n,onHoverEnd:r,isDisabled:o}=t,[i,s]=a.useState(!1),l=a.useRef({isHovered:!1,ignoreEmulatedMouseEvents:!1,pointerType:"",target:null}).current;a.useEffect(Cp,[]);let{addGlobalListener:u,removeAllGlobalListeners:c}=Jn(),{hoverProps:d,triggerHoverEnd:f}=a.useMemo(()=>{let p=(g,b)=>{if(l.pointerType=b,o||b==="touch"||l.isHovered||!Y(g.currentTarget,V(g)))return;l.isHovered=!0;let y=g.currentTarget;l.target=y,u(ee(V(g)),"pointerover",v=>{l.isHovered&&l.target&&!Y(l.target,V(v))&&m(v,v.pointerType)},{capture:!0}),e&&e({type:"hoverstart",target:y,pointerType:b}),n&&n(!0),s(!0)},m=(g,b)=>{let y=l.target;l.pointerType="",l.target=null,!(b==="touch"||!l.isHovered||!y)&&(l.isHovered=!1,c(),r&&r({type:"hoverend",target:y,pointerType:b}),n&&n(!1),s(!1))},h={};return typeof PointerEvent<"u"&&(h.onPointerEnter=g=>{Ur&&g.pointerType==="mouse"||p(g,g.pointerType)},h.onPointerLeave=g=>{!o&&Y(g.currentTarget,V(g))&&m(g,g.pointerType)}),{hoverProps:h,triggerHoverEnd:m}},[e,n,r,o,l,u,c]);return a.useEffect(()=>{o&&f({currentTarget:l.target},l.pointerType)},[o]),{hoverProps:d,isHovered:i}}const $o=a.createContext({}),xo=rn(function(e,n){[e,n]=me(e,n,$o);let r=e,{isPending:o}=r,{buttonProps:i,isPressed:s}=Ep(e,n);i=Pp(i,o);let{focusProps:l,isFocused:u,isFocusVisible:c}=Lt(e),{hoverProps:d,isHovered:f}=Ue({...e,isDisabled:e.isDisabled||o}),p={isHovered:f,isPressed:(r.isPressed||s)&&!o,isFocused:u,isFocusVisible:c,isDisabled:e.isDisabled||!1,isPending:o??!1},m=ve({...e,values:p,defaultClassName:"react-aria-Button"}),h=xe(i.id),g=xe(),b=i["aria-labelledby"];o&&(b?b=`${b} ${g}`:i["aria-label"]&&(b=`${h} ${g}`));let y=a.useRef(o);a.useEffect(()=>{let w={"aria-labelledby":b||h};(!y.current&&u&&o||y.current&&u&&!o)&&oi(w,"assertive"),y.current=o},[o,u,b,h]);let v=de(e,{global:!0});return delete v.onClick,$.createElement(ue.button,{...Q(v,m,i,l,d),type:i.type==="submit"&&o?"button":i.type,id:h,ref:n,"aria-labelledby":b,slot:e.slot||void 0,"aria-disabled":o?"true":i["aria-disabled"],"data-disabled":e.isDisabled||void 0,"data-pressed":p.isPressed||void 0,"data-hovered":f||void 0,"data-focused":u||void 0,"data-pending":o||void 0,"data-focus-visible":c||void 0},$.createElement(tl.Provider,{value:{id:g}},m.children))}),Sp=/Focus|Blur|Hover|Pointer(Enter|Leave|Over|Out)|Mouse(Enter|Leave|Over|Out)/;function Pp(t,e){if(e){for(const n in t)n.startsWith("on")&&!Sp.test(n)&&(t[n]=void 0);t.href=void 0,t.target=void 0}return t}const Tp=typeof document<"u"?$.useInsertionEffect??$.useLayoutEffect:()=>{};function rr(t,e,n){let[r,o]=a.useState(t||e),i=a.useRef(r),s=a.useRef(t!==void 0),l=t!==void 0;a.useEffect(()=>{s.current,s.current=l},[l]);let u=l?t:r;Tp(()=>{i.current=u});let[,c]=a.useReducer(()=>({}),{}),d=a.useCallback((f,...p)=>{let m=typeof f=="function"?f(i.current):f;Object.is(i.current,m)||(i.current=m,o(m),c(),n?.(m,...p))},[n]);return[u,d]}const hl=a.createContext({}),Q1=a.forwardRef(function(e,n){[e,n]=me(e,n,hl);let{children:r,level:o=3,className:i,...s}=e,l=ue[`h${o}`];return $.createElement(l,{...s,ref:n,className:i??"react-aria-Heading"},r)}),Ge=t=>t?"true":void 0;function Se(t,e){return ho(t,(n,r)=>{const o=typeof e=="function"?e(r)??"":e??"";return $0(o,n??"")??""})}const be=(t,e,n)=>typeof t=="function"?t({...n??{},className:e}):e,eg=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M2.97 5.47a.75.75 0 0 1 1.06 0L8 9.44l3.97-3.97a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 0 1 0-1.06",fill:"currentColor",fillRule:"evenodd"})}),tg=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M13.03 10.53a.75.75 0 0 1-1.06 0L8 6.56l-3.97 3.97a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06",fill:"currentColor",fillRule:"evenodd"})}),ng=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M10.53 2.97a.75.75 0 0 1 0 1.06L6.56 8l3.97 3.97a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 0",fill:"currentColor",fillRule:"evenodd"})}),kp=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M5.47 2.97a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06L9.44 8 5.47 4.03a.75.75 0 0 1 0-1.06Z",fill:"currentColor",fillRule:"evenodd"})}),Mp=({height:t=9,width:e=9,...n})=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:t,role:"presentation",viewBox:"0 0 7 7",width:e,xmlns:"http://www.w3.org/2000/svg",...n,children:E.jsx("path",{d:"M1.20592 6.84333L0.379822 6.01723L4.52594 1.8672H1.37819L1.38601 0.731812H6.48742V5.83714H5.34421L5.35203 2.6933L1.20592 6.84333Z",fill:"currentColor"})}),Ap=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M3.47 3.47a.75.75 0 0 1 1.06 0L8 6.94l3.47-3.47a.75.75 0 1 1 1.06 1.06L9.06 8l3.47 3.47a.75.75 0 1 1-1.06 1.06L8 9.06l-3.47 3.47a.75.75 0 0 1-1.06-1.06L6.94 8 3.47 4.53a.75.75 0 0 1 0-1.06Z",fill:"currentColor",fillRule:"evenodd"})}),jn=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M8 13.5a5.5 5.5 0 1 0 0-11a5.5 5.5 0 0 0 0 11M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14m1-9.5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25 3a.75.75 0 0 0-1.5 0V11a.75.75 0 0 0 1.5 0z",fill:"currentColor",fillRule:"evenodd"})}),ml=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M7.134 2.994L2.217 11.5a1 1 0 0 0 .866 1.5h9.834a1 1 0 0 0 .866-1.5L8.866 2.993a1 1 0 0 0-1.732 0m3.03-.75c-.962-1.665-3.366-1.665-4.329 0L.918 10.749c-.963 1.666.24 3.751 2.165 3.751h9.834c1.925 0 3.128-2.085 2.164-3.751zM8 5a.75.75 0 0 1 .75.75v2a.75.75 0 0 1-1.5 0v-2A.75.75 0 0 1 8 5m1 5.75a1 1 0 1 1-2 0a1 1 0 0 1 2 0",fill:"currentColor",fillRule:"evenodd"})}),bl=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M8 13.5a5.5 5.5 0 1 0 0-11a5.5 5.5 0 0 0 0 11M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14m1-4.5a1 1 0 1 1-2 0a1 1 0 0 1 2 0M8.75 5a.75.75 0 0 0-1.5 0v2.5a.75.75 0 0 0 1.5 0z",fill:"currentColor",fillRule:"evenodd"})}),gl=t=>E.jsx("svg",{"aria-hidden":"true",fill:"none",height:16,role:"presentation",viewBox:"0 0 16 16",width:16,xmlns:"http://www.w3.org/2000/svg",...t,children:E.jsx("path",{clipRule:"evenodd",d:"M13.5 8a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0m-3.9-1.55a.75.75 0 1 0-1.2-.9L7.419 8.858L6.03 7.47a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.13-.08z",fill:"currentColor",fillRule:"evenodd"})}),Eo=a.createContext({}),ln=a.createContext({}),Rp={variant:"default"},di=({children:t,className:e,status:n,...r})=>{const o=$.useMemo(()=>w0({status:n}),[n]),i=$.useMemo(()=>({slots:o,status:n}),[o,n]);return E.jsx(ln,{value:i,children:E.jsx(Eo,{value:Rp,children:E.jsx(ge.div,{className:o?.base({className:e}),"data-slot":"alert-root",...r,children:t})})})},Lp=({children:t,className:e,...n})=>{const{slots:r,status:o}=a.use(ln),i=()=>{switch(o){case"accent":return E.jsx(jn,{"data-slot":"alert-default-icon"});case"success":return E.jsx(gl,{"data-slot":"alert-default-icon"});case"warning":return E.jsx(ml,{"data-slot":"alert-default-icon"});case"danger":return E.jsx(bl,{"data-slot":"alert-default-icon"});default:return E.jsx(jn,{"data-slot":"alert-default-icon"})}};return E.jsx(ge.div,{className:be(r?.indicator,e),"data-slot":"alert-indicator",...n,children:t??i()})},Ip=({children:t,className:e,...n})=>{const{slots:r}=a.use(ln);return E.jsx(ge.div,{className:be(r?.content,e),"data-slot":"alert-content",...n,children:t})},Fp=({children:t,className:e,...n})=>{const{slots:r}=a.use(ln);return E.jsx(ge.p,{className:be(r?.title,e),"data-slot":"alert-title",...n,children:t})},Kp=({children:t,className:e,...n})=>{const{slots:r}=a.use(ln);return E.jsx(ge.span,{className:be(r?.description,e),"data-slot":"alert-description",...n,children:t})},En=Object.assign(di,{Root:di,Indicator:Lp,Content:Ip,Title:Fp,Description:Kp}),vl=a.createContext({placement:"bottom"}),rg=a.forwardRef(function(e,n){[e,n]=me(e,n,vl);let r=e.placement,o={position:"absolute",transform:r==="top"||r==="bottom"?"translateX(-50%)":"translateY(-50%)"};r!=null&&(o[r]="100%");let i=ve({...e,defaultClassName:"react-aria-OverlayArrow",values:{placement:r}});i.style&&Object.keys(i.style).forEach(l=>i.style[l]===void 0&&delete i.style[l]);let s=de(e);return $.createElement(ue.div,{...s,...i,style:{...o,...i.style},ref:n,"data-placement":r})}),Dp=typeof HTMLElement<"u"&&"inert"in HTMLElement.prototype;function fi(t){return t.dataset.liveAnnouncer==="true"||t.dataset.reactAriaTopLayer!==void 0}let Ot=new WeakMap,we=[];function Np(t,e){let n=he(t?.[0]),r=e instanceof n.Element?{root:e}:e,o=r?.root??document.body,i=r?.shouldUseInert&&Dp,s=new Set(t),l=new Set,u=b=>i&&b instanceof n.HTMLElement?b.inert:b.getAttribute("aria-hidden")==="true",c=(b,y)=>{i&&b instanceof n.HTMLElement?b.inert=y:y?b.setAttribute("aria-hidden","true"):(b.removeAttribute("aria-hidden"),b instanceof n.HTMLElement&&(b.inert=!1))},d=new Set;if(Ne()){let b=o.getRootNode();for(let y of t){let v=y.getRootNode();for(;Xn(v)&&v!==b;)d.add(v),v=v.host.getRootNode()}}let f=b=>{for(let x of b.querySelectorAll("[data-live-announcer], [data-react-aria-top-layer]"))s.add(x);let y=x=>{if(l.has(x)||s.has(x)||x.parentElement&&l.has(x.parentElement)&&x.parentElement.getAttribute("role")!=="row")return NodeFilter.FILTER_REJECT;for(let F of s)if(Y(x,F))return NodeFilter.FILTER_SKIP;return NodeFilter.FILTER_ACCEPT},v=gs(ee(b),b,NodeFilter.SHOW_ELEMENT,{acceptNode:y}),w=y(b);if(w===NodeFilter.FILTER_ACCEPT&&p(b),w!==NodeFilter.FILTER_REJECT){let x=v.nextNode();for(;x!=null;)p(x),x=v.nextNode()}},p=b=>{let y=Ot.get(b)??0;u(b)&&y===0||(y===0&&c(b,!0),l.add(b),Ot.set(b,y+1))};we.length&&we[we.length-1].disconnect(),f(o);let m=new MutationObserver(b=>{for(let y of b)if(y.type==="childList"){if(y.target.isConnected&&![...s,...l].some(v=>Y(v,y.target)))for(let v of y.addedNodes)(v instanceof HTMLElement||v instanceof SVGElement)&&fi(v)?s.add(v):v instanceof Element&&f(v);if(Ne()){for(let v of d)if(!v.isConnected){m.disconnect();break}}}});m.observe(o,{childList:!0,subtree:!0});let h=new Set;if(Ne())for(let b of d){let y=new MutationObserver(v=>{for(let w of v)if(w.type==="childList"){if(w.target.isConnected&&![...s,...l].some(x=>Y(x,w.target)))for(let x of w.addedNodes)(x instanceof HTMLElement||x instanceof SVGElement)&&fi(x)?s.add(x):x instanceof Element&&f(x);if(Ne()){for(let x of d)if(!x.isConnected){m.disconnect();break}}}});y.observe(b,{childList:!0,subtree:!0}),h.add(y)}let g={visibleNodes:s,hiddenNodes:l,observe(){m.observe(o,{childList:!0,subtree:!0})},disconnect(){m.disconnect()}};return we.push(g),()=>{if(m.disconnect(),Ne())for(let b of h)b.disconnect();for(let b of l){let y=Ot.get(b);y!=null&&(y===1?(c(b,!1),Ot.delete(b)):Ot.set(b,y-1))}g===we[we.length-1]?(we.pop(),we.length&&we[we.length-1].observe()):we.splice(we.indexOf(g),1)}}function Op(t){let e=we[we.length-1];if(e&&!e.visibleNodes.has(t))return e.visibleNodes.add(t),()=>{e.visibleNodes.delete(t)}}const Te={top:"top",bottom:"top",left:"left",right:"left"},zn={top:"bottom",bottom:"top",left:"right",right:"left"},Vp={top:"left",left:"top"},Zr={top:"height",left:"width"},yl={width:"totalWidth",height:"totalHeight"},wn={};let Bp=()=>typeof document<"u"?window.visualViewport:null;function pi(t,e){let n=0,r=0,o=0,i=0,s=0,l=0,u={},c=(e?.scale??1)>1;if(t.tagName==="BODY"||t.tagName==="HTML"){let d=document.documentElement;o=d.clientWidth,i=d.clientHeight,n=e?.width??o,r=e?.height??i,u.top=d.scrollTop||t.scrollTop,u.left=d.scrollLeft||t.scrollLeft,e&&(s=Math.max(0,e.pageTop-(u.top??0)),l=Math.max(0,e.pageLeft-(u.left??0)))}else({width:n,height:r,top:s,left:l}=Xt(t,!1)),u.top=t.scrollTop,u.left=t.scrollLeft,o=n,i=r;return pt()&&(t.tagName==="BODY"||t.tagName==="HTML")&&c&&(u.top=0,u.left=0,s=e?.pageTop??0,l=e?.pageLeft??0),{width:n,height:r,totalWidth:o,totalHeight:i,scroll:u,top:s,left:l}}function _p(t){return{top:t.scrollTop,left:t.scrollLeft,width:t.scrollWidth,height:t.scrollHeight}}function hi(t,e,n,r,o,i,s){let l=o.scroll[t]??0,u=r[Zr[t]],c=s[t]+r.scroll[Te[t]]+i,d=s[t]+r.scroll[Te[t]]+u-i,f=e-l+r.scroll[Te[t]]+s[t]-r[Te[t]],p=e-l+n+r.scroll[Te[t]]+s[t]-r[Te[t]];return fd?Math.max(d-p,c-f):0}function Hp(t){let e=window.getComputedStyle(t);return{top:parseInt(e.marginTop,10)||0,bottom:parseInt(e.marginBottom,10)||0,left:parseInt(e.marginLeft,10)||0,right:parseInt(e.marginRight,10)||0}}function mi(t){if(wn[t])return wn[t];let[e,n]=t.split(" "),r=Te[e]||"right",o=Vp[r];Te[n]||(n="center");let i=Zr[r],s=Zr[o];return wn[t]={placement:e,crossPlacement:n,axis:r,crossAxis:o,size:i,crossSize:s},wn[t]}function wr(t,e,n,r,o,i,s,l,u,c,d){let{placement:f,crossPlacement:p,axis:m,crossAxis:h,size:g,crossSize:b}=r,y={};y[h]=t[h]??0,p==="center"?y[h]+=((t[b]??0)-(n[b]??0))/2:p!==h&&(y[h]+=(t[b]??0)-(n[b]??0)),y[h]+=i;const v=t[h]-n[b]+u+c,w=t[h]+t[b]-u-c;if(y[h]=Wt(y[h],v,w),f===m){let x=l?d[g]:d[yl[g]];y[zn[m]]=Math.floor(x-t[m]+o)}else y[m]=Math.floor(t[m]+t[g]+o);return y}function jp(t,e,n,r,o,i,s,l,u,c,d){let f=(t.top!=null?t.top:u[yl.height]-(t.bottom??0)-s)-(u.scroll.top??0),p=c?n.top:0,m={top:Math.max(e.top+p,(d?.offsetTop??e.top)+p),bottom:Math.min(e.top+e.height+p,(d?.offsetTop??0)+(d?.height??0))};return l!=="top"?Math.max(0,m.bottom-f-((o.top??0)+(o.bottom??0)+i)):Math.max(0,f+s-m.top-((o.top??0)+(o.bottom??0)+i))}function bi(t,e,n,r,o,i,s,l){let{placement:u,axis:c,size:d}=i;return u===c?Math.max(0,n[c]-(s.scroll[c]??0)-(t[c]+(l?e[c]:0))-(r[c]??0)-r[zn[c]]-o):Math.max(0,t[d]+t[c]+(l?e[c]:0)-n[c]-n[d]+(s.scroll[c]??0)-(r[c]??0)-r[zn[c]]-o)}function zp(t,e,n,r,o,i,s,l,u,c,d,f,p,m,h,g,b,y){let v=mi(t),{size:w,crossAxis:x,crossSize:F,placement:K,crossPlacement:P}=v,S=wr(e,l,n,v,d,f,c,p,h,g,u),I=d,M=bi(l,c,e,o,i+d,v,u,b);if(s&&n[w]>M){let k=mi(`${zn[K]} ${P}`),Z=wr(e,l,n,k,d,f,c,p,h,g,u);bi(l,c,e,o,i+d,k,u,b)>M&&(v=k,S=Z,I=d)}let O="bottom";v.axis==="top"?v.placement==="top"?O="top":v.placement==="bottom"&&(O="bottom"):v.crossAxis==="top"&&(v.crossPlacement==="top"?O="bottom":v.crossPlacement==="bottom"&&(O="top"));let z=hi(x,S[x],n[F],l,u,i,c);S[x]+=z;let U=jp(S,l,c,p,o,i,n.height,O,u,b,y);m&&m{if(!n||r===null)return;let o=i=>{let s=V(i);if(!e.current||s instanceof Node&&!Y(s,e.current)||s instanceof HTMLInputElement||s instanceof HTMLTextAreaElement)return;let l=r||$l.get(e.current);l&&l()};return Pt(bs(e.current),"scroll",o,!0)},[n,r,e])}let ut=typeof document<"u"?window.visualViewport:null;function Gp(t){let{direction:e}=it(),{arrowSize:n,targetRef:r,overlayRef:o,arrowRef:i,scrollRef:s=o,placement:l="bottom",containerPadding:u=12,shouldFlip:c=!0,boundaryElement:d=typeof document<"u"?document.body:null,offset:f=0,crossOffset:p=0,shouldUpdatePosition:m=!0,isOpen:h=!0,onClose:g,maxHeight:b,arrowBoundaryOffset:y=0,getTargetRect:v}=t,[w,x]=a.useState(null),F=[m,l,o.current,r.current,i?.current,s.current,u,c,d,f,p,h,e,b,y,n],K=a.useRef(ut?.scale);a.useEffect(()=>{h&&(K.current=ut?.scale)},[h]);let P=a.useCallback(()=>{if(m===!1||!h||!o.current||!r.current||!d||ut?.scale!==K.current)return;let M=null;if(s.current&&Xe(s.current)){let C=re()?.getBoundingClientRect(),R=s.current.getBoundingClientRect();M={type:"top",offset:(C?.top??0)-R.top},M.offset>R.height/2&&(M.type="bottom",M.offset=(C?.bottom??0)-R.bottom)}let O=o.current;!b&&o.current&&(O.style.top="0px",O.style.bottom="",O.style.maxHeight=(window.visualViewport?.height??window.innerHeight)+"px");let z=Up({placement:Yp(l,e),overlayNode:o.current,targetNode:r.current,scrollNode:s.current||o.current,padding:u,shouldFlip:c,boundaryElement:d,offset:f,crossOffset:p,maxHeight:b,arrowSize:n??(i?.current?or(i.current,!0).width:0),arrowBoundaryOffset:y,targetRect:v?.(r.current)});if(!z.position)return;O.style.top="",O.style.bottom="",O.style.left="",O.style.right="",Object.keys(z.position).forEach(C=>O.style[C]=z.position[C]+"px"),O.style.maxHeight=z.maxHeight!=null?z.maxHeight+"px":"";let U=re();if(M&&U&&s.current){let C=U.getBoundingClientRect(),R=s.current.getBoundingClientRect(),H=C[M.type]-R[M.type];s.current.scrollTop+=H-M.offset}x(z)},F);J(P,F),qp(P),Dn({ref:o,onResize:P}),Dn({ref:r,onResize:P});let S=a.useRef(!1);J(()=>{let M,O=()=>{S.current=!0,clearTimeout(M),M=setTimeout(()=>{S.current=!1},500),P()},z=()=>{S.current&&O()};ut?.addEventListener("resize",O),ut?.addEventListener("scroll",z);let U=Pt(bs(window),"scroll",z);return()=>{ut?.removeEventListener("resize",O),ut?.removeEventListener("scroll",z),U()}},[P]);let I=a.useCallback(()=>{S.current||g?.()},[g,S]);return Wp({triggerRef:r,isOpen:h,onClose:g&&I}),{overlayProps:{style:{position:w?"absolute":"fixed",top:w?void 0:0,left:w?void 0:0,zIndex:1e5,...w?.position,maxHeight:w?.maxHeight??"100vh"}},placement:w?.placement??null,triggerAnchorPoint:w?.triggerAnchorPoint??null,arrowProps:{"aria-hidden":"true",role:"presentation",style:{left:w?.arrowOffsetLeft,top:w?.arrowOffsetTop}},updatePosition:P}}function qp(t){J(()=>(window.addEventListener("resize",t,!1),()=>{window.removeEventListener("resize",t,!1)}),[t])}function Yp(t,e){return e==="rtl"?t.replace("start","right").replace("end","left"):t.replace("start","left").replace("end","right")}const yi=$.createContext(null),Wr="react-aria-focus-scope-restore";let le=null;function xl(t){let{children:e,contain:n,restoreFocus:r,autoFocus:o}=t,i=a.useRef(null),s=a.useRef(null),l=a.useRef([]),{parentNode:u}=a.useContext(yi)||{},c=a.useMemo(()=>new qr({scopeRef:l}),[l]);J(()=>{let p=u||fe.root;if(fe.getTreeNode(p.scopeRef)&&le&&!Un(le,p.scopeRef)){let m=fe.getTreeNode(le);m&&(p=m)}p.addChild(c),fe.addNode(c)},[c,u]),J(()=>{let p=fe.getTreeNode(l);p&&(p.contain=!!n)},[n]),J(()=>{let p=i.current?.nextSibling,m=[],h=g=>g.stopPropagation();for(;p&&p!==s.current;)m.push(p),p.addEventListener(Wr,h),p=p.nextSibling;return l.current=m,()=>{for(let g of m)g.removeEventListener(Wr,h)}},[e]),rh(l,r,n),eh(l,n),oh(l,r,n),nh(l,o),a.useEffect(()=>{const p=re(ee(l.current?l.current[0]:void 0));let m=null;if(Ae(p,l.current)){for(let h of fe.traverse())h.scopeRef&&Ae(p,h.scopeRef.current)&&(m=h);m===fe.getTreeNode(l)&&(le=m.scopeRef)}},[l]),J(()=>()=>{let p=fe.getTreeNode(l)?.parent?.scopeRef??null;(l===le||Un(l,le))&&(!p||fe.getTreeNode(p))&&(le=p),fe.removeTreeNode(l)},[l]);let d=a.useMemo(()=>Xp(l),[]),f=a.useMemo(()=>({focusManager:d,parentNode:c}),[c,d]);return $.createElement(yi.Provider,{value:f},$.createElement("span",{"data-focus-scope-start":!0,hidden:!0,ref:i}),e,$.createElement("span",{"data-focus-scope-end":!0,hidden:!0,ref:s}))}function Xp(t){return{focusNext(e={}){let n=t.current,{from:r,tabbable:o,wrap:i,accept:s}=e,l=r||re(ee(n[0]??void 0)),u=n[0].previousElementSibling,c=ct(n),d=Ve(c,{tabbable:o,accept:s},n);d.currentNode=Ae(l,n)?l:u;let f=d.nextNode();return!f&&i&&(d.currentNode=u,f=d.nextNode()),f&&Le(f,!0),f},focusPrevious(e={}){let n=t.current,{from:r,tabbable:o,wrap:i,accept:s}=e,l=r||re(ee(n[0]??void 0)),u=n[n.length-1].nextElementSibling,c=ct(n),d=Ve(c,{tabbable:o,accept:s},n);d.currentNode=Ae(l,n)?l:u;let f=d.previousNode();return!f&&i&&(d.currentNode=u,f=d.previousNode()),f&&Le(f,!0),f},focusFirst(e={}){let n=t.current,{tabbable:r,accept:o}=e,i=ct(n),s=Ve(i,{tabbable:r,accept:o},n);s.currentNode=n[0].previousElementSibling;let l=s.nextNode();return l&&Le(l,!0),l},focusLast(e={}){let n=t.current,{tabbable:r,accept:o}=e,i=ct(n),s=Ve(i,{tabbable:r,accept:o},n);s.currentNode=n[n.length-1].nextElementSibling;let l=s.previousNode();return l&&Le(l,!0),l}}}function ct(t){return t[0].parentElement}function jt(t){let e=fe.getTreeNode(le);for(;e&&e.scopeRef!==t;){if(e.contain)return!1;e=e.parent}return!0}function Jp(t){if(!t.form)return Array.from(ee(t).querySelectorAll(`input[type="radio"][name="${CSS.escape(t.name)}"]`)).filter(r=>!r.form);const e=t.form.elements.namedItem(t.name);let n=he(t);return e instanceof n.RadioNodeList?Array.from(e).filter(r=>r instanceof n.HTMLInputElement):e instanceof n.HTMLInputElement?[e]:[]}function Qp(t){if(t.checked)return!0;const e=Jp(t);return e.length>0&&!e.some(n=>n.checked)}function eh(t,e){let n=a.useRef(void 0),r=a.useRef(void 0);J(()=>{let o=t.current;if(!e){r.current&&(cancelAnimationFrame(r.current),r.current=void 0);return}const i=ee(o?o[0]:void 0);let s=c=>{if(c.key!=="Tab"||c.altKey||c.ctrlKey||c.metaKey||!jt(t)||c.isComposing)return;let d=re(i),f=t.current;if(!f||!Ae(d,f))return;let p=ct(f),m=Ve(p,{tabbable:!0},f);if(!d)return;m.currentNode=d;let h=c.shiftKey?m.previousNode():m.nextNode();h||(m.currentNode=c.shiftKey?f[f.length-1].nextElementSibling:f[0].previousElementSibling,h=c.shiftKey?m.previousNode():m.nextNode()),c.preventDefault(),h&&(Le(h,!0),h instanceof he(h).HTMLInputElement&&h.select())},l=c=>{(!le||Un(le,t))&&Ae(V(c),t.current)?(le=t,n.current=V(c)):jt(t)&&!qe(V(c),t)?n.current?Le(n.current):le&&le.current&&Gr(le.current):jt(t)&&(n.current=V(c))},u=c=>{r.current&&cancelAnimationFrame(r.current),r.current=requestAnimationFrame(()=>{let d=Je(),f=(d==="virtual"||d===null)&&Kn()&&cs(),p=re(i);if(!f&&p&&jt(t)&&!qe(p,t)){le=t;let m=V(c);m&&m.isConnected?(n.current=m,Le(n.current)):le.current&&Gr(le.current)}})};return i.addEventListener("keydown",s,!1),i.addEventListener("focusin",l,!1),o?.forEach(c=>c.addEventListener("focusin",l,!1)),o?.forEach(c=>c.addEventListener("focusout",u,!1)),()=>{i.removeEventListener("keydown",s,!1),i.removeEventListener("focusin",l,!1),o?.forEach(c=>c.removeEventListener("focusin",l,!1)),o?.forEach(c=>c.removeEventListener("focusout",u,!1))}},[t,e]),J(()=>()=>{r.current&&cancelAnimationFrame(r.current)},[r])}function El(t){return qe(t)}function Ae(t,e){return!t||!e?!1:e.some(n=>Y(n,t))}function qe(t,e=null){if(t instanceof Element&&t.closest("[data-react-aria-top-layer]"))return!0;for(let{scopeRef:n}of fe.traverse(fe.getTreeNode(e)))if(n&&Ae(t,n.current))return!0;return!1}function th(t){return qe(t,le)}function Un(t,e){let n=fe.getTreeNode(e)?.parent;for(;n;){if(n.scopeRef===t)return!0;n=n.parent}return!1}function Le(t,e=!1){if(t!=null&&!e)try{et(t)}catch{}else if(t!=null)try{t.focus()}catch{}}function wl(t,e=!0){let n=t[0].previousElementSibling,r=ct(t),o=Ve(r,{tabbable:e},t);o.currentNode=n;let i=o.nextNode();return e&&!i&&(r=ct(t),o=Ve(r,{tabbable:!1},t),o.currentNode=n,i=o.nextNode()),i}function Gr(t,e=!0){Le(wl(t,e))}function nh(t,e){const n=$.useRef(e);a.useEffect(()=>{if(n.current){le=t;const r=ee(t.current?t.current[0]:void 0);!Ae(re(r),le.current)&&t.current&&Gr(t.current)}n.current=!1},[t])}function rh(t,e,n){J(()=>{if(e||n)return;let r=t.current;const o=ee(r?r[0]:void 0);let i=s=>{let l=V(s);Ae(l,t.current)?le=t:El(l)||(le=null)};return o.addEventListener("focusin",i,!1),r?.forEach(s=>s.addEventListener("focusin",i,!1)),()=>{o.removeEventListener("focusin",i,!1),r?.forEach(s=>s.removeEventListener("focusin",i,!1))}},[t,e,n])}function $i(t){let e=fe.getTreeNode(le);for(;e&&e.scopeRef!==t;){if(e.nodeToRestore)return!1;e=e.parent}return e?.scopeRef===t}function oh(t,e,n){const r=a.useRef(typeof document<"u"?re(ee(t.current?t.current[0]:void 0)):null);J(()=>{let o=t.current;const i=ee(o?o[0]:void 0);if(!e||n)return;let s=()=>{(!le||Un(le,t))&&Ae(re(i),t.current)&&(le=t)};return i.addEventListener("focusin",s,!1),o?.forEach(l=>l.addEventListener("focusin",s,!1)),()=>{i.removeEventListener("focusin",s,!1),o?.forEach(l=>l.removeEventListener("focusin",s,!1))}},[t,n]),J(()=>{const o=ee(t.current?t.current[0]:void 0);if(!e)return;let i=s=>{if(s.key!=="Tab"||s.altKey||s.ctrlKey||s.metaKey||!jt(t)||s.isComposing)return;let l=o.activeElement;if(!qe(l,t)||!$i(t))return;let u=fe.getTreeNode(t);if(!u)return;let c=u.nodeToRestore,d=Ve(o.body,{tabbable:!0});d.currentNode=l;let f=s.shiftKey?d.previousNode():d.nextNode();if((!c||!c.isConnected||c===o.body)&&(c=void 0,u.nodeToRestore=void 0),(!f||!qe(f,t))&&c){d.currentNode=c;do f=s.shiftKey?d.previousNode():d.nextNode();while(qe(f,t));s.preventDefault(),s.stopPropagation(),f?Le(f,!0):El(c)?Le(c,!0):l.blur()}};return n||o.addEventListener("keydown",i,!0),()=>{n||o.removeEventListener("keydown",i,!0)}},[t,e,n]),J(()=>{const o=ee(t.current?t.current[0]:void 0);if(!e)return;let i=fe.getTreeNode(t);if(i)return i.nodeToRestore=r.current??void 0,()=>{let s=fe.getTreeNode(t);if(!s)return;let l=s.nodeToRestore,u=re(o);if(e&&l&&(u&&qe(u,t)||u===o.body&&$i(t))){let c=fe.clone();requestAnimationFrame(()=>{if(o.activeElement===o.body){let d=c.getTreeNode(t);for(;d;){if(d.nodeToRestore&&d.nodeToRestore.isConnected){xi(d.nodeToRestore);return}d=d.parent}for(d=c.getTreeNode(t);d;){if(d.scopeRef&&d.scopeRef.current&&fe.getTreeNode(d.scopeRef)){let f=wl(d.scopeRef.current,!0);if(f){xi(f);return}}d=d.parent}}})}}},[t,e])}function xi(t){t.dispatchEvent(new CustomEvent(Wr,{bubbles:!0,cancelable:!0}))&&Le(t)}function Ve(t,e,n){let r=e?.tabbable?lo:ws,o=t?.nodeType===Node.ELEMENT_NODE?t:null,i=ee(o),s=gs(i,t||i,NodeFilter.SHOW_ELEMENT,{acceptNode(l){return Y(e?.from,l)||e?.tabbable&&l.tagName==="INPUT"&&l.getAttribute("type")==="radio"&&(!Qp(l)||s.currentNode.tagName==="INPUT"&&s.currentNode.type==="radio"&&s.currentNode.name===l.name)?NodeFilter.FILTER_REJECT:r(l)&&(!n||Ae(l,n))&&(!e?.accept||e.accept(l))?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});return e?.from&&(s.currentNode=e.from),s}class wo{constructor(){this.fastMap=new Map,this.root=new qr({scopeRef:null}),this.fastMap.set(null,this.root)}get size(){return this.fastMap.size}getTreeNode(e){return this.fastMap.get(e)}addTreeNode(e,n,r){let o=this.fastMap.get(n??null);if(!o)return;let i=new qr({scopeRef:e});o.addChild(i),i.parent=o,this.fastMap.set(e,i),r&&(i.nodeToRestore=r)}addNode(e){this.fastMap.set(e.scopeRef,e)}removeTreeNode(e){if(e===null)return;let n=this.fastMap.get(e);if(!n)return;let r=n.parent;for(let i of this.traverse())i!==n&&n.nodeToRestore&&i.nodeToRestore&&n.scopeRef&&n.scopeRef.current&&Ae(i.nodeToRestore,n.scopeRef.current)&&(i.nodeToRestore=n.nodeToRestore);let o=n.children;r&&(r.removeChild(n),o.size>0&&o.forEach(i=>r&&r.addChild(i))),this.fastMap.delete(n.scopeRef)}*traverse(e=this.root){if(e.scopeRef!=null&&(yield e),e.children.size>0)for(let n of e.children)yield*this.traverse(n)}clone(){let e=new wo;for(let n of this.traverse())e.addTreeNode(n.scopeRef,n.parent?.scopeRef??null,n.nodeToRestore);return e}}class qr{constructor(e){this.children=new Set,this.contain=!1,this.scopeRef=e.scopeRef}addChild(e){this.children.add(e),e.parent=this}removeChild(e){this.children.delete(e),e.parent=void 0}}let fe=new wo;function ih(t){let{ref:e,onInteractOutside:n,isDisabled:r,onInteractOutsideStart:o}=t,i=a.useRef({isPointerDown:!1,ignoreEmulatedMouseEvents:!1}),s=Ce(u=>{n&&Ei(u,e)&&(o&&o(u),i.current.isPointerDown=!0)}),l=Ce(u=>{n&&n(u)});a.useEffect(()=>{let u=i.current;if(r)return;const c=e.current,d=ee(c);if(typeof PointerEvent<"u"){let f=p=>{u.isPointerDown&&Ei(p,e)&&l(p),u.isPointerDown=!1};return d.addEventListener("pointerdown",s,!0),d.addEventListener("click",f,!0),()=>{d.removeEventListener("pointerdown",s,!0),d.removeEventListener("click",f,!0)}}},[e,r])}function Ei(t,e){if(t.button>0)return!1;let n=V(t);if(n){const r=n.ownerDocument;if(!r||!Y(r.documentElement,n)||n.closest("[data-react-aria-top-layer]"))return!1}return e.current?!t.composedPath().includes(e.current):!1}const De=[];function sh(t,e){let{onClose:n,shouldCloseOnBlur:r,isOpen:o,isDismissable:i=!1,isKeyboardDismissDisabled:s=!1,shouldCloseOnInteractOutside:l}=t,u=a.useRef(void 0);a.useEffect(()=>{if(o&&!De.includes(e))return De.push(e),()=>{let h=De.indexOf(e);h>=0&&De.splice(h,1)}},[o,e]);let c=()=>{De[De.length-1]===e&&n&&n()},d=h=>{const g=De[De.length-1];u.current=g,(!l||l(V(h)))&&g===e&&h.stopPropagation()},f=h=>{(!l||l(V(h)))&&(De[De.length-1]===e&&h.stopPropagation(),u.current===e&&c()),u.current=void 0},{keyboardProps:p}=tt({shortcuts:{Escape:()=>{if(!s){c();return}return!1}}});ih({ref:e,onInteractOutside:i&&o?f:void 0,onInteractOutsideStart:d});let{focusWithinProps:m}=sn({isDisabled:!r,onBlurWithin:h=>{!h.relatedTarget||th(h.relatedTarget)||(!l||l(h.relatedTarget))&&n?.()}});return{overlayProps:{...p,...m},underlayProps:{}}}const Zt=typeof document<"u"&&window.visualViewport;let Cn=0,Cr;function lh(t={}){let{isDisabled:e}=t;J(()=>{if(!e)return Cn++,Cn===1&&(gt()&&pt()?Cr=uh():Cr=ah()),()=>{Cn--,Cn===0&&Cr()}},[e])}function ah(){let t=window.innerWidth-document.documentElement.clientWidth;return ht(t>0&&("scrollbarGutter"in document.documentElement.style?kn(document.documentElement,"scrollbar-gutter","stable"):kn(document.documentElement,"padding-right",`${t}px`)),kn(document.documentElement,"overflow","hidden"))}function uh(){let t=kn(document.documentElement,"overflow","hidden"),e,n=!1,r=d=>{let f=V(d);e=Mt(f)?f:oo(f,!0),n=!1;let p=f.ownerDocument.defaultView.getSelection();p&&!p.isCollapsed&&p.containsNode(f,!0)&&(n=!0),d.composedPath().some(m=>m instanceof HTMLInputElement&&m.type==="range")&&(n=!0),"selectionStart"in f&&"selectionEnd"in f&&f.selectionStart{if(!(d.touches.length===2||n)){if(!e||e===document.documentElement||e===document.body){d.preventDefault();return}e.scrollHeight===e.clientHeight&&e.scrollWidth===e.clientWidth&&d.preventDefault()}},l=d=>{let f=V(d),p=d.relatedTarget;p&&br(p)?(p.focus({preventScroll:!0}),wi(p,br(f))):p||f.parentElement?.closest("[tabindex]")?.focus({preventScroll:!0})},u=HTMLElement.prototype.focus;Reflect.defineProperty(HTMLElement.prototype,"focus",{configurable:!0,writable:!0,value:function(d){let f=re(),p=f!=null&&br(f);u.call(this,{...d,preventScroll:!0}),(!d||!d.preventScroll)&&wi(this,p)}});let c=ht(Pt(document,"touchstart",r,{passive:!1,capture:!0}),Pt(document,"touchmove",s,{passive:!1,capture:!0}),Pt(document,"blur",l,!0));return()=>{t(),c(),o.remove(),Reflect.defineProperty(HTMLElement.prototype,"focus",{configurable:!0,writable:!0,value:u})}}function wi(t,e){e||!Zt?Ci(t):Zt.addEventListener("resize",()=>Ci(t),{once:!0})}function Ci(t){let e=document.scrollingElement||document.documentElement,n=t;for(;n&&n!==e;){let r=oo(n);if(r!==document.documentElement&&r!==document.body&&r!==n){let o=r.getBoundingClientRect(),i=n.getBoundingClientRect();if(i.topo.top+n.clientHeight){let s=o.bottom;Zt&&(s=Math.min(s,Zt.offsetTop+Zt.height));let l=i.top-o.top-((s-o.top)/2-i.height/2);r.scrollTo({top:Math.max(0,Math.min(r.scrollHeight-r.clientHeight,r.scrollTop+l)),behavior:"smooth"})}}n=r.parentElement}}function ch(t,e){let{triggerRef:n,popoverRef:r,groupRef:o,isNonModal:i,isKeyboardDismissDisabled:s,shouldCloseOnInteractOutside:l,...u}=t,c=u.trigger==="SubmenuTrigger",{overlayProps:d,underlayProps:f}=sh({isOpen:e.isOpen,onClose:e.close,shouldCloseOnBlur:!0,isDismissable:!i||c,isKeyboardDismissDisabled:s,shouldCloseOnInteractOutside:l},o??r),{overlayProps:p,arrowProps:m,placement:h,triggerAnchorPoint:g}=Gp({...u,targetRef:n,overlayRef:r,isOpen:e.isOpen,onClose:i&&!c?e.close:null,getTargetRect:u.getTargetRect??(e.point?()=>new DOMRect(e.point.x,e.point.y,0,0):void 0)});lh({isDisabled:i||!e.isOpen}),a.useEffect(()=>{if(e.isOpen&&r.current)return i?Op(o?.current??r.current):Np([o?.current??r.current],{shouldUseInert:!0})},[i,e.isOpen,r,o]);let{focusWithinProps:b}=sn(t);return{popoverProps:Q(d,p,b),arrowProps:m,underlayProps:f,placement:h,triggerAnchorPoint:g}}var Cl={};Cl={dismiss:"تجاهل"};var Sl={};Sl={dismiss:"Отхвърляне"};var Pl={};Pl={dismiss:"Odstranit"};var Tl={};Tl={dismiss:"Luk"};var kl={};kl={dismiss:"Schließen"};var Ml={};Ml={dismiss:"Απόρριψη"};var Al={};Al={dismiss:"Dismiss"};var Rl={};Rl={dismiss:"Descartar"};var Ll={};Ll={dismiss:"Lõpeta"};var Il={};Il={dismiss:"Hylkää"};var Fl={};Fl={dismiss:"Rejeter"};var Kl={};Kl={dismiss:"התעלם"};var Dl={};Dl={dismiss:"Odbaci"};var Nl={};Nl={dismiss:"Elutasítás"};var Ol={};Ol={dismiss:"Ignora"};var Vl={};Vl={dismiss:"閉じる"};var Bl={};Bl={dismiss:"무시"};var _l={};_l={dismiss:"Atmesti"};var Hl={};Hl={dismiss:"Nerādīt"};var jl={};jl={dismiss:"Lukk"};var zl={};zl={dismiss:"Negeren"};var Ul={};Ul={dismiss:"Zignoruj"};var Zl={};Zl={dismiss:"Descartar"};var Wl={};Wl={dismiss:"Dispensar"};var Gl={};Gl={dismiss:"Revocare"};var ql={};ql={dismiss:"Пропустить"};var Yl={};Yl={dismiss:"Zrušiť"};var Xl={};Xl={dismiss:"Opusti"};var Jl={};Jl={dismiss:"Odbaci"};var Ql={};Ql={dismiss:"Avvisa"};var ea={};ea={dismiss:"Kapat"};var ta={};ta={dismiss:"Скасувати"};var na={};na={dismiss:"取消"};var ra={};ra={dismiss:"關閉"};var oa={};oa={"ar-AE":Cl,"bg-BG":Sl,"cs-CZ":Pl,"da-DK":Tl,"de-DE":kl,"el-GR":Ml,"en-US":Al,"es-ES":Rl,"et-EE":Ll,"fi-FI":Il,"fr-FR":Fl,"he-IL":Kl,"hr-HR":Dl,"hu-HU":Nl,"it-IT":Ol,"ja-JP":Vl,"ko-KR":Bl,"lt-LT":_l,"lv-LV":Hl,"nb-NO":jl,"nl-NL":zl,"pl-PL":Ul,"pt-BR":Zl,"pt-PT":Wl,"ro-RO":Gl,"ru-RU":ql,"sk-SK":Yl,"sl-SI":Xl,"sr-SP":Jl,"sv-SE":Ql,"tr-TR":ea,"uk-UA":ta,"zh-CN":na,"zh-TW":ra};const dh=Symbol.for("react-aria.i18n.locale"),fh=Symbol.for("react-aria.i18n.strings");let Sn;class ir{constructor(e,n="en-US"){this.strings=Object.fromEntries(Object.entries(e).filter(([,r])=>r)),this.defaultLocale=n}getStringForLocale(e,n){let o=this.getStringsForLocale(n)[e];if(!o)throw new Error(`Could not find intl message ${e} in ${n} locale`);return o}getStringsForLocale(e){let n=this.strings[e];return n||(n=ph(e,this.strings,this.defaultLocale),this.strings[e]=n),n}static getGlobalDictionaryForPackage(e){if(typeof window>"u")return null;let n=window[dh];if(Sn===void 0){let o=window[fh];if(!o)return null;Sn={};for(let i in o)Sn[i]=new ir({[n]:o[i]},n)}let r=Sn?.[e];if(!r)throw new Error(`Strings for package "${e}" were not included by LocalizedStringProvider. Please add it to the list passed to createLocalizedStringDictionary.`);return r}}function ph(t,e,n="en-US"){if(e[t])return e[t];let r=hh(t),o=mh(t);if(o&&e[`${r}-${o}`])return e[`${r}-${o}`];if(e[r])return e[r];for(let i in e)if(i.startsWith(r+"-"))return e[i];return e[n]}function hh(t){return Intl.Locale?new Intl.Locale(t).language:t.split("-")[0]}function mh(t){if(Intl.Locale)return new Intl.Locale(t).script}const Si=new Map,Pi=new Map;class bh{constructor(e,n){this.locale=e,this.strings=n}format(e,n){let r=this.strings.getStringForLocale(e,this.locale);return typeof r=="function"?r(n,this):r}plural(e,n,r="cardinal"){let o=n["="+e];if(o)return typeof o=="function"?o():o;let i=this.locale+":"+r,s=Si.get(i);s||(s=new Intl.PluralRules(this.locale,{type:r}),Si.set(i,s));let l=s.select(e);return o=n[l]||n.other,typeof o=="function"?o():o}number(e){let n=Pi.get(this.locale);return n||(n=new Intl.NumberFormat(this.locale),Pi.set(this.locale,n)),n.format(e)}select(e,n){let r=e[n]||e.other;return typeof r=="function"?r():r}}const Ti=new WeakMap;function gh(t){let e=Ti.get(t);return e||(e=new ir(t),Ti.set(t,e)),e}function vh(t,e){return e&&ir.getGlobalDictionaryForPackage(e)||gh(t)}function sr(t,e){let{locale:n}=it(),r=vh(t,e);return a.useMemo(()=>new bh(n,r),[n,r])}const ki={border:0,clip:"rect(0 0 0 0)",clipPath:"inset(50%)",height:"1px",margin:"-1px",overflow:"hidden",padding:0,position:"absolute",width:"1px",whiteSpace:"nowrap"};function yh(t={}){let{style:e,isFocusable:n}=t,[r,o]=a.useState(!1),{focusWithinProps:i}=sn({isDisabled:!n,onFocusWithinChange:l=>o(l)}),s=a.useMemo(()=>r?e:e?{...ki,...e}:ki,[r]);return{visuallyHiddenProps:{...i,style:s}}}function $h(t){let{children:e,elementType:n="div",isFocusable:r,style:o,...i}=t,{visuallyHiddenProps:s}=yh(t);return $.createElement(n,Q(i,s),e)}function xh(t){return t&&t.__esModule?t.default:t}function Mi(t){let{onDismiss:e,...n}=t,r=sr(xh(oa),"@react-aria/overlays"),o=$s(n,r.format("dismiss")),i=()=>{e&&e()};return $.createElement($h,null,$.createElement("button",{...o,tabIndex:-1,onClick:i,style:{width:1,height:1}}))}const ia=$.forwardRef(({children:t,...e},n)=>{let r=a.useRef(!1),o=a.useContext(Yt),i=Q(o||{},{...e,register(){r.current=!0,o&&o.register()}});return i.ref=rt(n||o?.ref),ro(o,i.ref),a.useEffect(()=>{r.current||(r.current=!0)},[]),$.createElement(Yt.Provider,{value:i},t)});function Eh({children:t}){let e=a.useMemo(()=>({register:()=>{}}),[]);return $.createElement(Yt.Provider,{value:e},t)}const wh=a.createContext({});function sa(){return a.useContext(wh)??{}}const la=$.createContext(null);function Ai(t){let e=nt(),{portalContainer:n=e?null:document.body,isExiting:r}=t,[o,i]=a.useState(!1),s=a.useMemo(()=>({contain:o,setContain:i}),[o,i]),{getContainer:l}=sa();if(!t.portalContainer&&l&&(n=l()),!n)return null;let u=t.children;return t.disableFocusManagement||(u=$.createElement(xl,{restoreFocus:!0,contain:(t.shouldContainFocus||o)&&!r},u)),u=$.createElement(la.Provider,{value:s},$.createElement(Eh,null,$.createElement(qt.Provider,{value:null},u))),Sd.createPortal(u,n)}function Ch(){let e=a.useContext(la)?.setContain;J(()=>{e?.(!0)},[e])}function aa(t){let[e,n]=rr(t.isOpen,t.defaultOpen||!1,t.onOpenChange),[r,o]=a.useState(null);const i=a.useCallback(()=>{n(!0)},[n]),s=a.useCallback(()=>{n(!1)},[n]),l=a.useCallback(()=>{n(!e)},[n,e]);return{isOpen:e,setOpen:n,open:i,close:s,toggle:l,point:r,setPoint:o}}const lr=a.createContext(null),Ri=a.createContext(null),Sh=a.forwardRef(function(e,n){[e,n]=me(e,n,lr);let r=a.useContext(cn),o=aa(e),i=e.isOpen!=null||e.defaultOpen!=null||!r?o:r,s=Td(n,i.isOpen),l=e.isExiting||!e.shouldSkipAnimation&&s||!1,u=bo(),{direction:c}=it();if(u){let d=e.children;return typeof d=="function"&&(d=d({trigger:e.trigger||null,placement:"bottom",isEntering:!1,isExiting:!1,defaultChildren:null})),$.createElement($.Fragment,null,d)}return i&&!i.isOpen&&!l?null:$.createElement(Ph,{...e,triggerRef:e.triggerRef,state:i,popoverRef:n,isExiting:l,dir:c})});function Ph({state:t,isExiting:e,UNSTABLE_portalContainer:n,clearContexts:r,...o}){let i=a.useRef(null),s=a.useRef(null),l=a.useContext(Ri),u=l&&o.trigger==="SubmenuTrigger",{popoverProps:c,underlayProps:d,arrowProps:f,placement:p,triggerAnchorPoint:m}=ch({...o,offset:o.offset??8,arrowRef:i,groupRef:u?l:s},t),h=o.popoverRef,g=Pd(h,!!p),b=o.isEntering||!o.shouldSkipAnimation&&g||!1,y=ve({...o,defaultClassName:"react-aria-Popover",values:{trigger:o.trigger||null,placement:p,isEntering:b,isExiting:e}}),v=!o.isNonModal||o.trigger==="SubmenuTrigger"||o.trigger==="PreviewTrigger",[w,x]=a.useState(o.trigger==="PreviewTrigger");J(()=>{h.current&&x(v&&!h.current.querySelector("[role=dialog]"))},[h,v]),a.useEffect(()=>{w&&o.trigger!=="PreviewTrigger"&&(o.trigger!=="SubmenuTrigger"||Je()!=="pointer")&&h.current&&!Xe(h.current)&&et(h.current)},[w,h,o.trigger]);let F=a.useMemo(()=>{let O=y.children;if(r)for(let z of r)O=$.createElement(z.Provider,{value:null},O);return O},[y.children,r]),[K,P]=a.useState(null),S=a.useCallback(()=>{o.triggerRef.current&&P(o.triggerRef.current.getBoundingClientRect().width+"px")},[o.triggerRef]);J(S,[S]),Dn({ref:y.style?.["--trigger-width"]?void 0:o.triggerRef,onResize:S});let I={...c.style,"--trigger-anchor-point":m?`${m.x}px ${m.y}px`:void 0,...y.style,"--trigger-width":y.style?.["--trigger-width"]||K},M=$.createElement(ue.div,{...Q(de(o,{global:!0}),c),...y,id:w?o.id:void 0,role:w?"dialog":void 0,tabIndex:w?-1:void 0,"aria-label":o["aria-label"],"aria-labelledby":o["aria-labelledby"],ref:h,slot:o.slot||void 0,style:I,dir:o.dir,"data-trigger":o.trigger,"data-placement":p,"data-entering":b||void 0,"data-exiting":e||void 0},!o.isNonModal&&$.createElement(Mi,{onDismiss:t.close}),$.createElement(vl.Provider,{value:{...f,placement:p,ref:i}},F),$.createElement(Mi,{onDismiss:t.close}));return u?$.createElement(Ai,{...o,shouldContainFocus:w&&o.trigger!=="PreviewTrigger",isExiting:e,portalContainer:n??l?.current??void 0},M):$.createElement(Ai,{...o,shouldContainFocus:w&&o.trigger!=="PreviewTrigger",isExiting:e,portalContainer:n},!o.isNonModal&&t.isOpen&&$.createElement("div",{"data-testid":"underlay",...d,style:{position:"fixed",inset:0}}),$.createElement("div",{ref:s,style:{display:"contents"}},$.createElement(Ri.Provider,{value:s},M)))}class It{constructor(e){this.value=null,this.level=0,this.hasChildNodes=!1,this.rendered=null,this.textValue="",this["aria-label"]=void 0,this.index=0,this.parentKey=null,this.prevKey=null,this.nextKey=null,this.firstChildKey=null,this.lastChildKey=null,this.props={},this.colSpan=null,this.colIndex=null,this.type=this.constructor.type,this.key=e}get childNodes(){throw new Error("childNodes is not supported")}clone(){let e=new this.constructor(this.key);return e.value=this.value,e.level=this.level,e.hasChildNodes=this.hasChildNodes,e.rendered=this.rendered,e.textValue=this.textValue,e["aria-label"]=this["aria-label"],e.index=this.index,e.parentKey=this.parentKey,e.prevKey=this.prevKey,e.nextKey=this.nextKey,e.firstChildKey=this.firstChildKey,e.lastChildKey=this.lastChildKey,e.props=this.props,e.render=this.render,e.colSpan=this.colSpan,e.colIndex=this.colIndex,e}filter(e,n,r){let o=this.clone();return n.addDescendants(o,e),o}}class ua extends It{filter(e,n,r){let[o,i]=ca(e,n,this.firstChildKey,r),s=this.clone();return s.firstChildKey=o,s.lastChildKey=i,s}}class og extends It{static{this.type="header"}}class Th extends It{static{this.type="loader"}}class kh extends ua{static{this.type="item"}filter(e,n,r){if(r(this.textValue,this)){let o=this.clone();return n.addDescendants(o,e),o}return null}}class Mh extends ua{static{this.type="section"}filter(e,n,r){let o=super.filter(e,n,r);if(o&&o.lastChildKey!==null){let i=e.getItem(o.lastChildKey);if(i&&i.type!=="header")return o}return null}}class Ah{get size(){return this.itemCount}getKeys(){return this.keyMap.keys()}*[Symbol.iterator](){let e=this.firstKey!=null?this.keyMap.get(this.firstKey):void 0;for(;e;)yield e,e=e.nextKey!=null?this.keyMap.get(e.nextKey):void 0}getChildren(e){let n=this.keyMap;return{*[Symbol.iterator](){let r=n.get(e),o=r?.firstChildKey!=null?n.get(r.firstChildKey):null;for(;o;)yield o,o=o.nextKey!=null?n.get(o.nextKey):void 0}}}getKeyBefore(e){let n=this.keyMap.get(e);if(!n)return null;if(n.prevKey!=null){for(n=this.keyMap.get(n.prevKey);n&&n.type!=="item"&&n.lastChildKey!=null;)n=this.keyMap.get(n.lastChildKey);return n?.key??null}return n.parentKey}getKeyAfter(e){let n=this.keyMap.get(e);if(!n)return null;if(n.type!=="item"&&n.firstChildKey!=null)return n.firstChildKey;for(;n;){if(n.nextKey!=null)return n.nextKey;if(n.parentKey!=null)n=this.keyMap.get(n.parentKey);else return null}return null}getFirstKey(){return this.firstKey}getLastKey(){let e=this.lastKey!=null?this.keyMap.get(this.lastKey):null;for(;e?.lastChildKey!=null;)e=this.keyMap.get(e.lastChildKey);return e?.key??null}getItem(e){return this.keyMap.get(e)??null}at(){throw new Error("Not implemented")}clone(){let e=this.constructor,n=new e;return n.keyMap=new Map(this.keyMap),n.firstKey=this.firstKey,n.lastKey=this.lastKey,n.itemCount=this.itemCount,n}addNode(e){if(this.frozen)throw new Error("Cannot add a node to a frozen collection");e.type==="item"&&this.keyMap.get(e.key)==null&&this.itemCount++,this.keyMap.set(e.key,e)}addDescendants(e,n){this.addNode(e);let r=n.getChildren(e.key);for(let o of r)this.addDescendants(o,n)}removeNode(e){if(this.frozen)throw new Error("Cannot remove a node to a frozen collection");let n=this.keyMap.get(e);n!=null&&n.type==="item"&&this.itemCount--,this.keyMap.delete(e)}commit(e,n,r=!1){if(this.frozen)throw new Error("Cannot commit a frozen collection");this.firstKey=e,this.lastKey=n,this.frozen=!r}filter(e){let n=new this.constructor,[r,o]=ca(this,n,this.firstKey,e);return n?.commit(r,o),n}constructor(){this.keyMap=new Map,this.firstKey=null,this.lastKey=null,this.frozen=!1,this.itemCount=0}}function ca(t,e,n,r){if(n==null)return[null,null];let o=null,i=null,s=t.getItem(n);for(;s!=null;){let l=s.filter(t,e,r);l!=null&&(l.nextKey=null,i&&(l.prevKey=i.key,i.nextKey=l.key),o==null&&(o=l),e.addNode(l),i=l),s=s.nextKey!=null?t.getItem(s.nextKey):null}if(i&&i.type==="separator"){let l=i.prevKey;e.removeNode(i.key),l!=null?(i=e.getItem(l),i.nextKey=null):i=null}return[o?.key??null,i?.key??null]}class da{constructor(e){this._firstChild=null,this._lastChild=null,this._previousSibling=null,this._nextSibling=null,this._parentNode=null,this._minInvalidChildIndex=null,this.ownerDocument=e}*[Symbol.iterator](){let e=this.firstChild;for(;e;)yield e,e=e.nextSibling}get firstChild(){return this._firstChild}set firstChild(e){this._firstChild=e,this.ownerDocument.markDirty(this)}get lastChild(){return this._lastChild}set lastChild(e){this._lastChild=e,this.ownerDocument.markDirty(this)}get previousSibling(){return this._previousSibling}set previousSibling(e){this._previousSibling=e,this.ownerDocument.markDirty(this)}get nextSibling(){return this._nextSibling}set nextSibling(e){this._nextSibling=e,this.ownerDocument.markDirty(this)}get parentNode(){return this._parentNode}set parentNode(e){this._parentNode=e,this.ownerDocument.markDirty(this)}get isConnected(){return this.parentNode?.isConnected||!1}invalidateChildIndices(e){(this._minInvalidChildIndex==null||!this._minInvalidChildIndex.isConnected||e.indexthis.subscriptions.delete(e)}resetAfterSSR(){this.isSSR&&(this.isSSR=!1,this.firstChild=null,this.lastChild=null,this.nodeId=0)}}function fa(t){let{children:e,items:n,idScope:r,addIdAndValue:o,dependencies:i=[]}=t,s=a.useMemo(()=>{},[e]),l=a.useMemo(()=>new WeakMap,[...i,s]);return a.useMemo(()=>{if(n&&typeof e=="function"){let u=[];for(let c of n){let d=Lh(c)?c:null,f=d?l.get(d):null;if(!f){f=e(c);let p=f.props.id??c?.key??c?.id;r!=null&&f.props.id==null&&p!=null&&(p=r+":"+p);let m=p??u.length;f=a.cloneElement(f,o?{key:m,id:p,value:c}:{key:m}),d&&l.set(d,f)}u.push(f)}return u}else if(typeof e!="function")return e},[e,n,l,r,o])}function Lh(t){switch(typeof t){case"object":return t!=null;case"function":case"symbol":return!0;default:return!1}}var Sr={exports:{}},Pr={};var Li;function Ih(){if(Li)return Pr;Li=1;var t=Rc();function e(f,p){return f===p&&(f!==0||1/f===1/p)||f!==f&&p!==p}var n=typeof Object.is=="function"?Object.is:e,r=t.useState,o=t.useEffect,i=t.useLayoutEffect,s=t.useDebugValue;function l(f,p){var m=p(),h=r({inst:{value:m,getSnapshot:p}}),g=h[0].inst,b=h[1];return i(function(){g.value=m,g.getSnapshot=p,u(g)&&b({inst:g})},[f,m,p]),o(function(){return u(g)&&b({inst:g}),f(function(){u(g)&&b({inst:g})})},[f]),s(m),m}function u(f){var p=f.getSnapshot;f=f.value;try{var m=p();return!n(f,m)}catch{return!0}}function c(f,p){return p()}var d=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?c:l;return Pr.useSyncExternalStore=t.useSyncExternalStore!==void 0?t.useSyncExternalStore:d,Pr}var Ii;function Fh(){return Ii||(Ii=1,Sr.exports=Ih()),Sr.exports}var Co=Fh();const pa=a.createContext(!1),Jt=a.createContext(null);function Kh(t){if(a.useContext(Jt))return t.content;let{collection:n,document:r}=Vh(t.createCollection);return $.createElement($.Fragment,null,$.createElement(B0,null,$.createElement(Jt.Provider,{value:r},t.content)),$.createElement(Dh,{render:t.children,collection:n}))}function Dh({collection:t,render:e}){return e(t)}function Nh(t,e,n){let r=nt(),o=a.useRef(r);o.current=r;let i=a.useCallback(()=>o.current?n():e(),[e,n]);return Co.useSyncExternalStore(t,i)}const Oh=typeof $.useSyncExternalStore=="function"?$.useSyncExternalStore:Nh;function Vh(t){let[e]=a.useState(()=>new Rh(t?.()||new Ah)),n=a.useCallback(s=>e.subscribe(s),[e]),r=a.useCallback(()=>{let s=e.getCollection();return e.isSSR&&e.resetAfterSSR(),s},[e]),o=a.useCallback(()=>(e.isSSR=!0,e.getCollection()),[e]);return{collection:Oh(n,r,o),document:e}}const Yr=a.createContext(null);function Bh(t){return class extends It{static{this.type=t}}}function ha(t,e,n,r,o,i){typeof t=="string"&&(t=Bh(t));let s=a.useCallback(u=>{u?.setProps(e,n,t,r,i)},[e,n,r,i,t]),l=a.useContext(Yr);if(l){let u=l.ownerDocument.nodesByProps.get(e);return u||(u=l.ownerDocument.createElement(t.type),u.setProps(e,n,t,r,i),l.appendChild(u),l.ownerDocument.updateCollection(),l.ownerDocument.nodesByProps.set(e,u)),o?$.createElement(Yr.Provider,{value:u},o):null}return $.createElement(t.type,{ref:s},o)}function ma(t,e){let n=({node:o})=>e(o.props,o.props.ref,o),r=a.forwardRef((o,i)=>{let s=a.useContext(qt);if(!a.useContext(pa)){if(e.length>=3)throw new Error(e.name+" cannot be rendered outside a collection.");return e(o,i)}return ha(t,o,i,"children"in o?o.children:null,null,u=>$.createElement(qt.Provider,{value:s},$.createElement(n,{node:u})))});return r.displayName=e.name,r}function ba(t,e,n=ga){let r=({node:i})=>e(i.props,i.props.ref,i),o=a.forwardRef((i,s)=>{let l=n(i);return ha(t,i,s,null,l,u=>$.createElement(r,{node:u}))??$.createElement($.Fragment,null)});return o.displayName=e.name,o}function ga(t){return fa({...t,addIdAndValue:!0})}const Fi=a.createContext(null);function _h(t){let e=a.useContext(Fi),n=(e?.dependencies||[]).concat(t.dependencies),r=t.idScope??e?.idScope,o=ga({...t,idScope:r,dependencies:n});return a.useContext(Jt)&&(o=$.createElement(Hh,null,o)),e=a.useMemo(()=>({dependencies:n,idScope:r}),[r,...n]),$.createElement(Fi.Provider,{value:e},o)}function Hh({children:t}){let e=a.useContext(Jt),n=a.useMemo(()=>$.createElement(Jt.Provider,{value:null},$.createElement(pa.Provider,{value:!0},t)),[t]);return nt()?$.createElement(Yr.Provider,{value:e},n):Qe.createPortal(n,e)}const jh=a.createContext(null),zh={CollectionRoot({collection:t,renderDropIndicator:e}){return Ki(t,null,e)},CollectionBranch({collection:t,parent:e,renderDropIndicator:n}){return Ki(t,e,n)}};function Ki(t,e,n){return fa({items:e?t.getChildren(e.key):t,dependencies:[n],children(r){if(r.type==="content")return $.createElement($.Fragment,null);let o=r.render(r);return!n||r.type!=="item"?o:$.createElement($.Fragment,null,n({type:"item",key:r.key,dropPosition:"before"}),o,Uh(t,r,n))}})}function Uh(t,e,n){let r=e.key,o=t.getKeyAfter(r),i=o!=null?t.getItem(o):null;for(;i!=null&&i.type!=="item";)o=t.getKeyAfter(i.key),i=o!=null?t.getItem(o):null;let s=e.nextKey!=null?t.getItem(e.nextKey):null;for(;s!=null&&s.type!=="item";)s=s.nextKey!=null?t.getItem(s.nextKey):null;let l=[];if(s==null){let u=e;for(;u?.type==="item"&&(!i||u.parentKey!==i.parentKey&&i.levelt!=null?new Set([t]):null,[t])}function va(t){let e=Gh(ee(t));e!==t&&(e&&Wh(e,t),t&&ya(t,e))}function Wh(t,e){t.dispatchEvent(new FocusEvent("blur",{relatedTarget:e})),t.dispatchEvent(new FocusEvent("focusout",{bubbles:!0,relatedTarget:e}))}function ya(t,e){t.dispatchEvent(new FocusEvent("focus",{relatedTarget:e})),t.dispatchEvent(new FocusEvent("focusin",{bubbles:!0,relatedTarget:e}))}function Gh(t){let e=re(t),n=e?.getAttribute("aria-activedescendant");return n&&t.getElementById(n)||e}const Di=a.createContext(null),$a=a.createContext(null),qh=a.createContext({}),Yh=a.createContext({}),xa=a.createContext(null);function Xh(t){let e=a.useRef({});return $.createElement(xa.Provider,{value:e},t.children)}const Jh=a.forwardRef(function(e,n){let{name:r,isVisible:o=!0,children:i,className:s,style:l,render:u,...c}=e,[d,f]=a.useState(o?"visible":"hidden"),p=a.useContext(xa);if(!p)throw new Error(" must be rendered inside a ");o&&d==="hidden"&&f("visible"),n=rt(n),J(()=>{let h=n.current,g=p.current,b=g[r],y=null;if(h&&o&&b){f("visible");let v=h.getAnimations(),w=b.style.map(([x,F])=>{let K=h.style[x];if(x==="translate"){let P=b.rect,S=h.getBoundingClientRect(),I=P.left-S?.left,M=P.top-S?.top;h.style.translate=`${I}px ${M}px`}else h.style[x]=F;return[x,K]});for(let x of h.getAnimations())v.includes(x)||x.cancel();y=requestAnimationFrame(()=>{y=null;for(let[x,F]of w)h.style[x]=F}),delete g[r]}else h&&o&&!b?(queueMicrotask(()=>Qe.flushSync(()=>f("entering"))),y=requestAnimationFrame(()=>{y=null,f("visible")})):h&&!o&&queueMicrotask(()=>{g[r]?(delete g[r],Qe.flushSync(()=>f("exiting")),Promise.all(h.getAnimations().map(v=>v.finished)).then(()=>f("hidden")).catch(()=>{})):f("hidden")});return()=>{if(y!=null&&cancelAnimationFrame(y),h&&h.isConnected&&!h.hasAttribute("data-exiting")){let v=window.getComputedStyle(h);if(v.transitionProperty!=="none"){let w=v.transitionProperty.split(/\s*,\s*/);g[r]={rect:h.getBoundingClientRect(),style:w.map(x=>[x,v[x]])}}}}},[n,p,r,o]);let m=ve({children:i,className:s,style:l,render:u,values:{isEntering:d==="entering",isExiting:d==="exiting"}});return d==="hidden"?null:$.createElement(ue.div,{...c,...m,ref:n,"data-entering":d==="entering"||void 0,"data-exiting":d==="exiting"||void 0})}),Ea=a.createContext({isSelected:!1}),ig=a.forwardRef(function(e,n){[e,n]=me(e,n,Ea);let{isSelected:r,...o}=e;return $.createElement(Jh,{...o,ref:n,className:e.className||"react-aria-SelectionIndicator",name:"SelectionIndicator",isVisible:r})}),Qh=a.createContext({});class sg extends It{static{this.type="separator"}filter(e,n){let r=n.getItem(this.prevKey);if(r&&r.type!=="separator"){let o=this.clone();return n.addDescendants(o,e),o}return null}}const an=a.createContext({}),wa=rn(function(e,n){[e,n]=me(e,n,an);let{elementType:r="span",...o}=e,i=ue[r];return $.createElement(i,{className:"react-aria-Text",...o,ref:n})});var Ca={};Ca={longPressMessage:"اضغط مطولاً أو اضغط على Alt + السهم لأسفل لفتح القائمة"};var Sa={};Sa={longPressMessage:"Натиснете продължително или натиснете Alt+ стрелка надолу, за да отворите менюто"};var Pa={};Pa={longPressMessage:"Dlouhým stiskem nebo stisknutím kláves Alt + šipka dolů otevřete nabídku"};var Ta={};Ta={longPressMessage:"Langt tryk eller tryk på Alt + pil ned for at åbne menuen"};var ka={};ka={longPressMessage:"Drücken Sie lange oder drücken Sie Alt + Nach-unten, um das Menü zu öffnen"};var Ma={};Ma={longPressMessage:"Πιέστε παρατεταμένα ή πατήστε Alt + κάτω βέλος για να ανοίξετε το μενού"};var Aa={};Aa={longPressMessage:"Long press or press Alt + ArrowDown to open menu"};var Ra={};Ra={longPressMessage:"Mantenga pulsado o pulse Alt + flecha abajo para abrir el menú"};var La={};La={longPressMessage:"Menüü avamiseks vajutage pikalt või vajutage klahve Alt + allanool"};var Ia={};Ia={longPressMessage:"Avaa valikko painamalla pohjassa tai näppäinyhdistelmällä Alt + Alanuoli"};var Fa={};Fa={longPressMessage:"Appuyez de manière prolongée ou appuyez sur Alt + Flèche vers le bas pour ouvrir le menu."};var Ka={};Ka={longPressMessage:"לחץ לחיצה ארוכה או הקש Alt + ArrowDown כדי לפתוח את התפריט"};var Da={};Da={longPressMessage:"Dugo pritisnite ili pritisnite Alt + strelicu prema dolje za otvaranje izbornika"};var Na={};Na={longPressMessage:"Nyomja meg hosszan, vagy nyomja meg az Alt + lefele nyíl gombot a menü megnyitásához"};var Oa={};Oa={longPressMessage:"Premi a lungo o premi Alt + Freccia giù per aprire il menu"};var Va={};Va={longPressMessage:"長押しまたは Alt+下矢印キーでメニューを開く"};var Ba={};Ba={longPressMessage:"길게 누르거나 Alt + 아래쪽 화살표를 눌러 메뉴 열기"};var _a={};_a={longPressMessage:"Norėdami atidaryti meniu, nuspaudę palaikykite arba paspauskite „Alt + ArrowDown“."};var Ha={};Ha={longPressMessage:"Lai atvērtu izvēlni, turiet nospiestu vai nospiediet taustiņu kombināciju Alt + lejupvērstā bultiņa"};var ja={};ja={longPressMessage:"Langt trykk eller trykk Alt + PilNed for å åpne menyen"};var za={};za={longPressMessage:"Druk lang op Alt + pijl-omlaag of druk op Alt om het menu te openen"};var Ua={};Ua={longPressMessage:"Naciśnij i przytrzymaj lub naciśnij klawisze Alt + Strzałka w dół, aby otworzyć menu"};var Za={};Za={longPressMessage:"Pressione e segure ou pressione Alt + Seta para baixo para abrir o menu"};var Wa={};Wa={longPressMessage:"Prima continuamente ou prima Alt + Seta Para Baixo para abrir o menu"};var Ga={};Ga={longPressMessage:"Apăsați lung sau apăsați pe Alt + săgeată în jos pentru a deschide meniul"};var qa={};qa={longPressMessage:"Нажмите и удерживайте или нажмите Alt + Стрелка вниз, чтобы открыть меню"};var Ya={};Ya={longPressMessage:"Ponuku otvoríte dlhým stlačením alebo stlačením klávesu Alt + klávesu so šípkou nadol"};var Xa={};Xa={longPressMessage:"Za odprtje menija pritisnite in držite gumb ali pritisnite Alt+puščica navzdol"};var Ja={};Ja={longPressMessage:"Dugo pritisnite ili pritisnite Alt + strelicu prema dole da otvorite meni"};var Qa={};Qa={longPressMessage:"Håll nedtryckt eller tryck på Alt + pil nedåt för att öppna menyn"};var eu={};eu={longPressMessage:"Menüyü açmak için uzun basın veya Alt + Aşağı Ok tuşuna basın"};var tu={};tu={longPressMessage:"Довго або звичайно натисніть комбінацію клавіш Alt і стрілка вниз, щоб відкрити меню"};var nu={};nu={longPressMessage:"长按或按 Alt + 向下方向键以打开菜单"};var ru={};ru={longPressMessage:"長按或按 Alt+向下鍵以開啟功能表"};var ou={};ou={"ar-AE":Ca,"bg-BG":Sa,"cs-CZ":Pa,"da-DK":Ta,"de-DE":ka,"el-GR":Ma,"en-US":Aa,"es-ES":Ra,"et-EE":La,"fi-FI":Ia,"fr-FR":Fa,"he-IL":Ka,"hr-HR":Da,"hu-HU":Na,"it-IT":Oa,"ja-JP":Va,"ko-KR":Ba,"lt-LT":_a,"lv-LV":Ha,"nb-NO":ja,"nl-NL":za,"pl-PL":Ua,"pt-BR":Za,"pt-PT":Wa,"ro-RO":Ga,"ru-RU":qa,"sk-SK":Ya,"sl-SI":Xa,"sr-SP":Ja,"sv-SE":Qa,"tr-TR":eu,"uk-UA":tu,"zh-CN":nu,"zh-TW":ru};const em=500;function So(t){let{isDisabled:e,pointerType:n,onLongPressStart:r,onLongPressEnd:o,onLongPress:i,threshold:s=em,accessibilityDescription:l}=t;const u=a.useRef(void 0);let{addGlobalListener:c,removeAllGlobalListeners:d}=Jn(),f=h=>n?h.pointerType===n:h.pointerType==="mouse"||h.pointerType==="touch",{pressProps:p}=on({isDisabled:e,onPressStart(h){if(h.continuePropagation(),f(h)){r&&r({...h,type:"longpressstart"}),u.current=setTimeout(()=>{h.target.dispatchEvent(new PointerEvent("pointercancel",{bubbles:!0})),c(h.target,"click",b=>b.preventDefault(),{once:!0}),ee(h.target).activeElement!==h.target&&ce(h.target),i&&i({...h,type:"longpress"}),u.current=void 0},s),h.pointerType==="touch"&&c(h.target,"contextmenu",b=>b.preventDefault(),{once:!0});let g=he(h.target);c(g,"pointerup",()=>{setTimeout(()=>{d()},100)},{once:!0})}},onPressEnd(h){u.current&&clearTimeout(u.current),o&&f(h)&&o({...h,type:"longpressend"})}}),m=yd(i&&!e?l:void 0);return{longPressProps:Q(p,m)}}function tm(t){let{onContextMenu:e}=t,n=a.useRef(!1),{longPressProps:r}=So({onLongPressStart(){n.current=!1},onLongPress(o){n.current?n.current=!1:e?.({target:o.target,x:o.x,y:o.y})}});return e?{contextMenuProps:Q(gt()?r:{},{onContextMenu(o){o.stopPropagation(),o.preventDefault(),n.current=!0;let i=o.currentTarget.getBoundingClientRect();e({target:o.currentTarget,x:o.clientX-i.x,y:o.clientY-i.y})},onKeyDown(o){if(Me()&&o.ctrlKey&&o.key==="Enter"){n.current=!1;let i=o.currentTarget;o.stopPropagation(),setTimeout(()=>{if(n.current)n.current=!1;else{let s=i.getBoundingClientRect();e({target:i,x:s.width/2,y:s.height/2})}},10)}}})}:{contextMenuProps:{}}}function iu(t,e,n){let{type:r}=t,{isOpen:o}=e;a.useEffect(()=>{n&&n.current&&$l.set(n.current,e.close)});let i;r==="menu"?i=!0:r==="listbox"&&(i="listbox");let s=xe();return{triggerProps:{"aria-haspopup":i,"aria-expanded":o,"aria-controls":o?s:void 0,onPress:e.toggle},overlayProps:{id:s}}}function nm(t){return t&&t.__esModule?t.default:t}function rm(t,e,n){let{type:r="menu",isDisabled:o,trigger:i="press"}=t,s=xe(),{triggerProps:l,overlayProps:u}=iu({type:r},e,n),c=(b,y,v="first")=>{if(!b||y.isDefaultPrevented())return!1;e.toggle(v)},{keyboardProps:d}=tt({isDisabled:o,shortcuts:{Enter:b=>c(i!=="longPress",b,"first")," ":b=>c(i!=="longPress",b,"first"),ArrowDown:b=>c(i!=="longPress",b,"first"),ArrowUp:b=>c(i!=="longPress",b,"last"),"Alt+Enter":b=>c(i==="longPress",b,"first"),"Alt+ ":b=>c(i==="longPress",b,"first"),"Alt+ArrowDown":b=>c(!0,b,"first"),"Alt+ArrowUp":b=>c(!0,b,"last")}}),f=sr(nm(ou),"@react-aria/menu"),{longPressProps:p}=So({isDisabled:o||i!=="longPress",accessibilityDescription:f.format("longPressMessage"),onLongPressStart(){e.close()},onLongPress(){e.open("first")}}),m={preventFocusOnPress:!0,onPressStart(b){b.pointerType!=="touch"&&b.pointerType!=="keyboard"&&!o&&(ce(b.target),e.open(b.pointerType==="virtual"?"first":null))},onPress(b){b.pointerType==="touch"&&!o&&(ce(b.target),e.toggle())}};delete l.onPress;let{contextMenuProps:h}=tm({onContextMenu(b){let y=b.target.getBoundingClientRect();e.setPoint({x:y.x+b.x,y:y.y+b.y}),e.open()}});a.useEffect(()=>{if(e.isOpen&&i==="contextMenu"){let b=y=>{(y.button===2||y.button===0&&y.ctrlKey===!0)&&V(y)===document.body&&e.close()};return document.addEventListener("mousedown",b),()=>document.removeEventListener("mousedown",b)}},[e,i]);let g;if(i==="press")g={...m,...d};else if(i==="longPress")g={...p,...d};else if(i==="contextMenu"){g=h;let{"aria-haspopup":b,"aria-expanded":y,"aria-controls":v,...w}=l;l=w}return{menuTriggerProps:{...l,...g,id:s},menuProps:{...u,"aria-labelledby":s,autoFocus:e.focusStrategy||!0,onClose:e.close}}}const su=new WeakMap;function Xr(t){return Oc()?t.altKey:t.ctrlKey}function ft(t,e){let n=`[data-key="${CSS.escape(String(e))}"]`,r=t.current?.dataset.collection;return r&&(n=`[data-collection="${CSS.escape(r)}"]${n}`),t.current?.querySelector(n)}const lu=new WeakMap;function om(t){let e=xe();return lu.set(t,e),e}function im(t){return lu.get(t)}const Ni=1e3;function sm(t){let{keyboardDelegate:e,selectionManager:n,onTypeSelect:r}=t,o=a.useRef({search:"",timeout:void 0}),i=l=>{if(o.current.search.length>0&&l.key===" "){if(l.preventDefault(),(!("continuePropagation"in l)||"continuePropagation"in l&&!l.isPropagationStopped())&&l.stopPropagation(),o.current.search+=" ",e.getKeyForSearch!=null){let u=e.getKeyForSearch(o.current.search,n.focusedKey);u==null&&(u=e.getKeyForSearch(o.current.search)),u!=null&&(n.setFocusedKey(u),r&&r(u))}clearTimeout(o.current.timeout),o.current.timeout=setTimeout(()=>{o.current.search=""},Ni)}},s=l=>{let u=lm(l.key);if(!(!u||l.ctrlKey||l.metaKey||l.altKey||!Y(l.currentTarget,V(l))||o.current.search.length===0&&u===" ")){if(o.current.search+=u,e.getKeyForSearch!=null){let c=e.getKeyForSearch(o.current.search,n.focusedKey);if(c==null&&(c=e.getKeyForSearch(o.current.search)),c!=null)n.setFocusedKey(c),r&&r(c),l.preventDefault(),"continuePropagation"in l||l.stopPropagation();else{o.current.search="",clearTimeout(o.current.timeout),o.current.timeout=void 0;return}}clearTimeout(o.current.timeout),o.current.timeout=setTimeout(()=>{o.current.search=""},Ni)}};return a.useEffect(()=>{let l=o.current.timeout;return()=>{clearTimeout(l)}},[o]),{typeSelectProps:{onKeyDownCapture:e.getKeyForSearch?i:void 0,onKeyDown:e.getKeyForSearch?s:void 0}}}function lm(t){return t.length===1||!/^[A-Z]/i.test(t)?t:""}function am(t){let{selectionManager:e,keyboardDelegate:n,ref:r,autoFocus:o=!1,shouldFocusWrap:i=!1,disallowEmptySelection:s=!1,disallowSelectAll:l=!1,escapeKeyBehavior:u="clearSelection",selectOnFocus:c=e.selectionBehavior==="replace",disallowTypeAhead:d=!1,shouldUseVirtualFocus:f,allowsTabNavigation:p=!1,scrollRef:m=r,linkBehavior:h="action",UNSTABLE_focusOnEntry:g}=t,{direction:b}=it(),y=tn();const v=(D,N,ie)=>{if(N!=null){if(e.isLink(N)&&h==="selection"&&c&&!Xr(D)){Qe.flushSync(()=>{e.setFocusedKey(N,ie)});let se=ft(r,N),dn=e.getItemProps(N);if(se){y.open(se,D,dn.href,dn.routerOptions);return}return!1}if(e.setFocusedKey(N,ie),e.isLink(N)&&h==="override")return!1;if(D.shiftKey&&e.selectionMode==="multiple"){e.extendSelection(N);return}else if(c&&!Xr(D)){e.replaceSelection(N);return}}return!1};let w=D=>{if(n.getKeyBelow){let N=e.focusedKey!=null?n.getKeyBelow?.(e.focusedKey):n.getFirstKey?.();if(N==null&&i&&(N=n.getFirstKey?.(e.focusedKey)),N!=null){v(D,N);return}}return!1},x=D=>{if(n.getKeyAbove){let N=e.focusedKey!=null?n.getKeyAbove?.(e.focusedKey):n.getLastKey?.();if(N==null&&i&&(N=n.getLastKey?.(e.focusedKey)),N!=null){v(D,N);return}}return!1},F=D=>{if(n.getFirstKey){if(e.focusedKey===null&&D.shiftKey)return!1;let N=n.getFirstKey(e.focusedKey,Bt(D));if(e.setFocusedKey(N),N!=null){if(Bt(D)&&D.shiftKey&&e.selectionMode==="multiple"){e.extendSelection(N);return}else if(c){e.replaceSelection(N);return}}}return!1},K=D=>{if(n.getKeyLeftOf){let N=e.focusedKey!=null?n.getKeyLeftOf?.(e.focusedKey):n.getFirstKey?.();if(N==null&&i&&(N=b==="rtl"?n.getFirstKey?.(e.focusedKey):n.getLastKey?.(e.focusedKey)),N!=null){v(D,N,b==="rtl"?"first":"last");return}}return!1},P=D=>{if(n.getKeyRightOf){let N=e.focusedKey!=null?n.getKeyRightOf?.(e.focusedKey):n.getFirstKey?.();if(N==null&&i&&(N=b==="rtl"?n.getLastKey?.(e.focusedKey):n.getFirstKey?.(e.focusedKey)),N!=null){v(D,N,b==="rtl"?"last":"first");return}}return!1},S=D=>{if(n.getLastKey){if(e.focusedKey===null&&D.shiftKey)return!1;let N=n.getLastKey(e.focusedKey,Bt(D));if(e.setFocusedKey(N),N!=null){if(Bt(D)&&D.shiftKey&&e.selectionMode==="multiple"){e.extendSelection(N);return}else if(c){e.replaceSelection(N);return}}}return!1},I=D=>{if(n.getKeyPageBelow&&e.focusedKey!=null){let N=n.getKeyPageBelow(e.focusedKey);if(N!=null)return v(D,N)}return!1},M=D=>{if(n.getKeyPageAbove&&e.focusedKey!=null){let N=n.getKeyPageAbove(e.focusedKey);if(N!=null)return v(D,N)}return!1},O=()=>{if(e.selectionMode==="multiple"&&l!==!0){e.selectAll();return}return!1},z=()=>{if(u==="clearSelection"&&!s&&e.selectedKeys.size!==0){e.clearSelection();return}return!1},U=()=>{if(!p&&r.current){let D=Ve(r.current,{tabbable:!0}),N,ie;do ie=D.lastChild(),ie&&(N=ie);while(ie);let se=re();N&&(!Xe(N)||se&&!lo(se))&&ce(N)}return{shouldContinuePropagation:!0,shouldPreventDefault:!1}},C=()=>(!p&&r.current&&r.current.focus(),{shouldContinuePropagation:!0,shouldPreventDefault:!1}),R=(D,N)=>({[Me()?D+"+Shift+Alt":D+"+Shift+Control"]:N,[D+"+Shift"]:N,[Me()?D+"+Alt":D+"+Control"]:N,[D]:N}),{keyboardProps:H}=tt({shortcuts:{...R("ArrowDown",w),...R("ArrowUp",x),...R("ArrowLeft",K),...R("ArrowRight",P),...R("PageDown",I),...R("PageUp",M)},allowRepeats:!0}),{keyboardProps:L}=tt({shortcuts:{...R("Home",F),...R("End",S),"Mod+A":O,Escape:z,Tab:U,"Tab+Shift":C}}),q=a.useRef({top:0,left:0});_t(m,"scroll",()=>{q.current={top:m.current?.scrollTop??0,left:m.current?.scrollLeft??0}});let T=D=>{if(e.isFocused){Y(D.currentTarget,V(D))||e.setFocused(!1);return}if(!Y(D.currentTarget,V(D)))return;let N=Je();e.setFocused(!0);let ie=se=>{se!=null&&(e.setFocusedKey(se),c&&!e.isSelected(se)&&e.replaceSelection(se))};if(g&&(N==="keyboard"||N==="virtual"))ie(g==="first"?n.getFirstKey?.():n.getLastKey?.());else if(e.focusedKey==null){let se=D.relatedTarget;se&&D.currentTarget.compareDocumentPosition(se)&Node.DOCUMENT_POSITION_FOLLOWING?ie(e.lastSelectedKey??n.getLastKey?.()):ie(e.firstSelectedKey??n.getFirstKey?.())}else m.current&&(m.current.scrollTop=q.current.top,m.current.scrollLeft=q.current.left);if(e.focusedKey!=null&&m.current){let se=ft(r,e.focusedKey);se instanceof HTMLElement&&(!Xe(se)&&!f&&ce(se),(N==="keyboard"||g&&N==="virtual")&&zo(se,{containingElement:r.current}))}},A=D=>{Y(D.currentTarget,D.relatedTarget)||e.setFocused(!1)},j=a.useRef(!1);_t(r,Dc,f?D=>{let{detail:N}=D;D.stopPropagation(),e.setFocused(!0),N?.focusStrategy==="first"&&(j.current=!0)}:void 0);let W=n.getFirstKey?.()??null;jo(()=>{if(j.current)if(W==null){let D=re();va(r.current),ya(D,null),e.collection.size>0&&(j.current=!1)}else e.setFocusedKey(W),j.current=!1},[W,e.collection.size]),jo(()=>{e.collection.size>0&&(j.current=!1)},[e.focusedKey]),_t(r,Kc,f?D=>{D.stopPropagation(),e.setFocused(!1),D.detail?.clearFocusKey&&e.setFocusedKey(null)}:void 0);const ne=a.useRef(o),te=a.useRef(!1);a.useEffect(()=>{if(ne.current){let D=null;o==="first"&&(D=n.getFirstKey?.()??null),o==="last"&&(D=n.getLastKey?.()??null);let N=e.selectedKeys;if(N.size){for(let ie of N)if(e.canSelectItem(ie)){D=ie;break}}e.setFocused(!0),e.setFocusedKey(D),D!=null&&c&&!N.size&&e.canSelectItem(D)&&e.replaceSelection(D),D==null&&!f&&r.current&&et(r.current),e.collection.size>0&&(ne.current=!1,te.current=!0)}});let k=a.useRef(e.focusedKey),Z=a.useRef(null);a.useEffect(()=>{if(e.isFocused&&e.focusedKey!=null&&(e.focusedKey!==k.current||te.current)&&m.current&&r.current){let D=Je(),N=ft(r,e.focusedKey);if(!(N instanceof HTMLElement))return;(D==="keyboard"||te.current)&&(Z.current&&cancelAnimationFrame(Z.current),Z.current=requestAnimationFrame(()=>{m.current&&(Mn(m.current,N),D!=="virtual"&&zo(N,{containingElement:r.current}))}))}!f&&e.isFocused&&e.focusedKey==null&&k.current!=null&&r.current&&et(r.current),k.current=e.focusedKey,te.current=!1}),a.useEffect(()=>()=>{Z.current&&cancelAnimationFrame(Z.current)},[]),_t(r,"react-aria-focus-scope-restore",D=>{D.preventDefault(),e.setFocused(!0)});let oe={...Q(L,H),onFocus:T,onBlur:A,onMouseDown(D){m.current===V(D)&&D.preventDefault()}},{typeSelectProps:ye}=sm({keyboardDelegate:n,selectionManager:e});d||(oe=Q(ye,oe));let G;f||(G=e.focusedKey==null?0:-1);let Fe=om(e.collection);return{collectionProps:Q(oe,{tabIndex:G,"data-collection":Fe})}}class Oi{constructor(e){this.ref=e}getItemRect(e){let n=this.ref.current;if(!n)return null;let r=e!=null?ft(this.ref,e):null;if(!r)return null;let o=n.getBoundingClientRect(),i=r.getBoundingClientRect();return{x:i.left-o.left-n.clientLeft+n.scrollLeft,y:i.top-o.top-n.clientTop+n.scrollTop,width:i.width,height:i.height}}getContentSize(){let e=this.ref.current;return{width:e?.scrollWidth??0,height:e?.scrollHeight??0}}getVisibleRect(){let e=this.ref.current;return{x:e?.scrollLeft??0,y:e?.scrollTop??0,width:e?.clientWidth??0,height:e?.clientHeight??0}}}class um{constructor(...e){if(e.length===1){let n=e[0];this.collection=n.collection,this.ref=n.ref,this.collator=n.collator,this.disabledKeys=n.disabledKeys||new Set,this.disabledBehavior=n.disabledBehavior||"all",this.orientation=n.orientation||"vertical",this.direction=n.direction,this.layout=n.layout||"stack",this.layoutDelegate=n.layoutDelegate||new Oi(n.ref)}else this.collection=e[0],this.disabledKeys=e[1],this.ref=e[2],this.collator=e[3],this.layout="stack",this.orientation="vertical",this.disabledBehavior="all",this.layoutDelegate=new Oi(this.ref);this.layout==="stack"&&this.orientation==="vertical"&&(this.getKeyLeftOf=void 0,this.getKeyRightOf=void 0)}isDisabled(e){return this.disabledBehavior==="all"&&(e.props?.isDisabled||this.disabledKeys.has(e.key))&&e.props?.disabledBehavior!=="selection"}findNextNonDisabled(e,n,r=!1){let o=e;for(;o!=null;){let i=this.collection.getItem(o);if(i?.type==="item"&&(r||!this.isDisabled(i)))return o;o=n(o)}return null}getNextKey(e,n){let r=e;return r=this.collection.getKeyAfter(r),this.findNextNonDisabled(r,o=>this.collection.getKeyAfter(o),n?.includeDisabled)}getPreviousKey(e,n){let r=e;return r=this.collection.getKeyBefore(r),this.findNextNonDisabled(r,o=>this.collection.getKeyBefore(o),n?.includeDisabled)}findKey(e,n,r){let o=e,i=this.layoutDelegate.getItemRect(o);if(!i||o==null)return null;let s=i;do{if(o=n(o),o==null)break;i=this.layoutDelegate.getItemRect(o)}while(i&&r(s,i)&&o!=null);return o}isSameRow(e,n){return e.y===n.y||e.x!==n.x}isSameColumn(e,n){return e.x===n.x||e.y!==n.y}isReversed(e){let n=this.getNextKey(e),r=ft(this.ref,e);if(n!=null){let i=ft(this.ref,n);return!r||!i?!1:r.getBoundingClientRect().top>i.getBoundingClientRect().top}let o=this.getPreviousKey(e);if(o!=null){let i=ft(this.ref,o);return!r||!i?!1:i.getBoundingClientRect().top>r.getBoundingClientRect().top}return!1}getKeyBelow(e,n){return this.layout==="grid"&&this.orientation==="vertical"?this.findKey(e,r=>this.getNextKey(r,n),this.isSameRow):this.orientation==="vertical"?this.isReversed(e)?this.getPreviousKey(e,n):this.getNextKey(e,n):this.getNextKey(e,n)}getKeyAbove(e,n){return this.layout==="grid"&&this.orientation==="vertical"?this.findKey(e,r=>this.getPreviousKey(r,n),this.isSameRow):this.orientation==="vertical"?this.isReversed(e)?this.getNextKey(e,n):this.getPreviousKey(e,n):this.getPreviousKey(e,n)}getNextColumn(e,n,r){return n?this.getPreviousKey(e,r):this.getNextKey(e,r)}getKeyRightOf(e,n){let r=this.direction==="ltr"?"getKeyRightOf":"getKeyLeftOf";return this.layoutDelegate[r]?(e=this.layoutDelegate[r](e),this.findNextNonDisabled(e,o=>this.layoutDelegate[r](o),n?.includeDisabled)):this.layout==="grid"?this.orientation==="vertical"?this.getNextColumn(e,this.direction==="rtl",n):this.findKey(e,o=>this.getNextColumn(o,this.direction==="rtl",n),this.isSameColumn):this.orientation==="horizontal"?this.getNextColumn(e,this.direction==="rtl",n):null}getKeyLeftOf(e,n){let r=this.direction==="ltr"?"getKeyLeftOf":"getKeyRightOf";return this.layoutDelegate[r]?(e=this.layoutDelegate[r](e),this.findNextNonDisabled(e,o=>this.layoutDelegate[r](o),n?.includeDisabled)):this.layout==="grid"?this.orientation==="vertical"?this.getNextColumn(e,this.direction==="ltr",n):this.findKey(e,o=>this.getNextColumn(o,this.direction==="ltr",n),this.isSameColumn):this.orientation==="horizontal"?this.getNextColumn(e,this.direction==="ltr",n):null}getFirstKey(){let e=this.collection.getFirstKey();return this.findNextNonDisabled(e,n=>this.collection.getKeyAfter(n))}getLastKey(){let e=this.collection.getLastKey();return this.findNextNonDisabled(e,n=>this.collection.getKeyBefore(n))}getKeyPageAbove(e){let n=this.ref.current,r=this.layoutDelegate.getItemRect(e);if(!r)return null;let o=this.isReversed(e);if(n&&!Mt(n))return this.getFirstKey();let i=e;if(this.orientation==="horizontal"){let s=Math.max(0,r.x+r.width-this.layoutDelegate.getVisibleRect().width);for(;r&&r.x>s&&i!=null;)i=this.getKeyAbove(i),r=i==null?null:this.layoutDelegate.getItemRect(i)}else{let s=this.layoutDelegate.getVisibleRect(),l=o?r.y-s.height:Math.max(0,r.y+r.height-s.height);for(;r&&r.y>l&&i!=null;)i=this.getKeyAbove(i),r=i==null?null:this.layoutDelegate.getItemRect(i)}return i??(o?this.getLastKey():this.getFirstKey())}getKeyPageBelow(e){let n=this.ref.current,r=this.layoutDelegate.getItemRect(e);if(!r)return null;let o=this.isReversed(e);if(n&&!Mt(n))return this.getLastKey();let i=e;if(this.orientation==="horizontal"){let s=Math.min(this.layoutDelegate.getContentSize().width,r.x-r.width+this.layoutDelegate.getVisibleRect().width);for(;r&&r.xo[0]i||new um({collection:n,disabledKeys:r,disabledBehavior:c,ref:o,collator:u,layoutDelegate:s,orientation:l}),[i,s,n,r,o,u,c,l]),{collectionProps:f}=am({...t,ref:o,selectionManager:e,keyboardDelegate:d});return{listProps:f}}function fm(t,e,n){let{shouldFocusWrap:r=!0,onKeyDown:o,onKeyUp:i,...s}=t;!t["aria-label"]&&t["aria-labelledby"];let l=de(t,{labelable:!0}),{listProps:u}=dm({...s,ref:n,selectionManager:e.selectionManager,collection:e.collection,disabledKeys:e.disabledKeys,shouldFocusWrap:r,linkBehavior:"override"});return su.set(e,{onClose:t.onClose,onAction:t.onAction,shouldUseVirtualFocus:t.shouldUseVirtualFocus}),{menuProps:Q(l,{onKeyDown:o,onKeyUp:i},{role:"menu",...u,onKeyDown:c=>{(c.key!=="Escape"||t.shouldUseVirtualFocus)&&u.onKeyDown?.(c)}})}}function pm(t){let{id:e,selectionManager:n,key:r,ref:o,shouldSelectOnPressUp:i,shouldUseVirtualFocus:s,focus:l,isDisabled:u,onAction:c,allowsDifferentPressOrigin:d,linkBehavior:f="action"}=t,p=tn();e=xe(e);let m=k=>{if(k.pointerType==="keyboard"&&Xr(k))n.toggleSelection(r);else{if(n.selectionMode==="none")return;if(n.isLink(r)){if(f==="selection"&&o.current){let Z=n.getItemProps(r);p.open(o.current,k,Z.href,Z.routerOptions),n.setSelectedKeys(n.selectedKeys);return}else if(f==="override"||f==="none")return}n.selectionMode==="single"?n.isSelected(r)&&!n.disallowEmptySelection?n.toggleSelection(r):n.replaceSelection(r):k&&k.shiftKey?n.extendSelection(r):n.selectionBehavior==="toggle"||k&&(Bt(k)||k.pointerType==="touch"||k.pointerType==="virtual")?n.toggleSelection(r):n.replaceSelection(r)}};a.useEffect(()=>{r===n.focusedKey&&n.isFocused&&(s?va(o.current):l?l():re()!==o.current&&o.current&&et(o.current))},[o,r,n.focusedKey,n.childFocusStrategy,n.isFocused,s]),u=u||n.isDisabled(r);let h={};!s&&!u?h={tabIndex:r===n.focusedKey?0:-1,onFocus(k){V(k)===o.current&&n.setFocusedKey(r)}}:u&&(h.onMouseDown=k=>{k.preventDefault()}),a.useEffect(()=>{u&&n.focusedKey===r&&n.setFocusedKey(null)},[n,u,r]);let g=n.isLink(r)&&f==="override",b=c&&t.UNSTABLE_itemBehavior==="action",y=n.isLink(r)&&f!=="selection"&&f!=="none",v=!u&&n.canSelectItem(r)&&!g&&!b,w=(c||y)&&!u,x=w&&(n.selectionBehavior==="replace"?!v:!v||n.isEmpty),F=w&&v&&n.selectionBehavior==="replace",K=x||F,P=a.useRef(null),S=K&&v,I=a.useRef(!1),M=a.useRef(!1),O=n.getItemProps(r),z=k=>{c&&(c(),o.current?.dispatchEvent(new CustomEvent("react-aria-item-action",{bubbles:!0}))),y&&o.current&&p.open(o.current,k,O.href,O.routerOptions)},U={ref:o};i?(U.onPressStart=k=>{P.current=k.pointerType,I.current=S,k.pointerType==="keyboard"&&(!K||Bi(k.key))&&m(k)},d?(U.onPressUp=x?void 0:k=>{k.pointerType==="mouse"&&v&&m(k)},U.onPress=x?z:k=>{k.pointerType!=="keyboard"&&k.pointerType!=="mouse"&&v&&m(k)}):U.onPress=k=>{if(x||F&&k.pointerType!=="mouse"){if(k.pointerType==="keyboard"&&!Vi(k.key))return;z(k)}else k.pointerType!=="keyboard"&&v&&m(k)}):(U.onPressStart=k=>{P.current=k.pointerType,I.current=S,M.current=x,v&&(k.pointerType==="mouse"&&!x||k.pointerType==="keyboard"&&(!w||Bi(k.key)))&&m(k)},U.onPress=k=>{(k.pointerType==="touch"||k.pointerType==="pen"||k.pointerType==="virtual"||k.pointerType==="keyboard"&&K&&Vi(k.key)||k.pointerType==="mouse"&&M.current)&&(K?z(k):v&&m(k))});let C=im(n.collection);if(h["data-collection"]=C,h["data-key"]=r,U.preventFocusOnPress=s,s&&(U=Q(U,{onPressStart(k){k.pointerType!=="touch"&&(n.setFocused(!0),n.setFocusedKey(r))},onPress(k){k.pointerType==="touch"&&(n.setFocused(!0),n.setFocusedKey(r))}})),O)for(let k of["onPressStart","onPressEnd","onPressChange","onPress","onPressUp","onClick"])O[k]&&(U[k]=ht(U[k],O[k]));let{pressProps:R,isPressed:H}=on(U),L=F?k=>{P.current==="mouse"&&(k.stopPropagation(),k.preventDefault(),z(k))}:void 0,{longPressProps:q}=So({isDisabled:!S,onLongPress(k){k.pointerType==="touch"&&(m(k),n.setSelectionBehavior("toggle"))}}),T=k=>{P.current==="touch"&&I.current&&k.preventDefault()},A=f!=="none"&&n.isLink(r)?k=>{Be.isOpening||k.preventDefault()}:void 0,j=Q(h,v||x||s&&!u?R:{},S?q:{},{onDoubleClick:L,onDragStartCapture:T,onClick:A,id:e},s?{onMouseDown:k=>k.preventDefault()}:void 0),W=k=>{let Z=k;for(;Z&&Z!==o.current;){let oe=Z.getAttribute("data-collection");if(oe!=null)return oe!==C;Z=Z.parentElement}return lo(k)},ne=j.onPointerDown;j.onPointerDown=k=>{let Z=V(k);if(Z&&Z!==o.current&&W(Z)){k.stopPropagation();return}ne?.(k)};let te=j.onMouseDown;return j.onMouseDown=k=>{let Z=V(k);if(Z&&Z!==o.current&&W(Z)){k.stopPropagation();return}te?.(k)},{itemProps:j,isPressed:H,isSelected:n.isSelected(r),isFocused:n.isFocused&&n.focusedKey===r,isDisabled:u,allowsSelection:v,hasAction:K}}function Vi(t){return t==="Enter"}function Bi(t){return t===" "}function au(t,e){return typeof e.getChildren=="function"?e.getChildren(t.key):t.childNodes}function hm(t){return mm(t)}function mm(t,e){for(let n of t)return n}function kr(t,e,n){if(e.parentKey===n.parentKey)return e.index-n.index;let r=[..._i(t,e),e],o=[..._i(t,n),n],i=r.slice(0,o.length).findIndex((s,l)=>s!==o[l]);return i!==-1?(e=r[i],n=o[i],e.index-n.index):r.findIndex(s=>s===n)>=0?1:(o.findIndex(s=>s===e)>=0,-1)}function _i(t,e){let n=[],r=e;for(;r?.parentKey!=null;)r=t.getItem(r.parentKey),r&&n.unshift(r);return n}const Hi=new WeakMap;function bm(t){let e=Hi.get(t);if(e!=null)return e;let n=0,r=o=>{for(let i of o)i.type==="section"?r(au(i,t)):i.type==="item"&&n++};return r(t),Hi.set(t,n),n}function gm(t,e,n){let{id:r,key:o,closeOnSelect:i,shouldCloseOnSelect:s,isVirtualized:l,"aria-haspopup":u,onPressStart:c,onPressUp:d,onPress:f,onPressChange:p,onPressEnd:m,onClick:h,onHoverStart:g,onHoverChange:b,onHoverEnd:y,onKeyDown:v,onKeyUp:w,onFocus:x,onFocusChange:F,onBlur:K,selectionManager:P=e.selectionManager}=t,S=!!u,I=S&&t["aria-expanded"]==="true",M=t.isDisabled??P.isDisabled(o),O=t.isSelected??P.isSelected(o),z=su.get(e),U=e.collection.getItem(o),C=t.onClose||z.onClose,R=tn(),H=()=>{if(!S&&(U?.props?.onAction?U.props.onAction():t.onAction&&t.onAction(o),z.onAction)){let ae=z.onAction;ae(o,U?.value)}},L="menuitem";S||(P.selectionMode==="single"?L="menuitemradio":P.selectionMode==="multiple"&&(L="menuitemcheckbox"));let q=Ye(),T=Ye(),A=Ye(),j={id:r,"aria-disabled":M||void 0,role:L,"aria-label":t["aria-label"],"aria-labelledby":q,"aria-describedby":[t["aria-describedby"],T,A].filter(Boolean).join(" ")||void 0,"aria-controls":t["aria-controls"],"aria-haspopup":u,"aria-expanded":t["aria-expanded"]};if(P.selectionMode!=="none"&&!S&&(j["aria-checked"]=O),l){let ae=Number(U?.index);j["aria-posinset"]=Number.isNaN(ae)?void 0:ae+1,j["aria-setsize"]=bm(e.collection)}let W=a.useRef(!1),ne=ae=>{p?.(ae),W.current=ae},te=a.useRef(null),k=ae=>{ae.pointerType!=="keyboard"&&(te.current={pointerType:ae.pointerType}),ae.pointerType==="mouse"&&(W.current||ae.target.click()),d?.(ae)},Z=ae=>{h?.(ae),H(),fs(ae,R,U.props.href,U?.props.routerOptions);let yt=te.current?.pointerType==="keyboard"?te.current?.key==="Enter"||P.selectionMode==="none"||P.isLink(o):P.selectionMode!=="multiple"||P.isLink(o);yt=s??i??yt,C&&!S&&yt&&C(),te.current=null},{itemProps:oe,isFocused:ye}=pm({id:r,selectionManager:P,key:o,ref:n,shouldSelectOnPressUp:!0,allowsDifferentPressOrigin:!0,linkBehavior:"none",shouldUseVirtualFocus:z.shouldUseVirtualFocus}),{pressProps:G,isPressed:Fe}=on({onPressStart:c,onPress:f,onPressUp:k,onPressChange:ne,onPressEnd:m,isDisabled:M}),{hoverProps:D}=Ue({isDisabled:M,onHoverStart(ae){!At()&&!(I&&u)&&(P.setFocused(!0),P.setFocusedKey(o)),g?.(ae)},onHoverChange:b,onHoverEnd:y}),{keyboardProps:N}=tt({shortcuts:{" ":ae=>{te.current={pointerType:"keyboard",key:" "},V(ae).click(),Ln("keyboard")},Enter:ae=>{te.current={pointerType:"keyboard",key:"Enter"};let yt=V(ae);if(yt.tagName!=="A"){yt.click(),Ln("keyboard");return}return Ln("keyboard"),{shouldPreventDefault:!1,shouldContinuePropagation:!1}}},onKeyDown:v,onKeyUp:w}),{focusableProps:ie}=nr({onBlur:K,onFocus:x,onFocusChange:F},n),se=de(U?.props);delete se.id;let dn=ds(U?.props);return{menuItemProps:{...j,...Q(se,dn,S?{onFocus:oe.onFocus,"data-collection":oe["data-collection"],"data-key":oe["data-key"]}:oe,G,D,N,ie,z.shouldUseVirtualFocus||S?{onMouseDown:ae=>ae.preventDefault()}:void 0,M?void 0:{onClick:Z}),tabIndex:oe.tabIndex!=null&&I&&!z.shouldUseVirtualFocus?-1:oe.tabIndex},labelProps:{id:q},descriptionProps:{id:T},keyboardShortcutProps:{id:A},isFocused:ye,isFocusVisible:ye&&P.isFocused&&At()&&!I,isSelected:O,isPressed:Fe,isDisabled:M}}function vm(t){let{heading:e,"aria-label":n}=t,r=xe();return{itemProps:{role:"presentation"},headingProps:e?{id:r,role:"presentation"}:{},groupProps:{role:"group","aria-label":n,"aria-labelledby":e?r:void 0}}}const Pn=2,ym=50,$m=1e3,ji=Math.PI/12;function xm(t){let{menuRef:e,submenuRef:n,isOpen:r,isDisabled:o}=t,i=a.useRef(void 0),s=a.useRef(void 0),l=a.useRef(0),u=a.useRef(void 0),c=a.useRef(void 0),d=a.useRef(void 0),f=a.useRef(2),[p,m]=a.useState(!1);Dn({ref:r?n:void 0,onResize:()=>{n.current&&(s.current=n.current.getBoundingClientRect(),d.current=void 0)}});let g=()=>{m(!1),f.current=Pn,i.current=void 0},b=rp(),y=Ce(v=>{p&&v.preventDefault()});a.useEffect(()=>{p&&e.current?e.current.style.pointerEvents="none":e.current.style.pointerEvents=""},[e,p]),J(()=>{let v=n.current,w=e.current;if(o||!v||!r||b!=="pointer"||!w){g();return}s.current=v.getBoundingClientRect();let x=F=>{if(F.pointerType==="touch"||F.pointerType==="pen")return;let K=Date.now();if(K-l.currents.current.right?"left":"right"),Pw.getBoundingClientRect().right||Sw.getBoundingClientRect().bottom){g();return}let I=i.current.x,M=i.current.y,O=d.current==="right"?s.current.left-I:I-s.current.right,z=Math.atan2(M-s.current.top,O)+ji,U=Math.atan2(M-s.current.bottom,O)-ji,C=Math.atan2(M-S,d.current==="left"?-(P-I):P-I),R=CU;f.current=R?Math.min(f.current+1,Pn):Math.max(f.current-1,0),f.current>=Pn?m(!0):m(!1),l.current=K,i.current={x:P,y:S},R&&(u.current=setTimeout(()=>{g(),c.current=setTimeout(()=>{let H=document.elementFromPoint(P,S);H&&Y(w,H)&&H.dispatchEvent(new PointerEvent("pointerover",{bubbles:!0,cancelable:!0}))},100)},$m))};return window.addEventListener("pointermove",x),window.addEventListener("pointerdown",y,!0),()=>{window.removeEventListener("pointermove",x),window.removeEventListener("pointerdown",y,!0),clearTimeout(u.current),clearTimeout(c.current),f.current=Pn}},[o,r,e,b,m,n])}function Em(t,e,n){let{parentMenuRef:r,submenuRef:o,type:i="menu",isDisabled:s,delay:l=200,shouldUseVirtualFocus:u}=t,c=xe(),d=xe(),{direction:f}=it(),p=a.useRef(void 0),m=a.useCallback(()=>{p.current&&(clearTimeout(p.current),p.current=void 0)},[p]),h=a.useCallback(P=>{m(),e.open(P)},[e,m]),g=a.useCallback(()=>{m(),e.close()},[e,m]);J(()=>()=>{m()},[m]);let{keyboardProps:b}=tt({shortcuts:{ArrowLeft:P=>{if(!Xe(P.currentTarget))return!1;if(f==="ltr"&&Y(P.currentTarget,V(P))){g(),!u&&n.current&&ce(n.current);return}return!1},ArrowRight:P=>{if(!Xe(P.currentTarget))return!1;if(f==="rtl"&&Y(P.currentTarget,V(P))){g(),!u&&n.current&&ce(n.current);return}return!1},Escape:P=>{if(!Xe(P.currentTarget))return!1;if(Y(o.current,V(P))){g(),!u&&n.current&&ce(n.current);return}return!1}}}),y={id:d,"aria-labelledby":c,submenuLevel:e.submenuLevel,...i==="menu"&&{onClose:e.closeAll,autoFocus:e.focusStrategy??void 0,...b}},{keyboardProps:v}=tt({shortcuts:{ArrowRight:()=>{if(!s)if(f==="ltr"){e.isOpen||h("first"),i==="menu"&&o?.current&&re()===n?.current&&ce(o.current);return}else if(e.isOpen){g();return}else return!1;return!1},ArrowLeft:()=>{if(!s)if(f==="rtl"){e.isOpen||h("first"),i==="menu"&&o?.current&&re()===n?.current&&ce(o.current);return}else if(e.isOpen){g();return}else return!1;return!1}}}),w=P=>{!s&&(P.pointerType==="virtual"||P.pointerType==="keyboard")&&h("first")},x=P=>{!s&&(P.pointerType==="touch"||P.pointerType==="mouse")&&h()},F=P=>{s||(P&&!e.isOpen?p.current||(p.current=setTimeout(()=>{h()},l)):P||m())};_t(r,"focusin",P=>{e.isOpen&&Y(r.current,V(P))&&V(P)!==n.current&&g()});let K=P=>P!==n.current;return xm({menuRef:r,submenuRef:o,isOpen:e.isOpen,isDisabled:s}),{submenuTriggerProps:{onKeyDown:v.onKeyDown,onKeyUp:v.onKeyUp,id:c,"aria-controls":e.isOpen?d:void 0,"aria-haspopup":s?void 0:i,"aria-expanded":e.isOpen?"true":"false",onPressStart:w,onPress:x,onHoverChange:F,isOpen:e.isOpen},submenuProps:y,popoverProps:{isNonModal:!0,shouldCloseOnInteractOutside:K}}}function Po(t){let e=aa(t),[n,r]=a.useState(null),[o,i]=a.useState([]),s=()=>{i([]),e.close()};return{focusStrategy:n,...e,open(c=null){r(c),e.open()},toggle(c=null){r(c),e.toggle()},close(){s()},expandedKeysStack:o,openSubmenu:(c,d)=>{i(f=>d>f.length?f:[...f.slice(0,d),c])},closeSubmenu:(c,d)=>{i(f=>f[d]===c?f.slice(0,d):f)}}}function wm(t,e){let{triggerKey:n}=t,{expandedKeysStack:r,openSubmenu:o,closeSubmenu:i,close:s}=e,[l]=a.useState(r?.length),u=a.useMemo(()=>r[l]===n,[r,n,l]),[c,d]=a.useState(null),f=a.useCallback(h=>{d(h??null),o(n,l)},[o,l,n]),p=a.useCallback(()=>{d(null),i(n,l)},[i,l,n]),m=a.useCallback(h=>{d(h??null),u?p():f(h)},[p,f,u]);return a.useMemo(()=>({focusStrategy:c,isOpen:u,open:f,close:p,closeAll:s,submenuLevel:l,setOpen:()=>{},toggle:m,point:null,setPoint:()=>{}}),[u,f,p,s,c,m,l])}class Re extends Set{constructor(e,n,r){super(e),e instanceof Re?(this.anchorKey=n??e.anchorKey,this.currentKey=r??e.currentKey):(this.anchorKey=n??null,this.currentKey=r??null)}}class ur{constructor(e,n,r){this.collection=e,this.state=n,this.allowsCellSelection=r?.allowsCellSelection??!1,this._isSelectAll=null,this.layoutDelegate=r?.layoutDelegate||null,this.fullCollection=r?.fullCollection||null}get selectionMode(){return this.state.selectionMode}get disallowEmptySelection(){return this.state.disallowEmptySelection}get selectionBehavior(){return this.state.selectionBehavior}setSelectionBehavior(e){this.state.setSelectionBehavior(e)}get isFocused(){return this.state.isFocused}setFocused(e){this.state.setFocused(e)}get focusedKey(){return this.state.focusedKey}get childFocusStrategy(){return this.state.childFocusStrategy}setFocusedKey(e,n){(e==null||this.collection.getItem(e))&&this.state.setFocusedKey(e,n)}get selectedKeys(){return this.state.selectedKeys==="all"?new Set(this.getSelectAllKeys()):this.state.selectedKeys}get rawSelection(){return this.state.selectedKeys}isSelected(e){if(this.state.selectionMode==="none")return!1;let n=this.getKey(e);return n==null?!1:this.state.selectedKeys==="all"?this.canSelectItem(n):this.state.selectedKeys.has(n)}get isEmpty(){return this.state.selectedKeys!=="all"&&this.state.selectedKeys.size===0}get isSelectAll(){if(this.isEmpty)return!1;if(this.state.selectedKeys==="all")return!0;if(this._isSelectAll!=null)return this._isSelectAll;let e=this.getSelectAllKeys(),n=this.state.selectedKeys;return this._isSelectAll=e.every(r=>n.has(r)),this._isSelectAll}get firstSelectedKey(){let e=null;for(let n of this.state.selectedKeys){let r=this.collection.getItem(n);(!e||r&&kr(this.collection,r,e)<0)&&(e=r)}return e?.key??null}get lastSelectedKey(){let e=null;for(let n of this.state.selectedKeys){let r=this.collection.getItem(n);(!e||r&&kr(this.collection,r,e)>0)&&(e=r)}return e?.key??null}get disabledKeys(){return this.state.disabledKeys}get disabledBehavior(){return this.state.disabledBehavior}extendSelection(e){if(this.selectionMode==="none")return;if(this.selectionMode==="single"){this.replaceSelection(e);return}let n=this.getKey(e);if(n==null)return;let r;if(this.state.selectedKeys==="all")r=new Re([n],n,n);else{let o=this.state.selectedKeys,i=o.anchorKey??n;r=new Re(o,i,n);for(let s of this.getKeyRange(i,o.currentKey??n))r.delete(s);for(let s of this.getKeyRange(n,i))this.canSelectItem(s)&&r.add(s)}this.state.setSelectedKeys(r)}getKeyRange(e,n){let r=this.collection.getItem(e),o=this.collection.getItem(n);return r&&o?kr(this.collection,r,o)<=0?this.getKeyRangeInternal(e,n):this.getKeyRangeInternal(n,e):[]}getKeyRangeInternal(e,n){if(this.layoutDelegate?.getKeyRange)return this.layoutDelegate.getKeyRange(e,n);let r=[],o=e;for(;o!=null;){let i=this.collection.getItem(o);if(i&&(i.type==="item"||i.type==="cell"&&this.allowsCellSelection)&&r.push(o),o===n)return r;o=this.collection.getKeyAfter(o)}return[]}getKey(e){let n=this.collection.getItem(e);if(!n||n.type==="cell"&&this.allowsCellSelection)return e;for(;n&&n.type!=="item"&&n.parentKey!=null;)n=this.collection.getItem(n.parentKey);return!n||n.type!=="item"?null:n.key}toggleSelection(e){if(this.selectionMode==="none")return;if(this.selectionMode==="single"&&!this.isSelected(e)){this.replaceSelection(e);return}let n=this.getKey(e);if(n==null)return;let r=new Re(this.state.selectedKeys==="all"?this.getSelectAllKeys():this.state.selectedKeys);r.has(n)?r.delete(n):this.canSelectItem(n)&&(r.add(n),r.anchorKey=n,r.currentKey=n),!(this.disallowEmptySelection&&r.size===0)&&this.state.setSelectedKeys(r)}replaceSelection(e){if(this.selectionMode==="none")return;let n=this.getKey(e);if(n==null)return;let r=this.canSelectItem(n)?new Re([n],n,n):new Re;this.state.setSelectedKeys(r)}setSelectedKeys(e){if(this.selectionMode==="none")return;let n=new Re;for(let r of e){let o=this.getKey(r);if(o!=null&&(n.add(o),this.selectionMode==="single"))break}this.state.setSelectedKeys(n)}getSelectAllKeys(){let e=this.fullCollection??this.collection,n=[],r=o=>{for(;o!=null;){if(this.canSelectItemIn(o,e)){let i=e.getItem(o);i?.type==="item"&&n.push(o),i?.hasChildNodes&&(this.allowsCellSelection||i.type!=="item")&&r(hm(au(i,e))?.key??null)}o=e.getKeyAfter(o)}};return r(e.getFirstKey()),n}selectAll(){!this.isSelectAll&&this.selectionMode==="multiple"&&this.state.setSelectedKeys("all")}clearSelection(){!this.disallowEmptySelection&&(this.state.selectedKeys==="all"||this.state.selectedKeys.size>0)&&this.state.setSelectedKeys(new Re)}toggleSelectAll(){this.isSelectAll?this.clearSelection():this.selectAll()}select(e,n){this.selectionMode!=="none"&&(this.selectionMode==="single"?this.isSelected(e)&&!this.disallowEmptySelection?this.toggleSelection(e):this.replaceSelection(e):this.selectionBehavior==="toggle"||n&&(n.pointerType==="touch"||n.pointerType==="virtual")?this.toggleSelection(e):this.replaceSelection(e))}isSelectionEqual(e){if(e===this.state.selectedKeys)return!0;let n=this.selectedKeys;if(e.size!==n.size)return!1;for(let r of e)if(!n.has(r))return!1;for(let r of n)if(!e.has(r))return!1;return!0}canSelectItem(e){return this.canSelectItemIn(e,this.collection)}canSelectItemIn(e,n){if(this.state.selectionMode==="none"||this.state.disabledKeys.has(e))return!1;let r=n.getItem(e);return!(!r||r?.props?.isDisabled||r.type==="cell"&&!this.allowsCellSelection)}isDisabled(e){let n=this.collection.getItem(e);return this.state.disabledBehavior==="all"&&(this.state.disabledKeys.has(e)||!!n?.props?.isDisabled)&&n?.props?.disabledBehavior!=="selection"}isLink(e){return!!this.collection.getItem(e)?.props?.href}getItemProps(e){return this.collection.getItem(e)?.props}withCollection(e){return new ur(e,this.state,{allowsCellSelection:this.allowsCellSelection,layoutDelegate:this.layoutDelegate||void 0,fullCollection:this.fullCollection??this.collection})}}class Cm{constructor(e,{expandedKeys:n}={}){this.keyMap=new Map,this.firstKey=null,this.lastKey=null,this.iterable=e,n=n||new Set;let r=s=>{if(this.keyMap.set(s.key,s),s.childNodes&&(s.type==="section"||n.has(s.key)))for(let l of s.childNodes)r(l)};for(let s of e)r(s);let o=null,i=0;for(let[s,l]of this.keyMap)o?(o.nextKey=s,l.prevKey=o.key):(this.firstKey=s,l.prevKey=void 0),l.type==="item"&&(l.index=i++),o=l,o.nextKey=void 0;this.lastKey=o?.key??null}*[Symbol.iterator](){yield*this.iterable}get size(){return this.keyMap.size}getKeys(){return this.keyMap.keys()}getKeyBefore(e){let n=this.keyMap.get(e);return n?n.prevKey??null:null}getKeyAfter(e){let n=this.keyMap.get(e);return n?n.nextKey??null:null}getFirstKey(){return this.firstKey}getLastKey(){return this.lastKey}getItem(e){return this.keyMap.get(e)??null}at(e){const n=[...this.getKeys()];return this.getItem(n[e])}}class Sm{build(e,n){return this.context=n,zi(()=>this.iterateCollection(e))}*iterateCollection(e){let{children:n,items:r}=e;if($.isValidElement(n)&&n.type===$.Fragment)yield*this.iterateCollection({children:n.props.children,items:r});else if(typeof n=="function"){if(!r)throw new Error("props.children was a function but props.items is missing");let o=0;for(let i of r)yield*this.getFullNode({value:i,index:o},{renderer:n}),o++}else{let o=[];$.Children.forEach(n,s=>{s&&o.push(s)});let i=0;for(let s of o){let l=this.getFullNode({element:s,index:i},{});for(let u of l)i++,yield u}}}getKey(e,n,r,o){if(e.key!=null)return e.key;if(n.type==="cell"&&n.key!=null)return`${o}${n.key}`;let i=n.value;if(i!=null){let s=i.key??i.id;if(s==null)throw new Error("No key found for item");return s}return o?`${o}.${n.index}`:`$.${n.index}`}getChildState(e,n){return{renderer:n.renderer||e.renderer}}*getFullNode(e,n,r,o){if($.isValidElement(e.element)&&e.element.type===$.Fragment){let u=[];$.Children.forEach(e.element.props.children,d=>{u.push(d)});let c=e.index??0;for(const d of u)yield*this.getFullNode({element:d,index:c++},n,r,o);return}let i=e.element;if(!i&&e.value&&n&&n.renderer){let u=this.cache.get(e.value);if(u&&(!u.shouldInvalidate||!u.shouldInvalidate(this.context))){u.index=e.index,u.parentKey=o?o.key:null,yield u;return}i=n.renderer(e.value)}if($.isValidElement(i)){let u=i.type;if(typeof u!="function"&&typeof u.getCollectionNode!="function"){let p=i.type;throw new Error(`Unknown element <${p}> in collection.`)}let c=u.getCollectionNode(i.props,this.context),d=e.index??0,f=c.next();for(;!f.done&&f.value;){let p=f.value;e.index=d;let m=p.key??null;m==null&&(m=p.element?null:this.getKey(i,e,n,r));let g=[...this.getFullNode({...p,key:m,index:d,wrapper:Pm(e.wrapper,p.wrapper)},this.getChildState(n,p),r?`${r}${i.key}`:i.key,o)];for(let b of g){if(b.value=p.value??e.value??null,b.value&&this.cache.set(b.value,b),e.type&&b.type!==e.type)throw new Error(`Unsupported type <${Mr(b.type)}> in <${Mr(o?.type??"unknown parent type")}>. Only <${Mr(e.type)}> is supported.`);d++,yield b}f=c.next(g)}return}if(e.key==null||e.type==null)return;let s=this,l={type:e.type,props:e.props,key:e.key,parentKey:o?o.key:null,value:e.value??null,level:(o?.level??0)+(o?.type==="item"?1:0),index:e.index,rendered:e.rendered,textValue:e.textValue??"","aria-label":e["aria-label"],wrapper:e.wrapper,shouldInvalidate:e.shouldInvalidate,hasChildNodes:e.hasChildNodes||!1,childNodes:zi(function*(){if(!e.hasChildNodes||!e.childNodes)return;let u=0;for(let c of e.childNodes()){c.key!=null&&(c.key=`${l.key}${c.key}`);let d=s.getFullNode({...c,index:u},s.getChildState(n,c),l.key,l);for(let f of d)u++,yield f}})};yield l}constructor(){this.cache=new WeakMap}}function zi(t){let e=[],n=null;return{*[Symbol.iterator](){for(let r of e)yield r;n||(n=t());for(let r of n)e.push(r),yield r}}}function Pm(t,e){if(t&&e)return n=>t(e(n));if(t)return t;if(e)return e}function Mr(t){return t[0].toUpperCase()+t.slice(1)}function Tm(t,e,n){let r=a.useMemo(()=>new Sm,[]),{children:o,items:i,collection:s}=t;return a.useMemo(()=>{if(s)return s;let u=r.build({children:o,items:i},n);return e(u)},[r,o,i,s,n,e])}function km(t,e){if(t.size!==e.size)return!1;for(let n of t)if(!e.has(n))return!1;return!0}function uu(t){let{selectionMode:e="none",disallowEmptySelection:n=!1,allowDuplicateSelectionEvents:r,selectionBehavior:o="toggle",disabledBehavior:i="all"}=t,s=a.useRef(!1),[,l]=a.useState(!1),u=a.useRef(null),c=a.useRef(null),[,d]=a.useState(null),f=a.useMemo(()=>Ui(t.selectedKeys),[t.selectedKeys]),p=a.useMemo(()=>Ui(t.defaultSelectedKeys,new Re),[t.defaultSelectedKeys]),[m,h]=rr(f,p,t.onSelectionChange),g=a.useMemo(()=>t.disabledKeys?new Set(t.disabledKeys):new Set,[t.disabledKeys]),[b,y]=a.useState(o);o==="replace"&&b==="toggle"&&typeof m=="object"&&m.size===0&&y("replace");let v=a.useRef(o);return a.useEffect(()=>{o!==v.current&&(y(o),v.current=o)},[o]),{selectionMode:e,disallowEmptySelection:n,selectionBehavior:b,setSelectionBehavior:y,get isFocused(){return s.current},setFocused(w){s.current=w,l(w)},get focusedKey(){return u.current},get childFocusStrategy(){return c.current},setFocusedKey(w,x="first"){u.current=w,c.current=x,d(w)},selectedKeys:m,setSelectedKeys(w){(r||!km(w,m))&&h(w)},disabledKeys:g,disabledBehavior:i}}function Ui(t,e){return t?t==="all"?"all":new Re(t):e}function Mm(t){let{onExpandedChange:e}=t,[n,r]=rr(t.expandedKeys?new Set(t.expandedKeys):void 0,t.defaultExpandedKeys?new Set(t.defaultExpandedKeys):new Set,e),o=uu(t),i=a.useMemo(()=>t.disabledKeys?new Set(t.disabledKeys):new Set,[t.disabledKeys]),s=Tm(t,a.useCallback(u=>new Cm(u,{expandedKeys:n}),[n]),null);return a.useEffect(()=>{o.focusedKey!=null&&!s.getItem(o.focusedKey)&&o.setFocusedKey(null)},[s,o.focusedKey]),{collection:s,expandedKeys:n,disabledKeys:i,toggleKey:u=>{r(Am(n,u))},setExpandedKeys:r,selectionManager:new ur(s,o)}}function Am(t,e){let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n}const To=a.createContext(null),un=a.createContext(null),Qt=a.createContext(null),Zn=a.createContext(null);function Rm(t){let e=Po(t),n=a.useRef(null),{menuTriggerProps:r,menuProps:o}=rm({...t,type:"menu"},e,n),i=a.useRef(null);return bo()?null:$.createElement(ze,{values:[[To,{...o,ref:i}],[cn,e],[Qt,e],[lr,{trigger:"MenuTrigger",triggerRef:n,scrollRef:i,placement:"bottom start","aria-labelledby":o["aria-labelledby"],offset:t.trigger==="contextMenu"?0:void 0}]]},$.createElement(ia,{...r,ref:n,isPressed:e.isOpen},t.children))}const cu=a.createContext(null);class Lm extends It{static{this.type="submenutrigger"}filter(e,n,r){let o=e.getItem(this.firstChildKey);if(o&&r(o.textValue,this)){let i=this.clone();return n.addDescendants(i,e),i}return null}}const Im=ba(Lm,(t,e,n)=>{let{CollectionBranch:r}=a.useContext(ar),o=a.useContext(un),i=a.useContext(Qt),s=wm({triggerKey:n.key},i),l=a.useRef(null),u=rt(e),{parentMenuRef:c,shouldUseVirtualFocus:d}=a.useContext(cu),{submenuTriggerProps:f,submenuProps:p,popoverProps:m}=Em({parentMenuRef:c,submenuRef:l,delay:t.delay,shouldUseVirtualFocus:d},s,u);return $.createElement(ze,{values:[[Rt,{...f,onAction:void 0,ref:u}],[To,{ref:l,...p}],[cn,s],[lr,{trigger:"SubmenuTrigger",triggerRef:u,placement:"end top","aria-labelledby":p["aria-labelledby"],...m}]]},$.createElement(r,{collection:o.collection,parent:n}),t.children[1])},t=>t.children[0]),Fm=a.forwardRef(function(e,n){return[e,n]=me(e,n,To),$.createElement(Kh,{content:$.createElement(_h,e)},r=>$.createElement(Km,{props:e,collection:r,menuRef:n}))});function Km({props:t,collection:e,menuRef:n}){[t,n]=me(t,n,Di);let{filter:r,...o}=t,i=a.useMemo(()=>r?e.filter(r):e,[e,r]),s=Mm({...t,collection:i,children:void 0}),l=a.useContext(Qt),{isVirtualized:u,CollectionRoot:c}=a.useContext(ar),{menuProps:d}=fm({...t,isVirtualized:u,onClose:t.onClose||l?.close},s,n),f=ve({...t,children:void 0,defaultClassName:"react-aria-Menu",values:{isEmpty:s.collection.size===0}}),p=null;s.collection.size===0&&t.renderEmptyState&&(p=$.createElement("div",{role:"menuitem",style:{display:"contents"}},t.renderEmptyState()));let m=de(t,{global:!0});return $.createElement(xl,null,$.createElement(ue.div,{...Q(m,f,d),ref:n,slot:t.slot||void 0,"data-empty":s.collection.size===0||void 0,onScroll:t.onScroll},$.createElement(ze,{values:[[un,s],[Qh,{elementType:"div"}],[jh,{name:"MenuSection",render:du}],[cu,{parentMenuRef:n,shouldUseVirtualFocus:o?.shouldUseVirtualFocus}],[Rt,{shouldCloseOnSelect:t.shouldCloseOnSelect}],[Di,null],[$a,null],[Zn,s.selectionManager],[Qt,l??Po({})]]},$.createElement(Xh,null,$.createElement(c,{collection:s.collection,persistedKeys:Zh(s.selectionManager.focusedKey),scrollRef:n}))),p))}class Dm extends ur{constructor(e,n){super(e.collection,n),this.parent=e}get focusedKey(){return this.parent.focusedKey}get isFocused(){return this.parent.isFocused}setFocusedKey(e,n){return this.parent.setFocusedKey(e,n)}setFocused(e){this.parent.setFocused(e)}get childFocusStrategy(){return this.parent.childFocusStrategy}}function du(t,e,n,r="react-aria-MenuSection"){let o=a.useContext(un),{CollectionBranch:i}=a.useContext(ar),[s,l]=mo(),{headingProps:u,groupProps:c}=vm({heading:l,"aria-label":n.props["aria-label"]??void 0}),d=ve({...t,id:void 0,children:void 0,defaultClassName:r,className:n.props?.className,style:n.props?.style,values:void 0}),f=a.useContext(Zn),p=uu(t),m=t.selectionMode!=null?new Dm(f,p):f,h=er(Rt)?.shouldCloseOnSelect,g=de(t,{global:!0});return delete g.id,$.createElement(ue.section,{...Q(g,d,c),ref:e},$.createElement(ze,{values:[[qh,{...u,ref:s}],[Zn,m],[Rt,{shouldCloseOnSelect:t.shouldCloseOnSelect??h}]]},$.createElement(i,{collection:o.collection,parent:n})))}const Nm=ba(Mh,du),Rt=a.createContext(null),Om=ma(kh,function(e,n,r){[e,n]=me(e,n,Rt);let o=er(Rt)?.id,i=a.useContext(un),s=rt(n),l=a.useContext(Zn),{isVirtualized:u}=a.useContext(ar),{menuItemProps:c,labelProps:d,descriptionProps:f,keyboardShortcutProps:p,...m}=gm({...e,id:o,key:r.key,selectionManager:l,isVirtualized:u},i,s),{hoverProps:h,isHovered:g}=Ue({isDisabled:m.isDisabled}),b=ve({...e,id:void 0,children:r.rendered,defaultClassName:"react-aria-MenuItem",values:{...m,isHovered:g,isFocusVisible:m.isFocusVisible,selectionMode:l.selectionMode,selectionBehavior:l.selectionBehavior,hasSubmenu:!!e["aria-haspopup"],isOpen:e["aria-expanded"]==="true"}}),y=e.href?ue.a:ue.div,v=de(e,{global:!0});return delete v.id,delete v.onClick,$.createElement(y,{...Q(v,b,c,h),ref:s,"data-disabled":m.isDisabled||void 0,"data-hovered":g||void 0,"data-focused":m.isFocused||void 0,"data-focus-visible":m.isFocusVisible||void 0,"data-pressed":m.isPressed||void 0,"data-selected":m.isSelected||void 0,"data-selection-mode":l.selectionMode==="none"?void 0:l.selectionMode,"data-has-submenu":!!e["aria-haspopup"]||void 0,"data-open":e["aria-expanded"]==="true"||void 0},$.createElement(ze,{values:[[an,{slots:{[dt]:d,label:d,description:f}}],[Yh,p],[Ea,{isSelected:m.isSelected}]]},b.children))});ma(Th,function(e,n,r){let o=a.useContext(un),{isLoading:i,onLoadMore:s,scrollOffset:l,...u}=e,c=a.useRef(null),d=a.useMemo(()=>({onLoadMore:s,collection:o?.collection,sentinelRef:c,scrollOffset:l}),[s,l,o?.collection]);wd(d,c);let f=ve({...u,id:void 0,children:r.rendered,defaultClassName:"react-aria-MenuLoadingIndicator",values:void 0}),p={tabIndex:-1};return $.createElement($.Fragment,null,$.createElement("div",{style:{position:"relative",width:0,height:0},inert:Cd(!0)},$.createElement("div",{"data-testid":"loadMoreSentinel",ref:c,style:{position:"absolute",height:1,width:1}})),i&&f.children&&$.createElement($.Fragment,null,$.createElement(ue.div,{...Q(de(e,{global:!0}),p),...f,role:"menuitem",ref:n},f.children)))});function Vm(t,e){let{role:n="dialog"}=t,r=Ye();r=t["aria-label"]?void 0:r;let o=Ye();o=n==="alertdialog"&&!t["aria-describedby"]?o:void 0;let i=a.useRef(!1);a.useEffect(()=>{if(e.current&&!Xe(e.current)){et(e.current);let l=setTimeout(()=>{(re()===e.current||re()===document.body)&&(i.current=!0,e.current&&(e.current.blur(),et(e.current)),i.current=!1)},500);return()=>{clearTimeout(l)}}},[e]),Ch(),a.useRef(!1),a.useEffect(()=>{});let s=t["aria-describedby"]??o;return{dialogProps:{...de(t,{labelable:!0}),role:n,tabIndex:-1,"aria-labelledby":t["aria-labelledby"]??r,"aria-describedby":s,onBlur:l=>{i.current&&l.stopPropagation()}},titleProps:{id:r},contentProps:{id:o}}}const fu=a.createContext(null),cn=a.createContext(null);function lg(t){let e=Po(t),n=a.useRef(null),{triggerProps:r,overlayProps:o}=iu({type:"dialog"},e,n);return r.id=xe(),o["aria-labelledby"]=r.id,bo()?null:$.createElement(ze,{values:[[cn,e],[Qt,e],[fu,o],[lr,{trigger:"DialogTrigger",triggerRef:n,id:o.id,"aria-labelledby":o["aria-labelledby"]}]]},$.createElement(ia,{...r,ref:n,isPressed:e.isOpen},t.children))}const ag=a.forwardRef(function(e,n){let r=e["aria-labelledby"];[e,n]=me(e,n,fu);let{dialogProps:o,titleProps:i,contentProps:s}=Vm({...e,"aria-labelledby":r},n),l=a.useContext(cn);!o["aria-label"]&&!o["aria-labelledby"]&&e["aria-labelledby"]&&(o["aria-labelledby"]=e["aria-labelledby"]);let u=ve({defaultClassName:"react-aria-Dialog",className:e.className,style:e.style,children:e.children,values:{close:l?.close||(()=>{})}}),c=de(e,{global:!0});return $.createElement(ue.section,{...Q(c,u,o),render:e.render,ref:n,slot:e.slot||void 0},$.createElement(ze,{values:[[hl,{slots:{[dt]:{},title:{...i,level:2}}}],[an,{slots:{[dt]:{},description:s}}],[$o,{slots:{[dt]:{},close:{onPress:()=>l?.close()}}}]]},u.children))}),Zi=({children:t,className:e,slot:n,style:r,variant:o,...i})=>{const s=a.useMemo(()=>T0({variant:o}),[o]);return E.jsx(xo,{"aria-label":"Close",className:Se(e,s),"data-slot":"close-button",slot:n,style:r,type:"button",...i,children:l=>typeof t=="function"?t(l):t??E.jsx(Ap,{"data-slot":"close-button-icon"})})},Bm=Object.assign(Zi,{Root:Zi}),_m=a.createContext({}),Hm=a.createContext(null),pu={badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valueMissing:!1,valid:!0},hu={...pu,customError:!0,valid:!1},Vt={isInvalid:!1,validationDetails:pu,validationErrors:[]},jm=a.createContext({}),Wi="__reactAriaFormValidationState";function zm(t){if(t[Wi]){let{realtimeValidation:e,displayValidation:n,updateValidation:r,resetValidation:o,commitValidation:i}=t[Wi];return{realtimeValidation:e,displayValidation:n,updateValidation:r,resetValidation:o,commitValidation:i}}return Um(t)}function Um(t){let{isInvalid:e,validationState:n,name:r,value:o,builtinValidation:i,validate:s,validationBehavior:l="aria"}=t;n&&(e||=n==="invalid");let u=e!==void 0?{isInvalid:e,validationErrors:[],validationDetails:hu}:null,c=a.useMemo(()=>{if(!s||o==null)return null;let M=Zm(s,o);return Gi(M)},[s,o]);i?.validationDetails.valid&&(i=void 0);let d=a.useContext(jm),f=a.useMemo(()=>r?Array.isArray(r)?r.flatMap(M=>Jr(d[M])):Jr(d[r]):[],[d,r]),[p,m]=a.useState(d),[h,g]=a.useState(!1);d!==p&&(m(d),g(!1));let b=a.useMemo(()=>Gi(h?[]:f),[h,f]),y=a.useRef(Vt),[v,w]=a.useState(Vt),x=a.useRef(Vt),F=()=>{if(!K)return;P(!1);let M=c||i||y.current;Ar(M,x.current)||(x.current=M,w(M))},[K,P]=a.useState(!1);return a.useEffect(F),{realtimeValidation:u||b||c||i||Vt,displayValidation:l==="native"?u||b||v:u||b||c||i||v,updateValidation(M){l==="aria"&&!Ar(v,M)?w(M):y.current=M},resetValidation(){let M=Vt;Ar(M,x.current)||(x.current=M,w(M)),l==="native"&&P(!1),g(!0)},commitValidation(){l==="native"&&P(!0),g(!0)}}}function Jr(t){return t?Array.isArray(t)?t:[t]:[]}function Zm(t,e){if(typeof t=="function"){let n=t(e);if(n&&typeof n!="boolean")return Jr(n)}return[]}function Gi(t){return t.length?{isInvalid:!0,validationErrors:t,validationDetails:hu}:null}function Ar(t,e){return t===e?!0:!!t&&!!e&&t.isInvalid===e.isInvalid&&t.validationErrors.length===e.validationErrors.length&&t.validationErrors.every((n,r)=>n===e.validationErrors[r])&&Object.entries(t.validationDetails).every(([n,r])=>e.validationDetails[n]===r)}const Wm=a.createContext(null);function Gm(t){let{description:e,errorMessage:n,isInvalid:r,validationState:o}=t,{labelProps:i,fieldProps:s}=Xs(t),l=Ye([!!e,!!n,r,o]),u=Ye([!!e,!!n,r,o]);return s=Q(s,{"aria-describedby":[l,u,t["aria-describedby"]].filter(Boolean).join(" ")||void 0}),{labelProps:i,fieldProps:s,descriptionProps:{id:l},errorMessageProps:{id:u}}}function qm(t,e,n){let{validationBehavior:r,focus:o}=t;J(()=>{if(r==="native"&&n?.current&&"setCustomValidity"in n.current&&!n.current.disabled){let c=e.realtimeValidation.isInvalid?e.realtimeValidation.validationErrors.join(" ")||"Invalid value.":"";n.current.setCustomValidity(c),n.current.hasAttribute("title")||(n.current.title=""),e.realtimeValidation.isInvalid||e.updateValidation(Xm(n.current))}});let i=a.useRef(!1),s=Ce(()=>{i.current||e.resetValidation()}),l=Ce(c=>{e.displayValidation.isInvalid||e.commitValidation();let d=n?.current?.form;!c.defaultPrevented&&n&&d&&Jm(d)===n.current&&(o?o():n.current?.focus(),Ln("keyboard")),c.preventDefault()}),u=Ce(()=>{e.commitValidation()});a.useEffect(()=>{let c=n?.current;if(!c)return;let d=c.form,f=d?.reset;return d&&(d.reset=()=>{i.current=!window.event||window.event.type==="message"&&V(window.event)instanceof MessagePort,f?.call(d),i.current=!1}),c.addEventListener("invalid",l),c.addEventListener("change",u),d?.addEventListener("reset",s),()=>{c.removeEventListener("invalid",l),c.removeEventListener("change",u),d?.removeEventListener("reset",s),d&&(d.reset=f)}},[n,r])}function Ym(t){let e=t.validity;return{badInput:e.badInput,customError:e.customError,patternMismatch:e.patternMismatch,rangeOverflow:e.rangeOverflow,rangeUnderflow:e.rangeUnderflow,stepMismatch:e.stepMismatch,tooLong:e.tooLong,tooShort:e.tooShort,typeMismatch:e.typeMismatch,valueMissing:e.valueMissing,valid:e.valid}}function Xm(t){return{isInvalid:!t.validity.valid,validationDetails:Ym(t),validationErrors:t.validationMessage?[t.validationMessage]:[]}}function Jm(t){for(let e=0;eQm(t,"name",{value:e,configurable:!0});function eb(t,e){const n=a.createContext(e);n.displayName=t+"Context";const r=ke(i=>{const{children:s,...l}=i,u=a.useMemo(()=>l,Object.values(l));return E.jsx(n.Provider,{value:u,children:s})},"Provider");r.displayName=t+"Provider";function o(i,s={}){const{optional:l=!1}=s,u=a.useContext(n);if(u)return u;if(e!==void 0)return e;if(!l)throw new Error(`\`${i}\` must be used within \`${t}\``)}return ke(o,"useContext"),[r,o]}ke(eb,"createContext");function mu(t,e=[]){let n=[];function r(i,s){const l=a.createContext(s);l.displayName=i+"Context";const u=n.length;n=[...n,s];const c=ke(f=>{const{scope:p,children:m,...h}=f,g=p?.[t]?.[u]||l,b=a.useMemo(()=>h,Object.values(h));return E.jsx(g.Provider,{value:b,children:m})},"Provider");c.displayName=i+"Provider";function d(f,p,m={}){const{optional:h=!1}=m,g=p?.[t]?.[u]||l,b=a.useContext(g);if(b)return b;if(s!==void 0)return s;if(!h)throw new Error(`\`${f}\` must be used within \`${i}\``)}return ke(d,"useContext"),[c,d]}ke(r,"createContext");const o=ke(()=>{const i=n.map(s=>a.createContext(s));return ke(function(l){const u=l?.[t]||i;return a.useMemo(()=>({[`__scope${t}`]:{...l,[t]:u}}),[l,u])},"useScope")},"createScope");return o.scopeName=t,[r,bu(o,...e)]}ke(mu,"createContextScope");function bu(...t){const e=t[0];if(t.length===1)return e;const n=ke(()=>{const r=t.map(o=>({useScope:o(),scopeName:o.scopeName}));return ke(function(i){const s=r.reduce((l,{useScope:u,scopeName:c})=>{const f=u(i)[`__scope${c}`];return{...l,...f}},{});return a.useMemo(()=>({[`__scope${e.scopeName}`]:s}),[s])},"useComposedScopes")},"createScope");return n.scopeName=e.scopeName,n}ke(bu,"composeContextScopes");var tb=Object.defineProperty,nb=(t,e)=>tb(t,"name",{value:e,configurable:!0});function gu(t){const e=a.useRef(t);return a.useEffect(()=>{e.current=t}),a.useMemo(()=>((...n)=>e.current?.(...n)),[])}nb(gu,"useCallbackRef");var vu=globalThis?.document?a.useLayoutEffect:()=>{},rb=Object.defineProperty,ko=(t,e)=>rb(t,"name",{value:e,configurable:!0});function Qr(t,e){if(typeof t=="function")return t(e);t!=null&&(t.current=e)}ko(Qr,"setRef");function yu(...t){return e=>{let n=!1;const r=t.map(o=>{const i=Qr(o,e);return!n&&typeof i=="function"&&(n=!0),i});if(n)return()=>{for(let o=0;oob(t,"name",{value:e,configurable:!0});function xu(t){const e=a.forwardRef((n,r)=>{let{children:o,...i}=n,s=null,l=!1;const u=[];eo(o)&&typeof Tn=="function"&&(o=Tn(o._payload)),a.Children.forEach(o,p=>{if(Su(p)){l=!0;const m=p;let h="child"in m.props?m.props.child:m.props.children;eo(h)&&typeof Tn=="function"&&(h=Tn(h._payload)),s=sb(m,h),u.push(s?.props?.children)}else u.push(p)}),s?s=a.cloneElement(s,void 0,u):!l&&a.Children.count(o)===1&&a.isValidElement(o)&&(s=o);const c=s?Cu(s):void 0,d=$u(r,c);if(!s){if(o||o===0)throw new Error(l?ub(t):ab(t));return o}const f=wu(i,s.props??{});return s.type!==a.Fragment&&(f.ref=r?d:c),a.cloneElement(s,f)});return e.displayName=`${t}.Slot`,e}Ie(xu,"createSlot");var Eu=Symbol.for("radix.slottable");function ib(t){const e=Ie(n=>"child"in n?n.children(n.child):n.children,"Slottable");return e.displayName=`${t}.Slottable`,e.__radixId=Eu,e}Ie(ib,"createSlottable");var sb=Ie((t,e)=>{if("child"in t.props){const n=t.props.child;return a.isValidElement(n)?a.cloneElement(n,void 0,t.props.children(n.props.children)):null}return a.isValidElement(e)?e:null},"getSlottableElementFromSlottable");function wu(t,e){const n={...e};for(const r in e){const o=t[r],i=e[r];/^on[A-Z]/.test(r)?o&&i?n[r]=(...l)=>{const u=i(...l);return o(...l),u}:o&&(n[r]=o):r==="style"?n[r]={...o,...i}:r==="className"&&(n[r]=[o,i].filter(Boolean).join(" "))}return{...t,...n}}Ie(wu,"mergeProps");function Cu(t){let e=Object.getOwnPropertyDescriptor(t.props,"ref")?.get,n=e&&"isReactWarning"in e&&e.isReactWarning;return n?t.ref:(e=Object.getOwnPropertyDescriptor(t,"ref")?.get,n=e&&"isReactWarning"in e&&e.isReactWarning,n?t.props.ref:t.props.ref||t.ref)}Ie(Cu,"getElementRef");function Su(t){return a.isValidElement(t)&&typeof t.type=="function"&&"__radixId"in t.type&&t.type.__radixId===Eu}Ie(Su,"isSlottable");var lb=Symbol.for("react.lazy");function eo(t){return t!=null&&typeof t=="object"&&"$$typeof"in t&&t.$$typeof===lb&&"_payload"in t&&Pu(t._payload)}Ie(eo,"isLazyComponent");function Pu(t){return typeof t=="object"&&t!==null&&"then"in t}Ie(Pu,"isPromiseLike");var ab=Ie(t=>`${t} failed to slot onto its children. Expected a single React element child or \`Slottable\`.`,"createSlotError"),ub=Ie(t=>`${t} failed to slot onto its \`Slottable\`. Expected \`Slottable\` to receive a single React element child.`,"createSlottableError"),Tn=Lc[" use ".trim().toString()],cb=Object.defineProperty,db=(t,e)=>cb(t,"name",{value:e,configurable:!0}),fb=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],Mo=fb.reduce((t,e)=>{const n=xu(`Primitive.${e}`),r=a.forwardRef((o,i)=>{const{asChild:s,...l}=o,u=s?n:e;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),E.jsx(u,{...l,ref:i})});return r.displayName=`Primitive.${e}`,{...t,[e]:r}},{});function pb(t,e){t&&Qe.flushSync(()=>t.dispatchEvent(e))}db(pb,"dispatchDiscreteCustomEvent");var hb=Object.defineProperty,He=(t,e)=>hb(t,"name",{value:e,configurable:!0}),Tu="Avatar",[mb,ug]=mu(Tu),bb=[0,()=>{}],[gb,ku]=mb(Tu),vb=a.forwardRef(He(function(e,n){const{__scopeAvatar:r,...o}=e,[i,s]=a.useState("idle"),[l,u]=Au();return E.jsx(gb,{scope:r,imageLoadingStatus:i,setImageLoadingStatus:s,imageCount:l,setImageCount:u,children:E.jsx(Mo.span,{...o,ref:n})})},"Avatar")),yb="AvatarImage",$b=a.forwardRef(He(function(e,n){const{__scopeAvatar:r,src:o,onLoadingStatusChange:i,...s}=e,l=ku(yb,r);l.setImageCount;const u=Mu(o,{referrerPolicy:s.referrerPolicy,crossOrigin:s.crossOrigin,loadingStatus:l.imageLoadingStatus,setLoadingStatus:l.setImageLoadingStatus}),c=gu(f=>{i?.(f)}),d=a.useRef(u);return vu(()=>{const f=d.current;d.current=u,u!==f&&c(u)},[u,c]),u==="loaded"?E.jsx(Mo.img,{...s,ref:n,src:o}):null},"AvatarImage")),xb="AvatarFallback",Eb=a.forwardRef(He(function(e,n){const{__scopeAvatar:r,delayMs:o,...i}=e,s=ku(xb,r),[l,u]=a.useState(o===void 0);return a.useEffect(()=>{if(o!==void 0){const c=window.setTimeout(()=>u(!0),o);return()=>window.clearTimeout(c)}},[o]),l&&s.imageLoadingStatus!=="loaded"?E.jsx(Mo.span,{...i,ref:n}):null},"AvatarFallback"));function Mu(t,{loadingStatus:e,setLoadingStatus:n,referrerPolicy:r,crossOrigin:o}){return vu(()=>{if(!t){n("error");return}const i=new window.Image,s=He(u=>{const c=u.currentTarget;n(to(c))},"handleLoad"),l=He(()=>n("error"),"handleError");return i.addEventListener("load",s),i.addEventListener("error",l),r&&(i.referrerPolicy=r),i.crossOrigin=o??null,i.src=t,n(to(i)),()=>{i.removeEventListener("load",s),i.removeEventListener("error",l),n("idle")}},[t,o,r,n]),e}He(Mu,"useImageLoadingStatus");function to(t){return t.complete?t.naturalWidth>0?"loaded":"error":"loading"}He(to,"getImageLoadingStatus");function Au(){return bb}He(Au,"useImageCount");function wb(t){}He(wb,"useUpdateImageCount");const Ao=a.createContext({}),qi=({children:t,className:e,color:n,size:r,variant:o,[E0]:i,...s})=>{const l=a.use(x0),u=i===!0,c=r??(u?l?.size:void 0),d=n??(u?l?.color:void 0),f=o??(u?l?.variant:void 0),p=$.useMemo(()=>C0({color:d,size:c,variant:f}),[d,c,f]),m=$.useMemo(()=>({slots:p}),[p]);return E.jsx(Ao,{value:m,children:E.jsx(vb,{className:p.base({className:e}),...s,children:t})})},Cb=({className:t,crossOrigin:e,loading:n,onError:r,onLoad:o,sizes:i,src:s,srcSet:l,...u})=>{const{slots:c}=$.use(Ao);return E.jsx($b,{className:be(c?.image,t),crossOrigin:e,loading:n,sizes:i,src:s,srcSet:l,onError:r,onLoad:o,...u})},Sb=({className:t,color:e,...n})=>{const{slots:r}=$.use(Ao);return E.jsx(Eb,{className:be(r?.fallback,t,{color:e}),"data-slot":"avatar-fallback",...n})},cg=Object.assign(qi,{Root:qi,Image:Cb,Fallback:Sb}),Pb=a.createContext({}),Tb="__button_group_child";function kb(t,e){let{elementType:n="a",onPress:r,onPressStart:o,onPressEnd:i,onPressChange:s,onClick:l,isDisabled:u,...c}=t,d={};n!=="a"&&(d={role:"link",tabIndex:u?void 0:0});let{focusableProps:f}=nr(t,e),{pressProps:p,isPressed:m}=on({onPress:r,onPressStart:o,onPressEnd:i,onPressChange:s,onClick:l,isDisabled:u,ref:e}),h=de(c,{labelable:!0}),g=Q(f,p),b=tn(),y=ds(t);return{isPressed:m,linkProps:Q(h,y,{...g,...d,"aria-disabled":u||void 0,"aria-current":t["aria-current"],onClick:v=>{p.onClick?.(v),fs(v,b,t.href,t.routerOptions)}})}}const Mb=a.createContext(null),Ab=a.forwardRef(function(e,n){[e,n]=me(e,n,Mb);let r=e.href&&!e.isDisabled?"a":"span",{linkProps:o,isPressed:i}=kb({...e,elementType:r},n),s=ue[r],{hoverProps:l,isHovered:u}=Ue(e),{focusProps:c,isFocused:d,isFocusVisible:f}=Lt(),p=ve({...e,defaultClassName:"react-aria-Link",values:{isCurrent:!!e["aria-current"],isDisabled:e.isDisabled||!1,isPressed:i,isHovered:u,isFocused:d,isFocusVisible:f}}),m=de(e,{global:!0});return delete m.onClick,$.createElement(s,{ref:n,slot:e.slot||void 0,...Q(m,p,o,l,c),"data-focused":d||void 0,"data-hovered":u||void 0,"data-pressed":i||void 0,"data-focus-visible":f||void 0,"data-current":!!e["aria-current"]||void 0,"data-disabled":e.isDisabled||void 0},p.children)}),Ru=a.createContext({}),Yi=({children:t,className:e,...n})=>{const r=$.useMemo(()=>R0(),[]),o=$.useMemo(()=>({slots:r}),[r]);return E.jsx(Ru,{value:o,children:E.jsx(Ab,{"data-slot":"link",...n,className:Se(e,r?.base()),children:i=>E.jsx(E.Fragment,{children:typeof t=="function"?t(i):t})})})},Rb=({children:t,className:e,...n})=>{const{slots:r}=a.use(Ru);return E.jsx(ge.span,{className:be(r?.icon,e),"data-default-icon":Ge(!t),"data-slot":"link-icon",...n,children:t??E.jsx(Mp,{"data-slot":"link-default-icon"})})},Lb=Object.assign(Yi,{Root:Yi,Icon:Rb}),Xi=({children:t,className:e,fullWidth:n,isDisabled:r,isIconOnly:o,size:i,slot:s,style:l,variant:u,[Tb]:c,...d})=>{const f=a.use(Pb),p=c===!0,m=i??(p?f?.size:void 0),h=u??(p?f?.variant:void 0),g=r??(p?f?.isDisabled:void 0),b=n??(p?f?.fullWidth:void 0),y=S0({fullWidth:b,isIconOnly:o,size:m,variant:h});return E.jsx(xo,{className:Se(e,y),"data-slot":"button",isDisabled:g,slot:s,style:l,...d,children:v=>typeof t=="function"?t(v):t})},Ro=Object.assign(Xi,{Root:Xi}),Ft=a.createContext({}),Ji=({children:t,className:e,variant:n="default",...r})=>{const o=$.useMemo(()=>P0({variant:n}),[n]),i=$.useMemo(()=>({slots:o}),[o]),s=$.useMemo(()=>({variant:n}),[n]),l=E.jsx(ge.div,{className:o.base({className:e}),"data-slot":"card",...r,children:t});return E.jsx(Ft,{value:i,children:n==="transparent"?l:E.jsx(Eo,{value:s,children:l})})},Ib=({className:t,...e})=>{const{slots:n}=a.use(Ft);return E.jsx(ge.div,{className:be(n?.header,t),"data-slot":"card-header",...e})},Fb=({children:t,className:e,...n})=>{const{slots:r}=a.use(Ft);return E.jsx(ge.h3,{className:be(r?.title,e),"data-slot":"card-title",...n,children:t})},Kb=({children:t,className:e,...n})=>{const{slots:r}=a.use(Ft);return E.jsx(ge.p,{className:be(r?.description,e),"data-slot":"card-description",...n,children:t})},Db=({className:t,...e})=>{const{slots:n}=a.use(Ft);return E.jsx(ge.div,{className:be(n?.content,t),"data-slot":"card-content",...e})},Nb=({className:t,...e})=>{const{slots:n}=a.use(Ft);return E.jsx(ge.div,{className:be(n?.footer,t),"data-slot":"card-footer",...e})},Rr=Object.assign(Ji,{Root:Ji,Header:Ib,Title:Fb,Description:Kb,Content:Db,Footer:Nb}),Lu=a.createContext({});let Ob=t=>{let{onHoverStart:e,onHoverChange:n,onHoverEnd:r,...o}=t;return o};const Vb=rn(function(e,n){[e,n]=me(e,n,Lu);let{hoverProps:r,isHovered:o}=Ue({...e,isDisabled:e.disabled}),{isFocused:i,isFocusVisible:s,focusProps:l}=Lt({isTextInput:!0,autoFocus:e.autoFocus}),u=!!e["aria-invalid"]&&e["aria-invalid"]!=="false",c=ve({...e,values:{isHovered:o,isFocused:i,isFocusVisible:s,isDisabled:e.disabled||!1,isInvalid:u},defaultClassName:"react-aria-Input"});return $.createElement(ue.input,{...Q(Ob(e),l,r),...c,ref:n,"data-focused":i||void 0,"data-disabled":e.disabled||void 0,"data-hovered":o||void 0,"data-focus-visible":s||void 0,"data-invalid":u||void 0})});function Bb(t,e){let{inputElementType:n="input",isDisabled:r=!1,isRequired:o=!1,isReadOnly:i=!1,type:s="text",validationBehavior:l="aria"}=t,[u,c]=rr(t.value,t.defaultValue||"",t.onChange),{focusableProps:d}=nr(t,e),f=zm({...t,value:u}),{isInvalid:p,validationErrors:m,validationDetails:h}=f.displayValidation,{labelProps:g,fieldProps:b,descriptionProps:y,errorMessageProps:v}=Gm({...t,isInvalid:p,errorMessage:t.errorMessage||m}),w=de(t,{labelable:!0});const x={type:s,pattern:t.pattern};let[F]=a.useState(u);return Ed(e,t.defaultValue??F,c),qm(t,f,e),{labelProps:g,inputProps:Q(w,n==="input"?x:void 0,{disabled:r,readOnly:i,required:o&&l==="native","aria-required":o&&l==="aria"||void 0,"aria-invalid":p||void 0,"aria-errormessage":t["aria-errormessage"],"aria-activedescendant":t["aria-activedescendant"],"aria-autocomplete":t["aria-autocomplete"],"aria-haspopup":t["aria-haspopup"],"aria-controls":t["aria-controls"],value:u,onChange:K=>c(V(K).value),autoComplete:t.autoComplete,autoCapitalize:t.autoCapitalize,maxLength:t.maxLength,minLength:t.minLength,name:t.name,form:t.form,placeholder:t.placeholder,inputMode:t.inputMode,autoCorrect:t.autoCorrect,spellCheck:t.spellCheck,[parseInt($.version,10)>=17?"enterKeyHint":"enterkeyhint"]:t.enterKeyHint,onCopy:t.onCopy,onCut:t.onCut,onPaste:t.onPaste,onCompositionEnd:t.onCompositionEnd,onCompositionStart:t.onCompositionStart,onCompositionUpdate:t.onCompositionUpdate,onSelect:t.onSelect,onBeforeInput:t.onBeforeInput,onInput:t.onInput,...d,...b}),descriptionProps:y,errorMessageProps:v,isInvalid:p,validationErrors:m,validationDetails:h}}const Iu=a.createContext({});let _b=t=>{let{onHoverStart:e,onHoverChange:n,onHoverEnd:r,...o}=t;return o};const dg=a.forwardRef(function(e,n){[e,n]=me(e,n,Iu);let{hoverProps:r,isHovered:o}=Ue(e),{isFocused:i,isFocusVisible:s,focusProps:l}=Lt({isTextInput:!0,autoFocus:e.autoFocus}),u=!!e["aria-invalid"]&&e["aria-invalid"]!=="false",c=ve({...e,values:{isHovered:o,isFocused:i,isFocusVisible:s,isDisabled:e.disabled||!1,isInvalid:u},defaultClassName:"react-aria-TextArea"});return $.createElement(ue.textarea,{...Q(_b(e),l,r),...c,ref:n,"data-focused":i||void 0,"data-disabled":e.disabled||void 0,"data-hovered":o||void 0,"data-focus-visible":s||void 0,"data-invalid":u||void 0})}),Hb=a.createContext(null),jb=rn(function(e,n){[e,n]=me(e,n,Hb);let{validationBehavior:r}=er(Wm)||{},o=e.validationBehavior??r??"native",i=a.useRef(null);[e,i]=me(e,i,$a);let[s,l]=mo(!e["aria-label"]&&!e["aria-labelledby"]),[u,c]=a.useState("input"),{labelProps:d,inputProps:f,descriptionProps:p,errorMessageProps:m,...h}=Bb({...O0(e),inputElementType:u,label:l,validationBehavior:o},i),g=a.useCallback(v=>{i.current=v,v&&c(v instanceof HTMLTextAreaElement?"textarea":"input")},[i]),b=ve({...e,values:{isDisabled:e.isDisabled||!1,isInvalid:h.isInvalid,isReadOnly:e.isReadOnly||!1,isRequired:e.isRequired||!1},defaultClassName:"react-aria-TextField"}),y=de(e,{global:!0});return delete y.id,$.createElement(ue.div,{...y,...b,ref:n,slot:e.slot||void 0,"data-disabled":e.isDisabled||void 0,"data-invalid":h.isInvalid||void 0,"data-readonly":e.isReadOnly||void 0,"data-required":e.isRequired||void 0},$.createElement(ze,{values:[[go,{...d,ref:s}],[Lu,{...f,ref:g}],[Iu,{...f,ref:g}],[_m,{role:"presentation",isInvalid:h.isInvalid,isDisabled:e.isDisabled||!1}],[an,{slots:{description:p,errorMessage:m}}],[Hm,h]]},b.children))});var Fu={};Fu={close:"إغلاق",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} إشعار`,other:()=>`${e.number(t.count)} إشعارات`})}.`};var Ku={};Ku={close:"Затвори",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} известие`,other:()=>`${e.number(t.count)} известия`})}.`};var Du={};Du={close:"Zavřít",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} oznámení`,other:()=>`${e.number(t.count)} oznámení`})}.`};var Nu={};Nu={close:"Luk",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} besked`,other:()=>`${e.number(t.count)} beskeder`})}.`};var Ou={};Ou={close:"Schließen",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} Benachrichtigung`,other:()=>`${e.number(t.count)} Benachrichtigungen`})}.`};var Vu={};Vu={close:"Κλείσιμο",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} ειδοποίηση`,other:()=>`${e.number(t.count)} ειδοποιήσεις`})}.`};var Bu={};Bu={close:"Close",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} notification`,other:()=>`${e.number(t.count)} notifications`})}.`};var _u={};_u={close:"Cerrar",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} notificación`,other:()=>`${e.number(t.count)} notificaciones`})}.`};var Hu={};Hu={close:"Sule",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} teatis`,other:()=>`${e.number(t.count)} teatist`})}.`};var ju={};ju={close:"Sulje",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} ilmoitus`,other:()=>`${e.number(t.count)} ilmoitusta`})}.`};var zu={};zu={close:"Fermer",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} notification`,other:()=>`${e.number(t.count)} notifications`})}.`};var Uu={};Uu={close:"סגור",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} התראה`,other:()=>`${e.number(t.count)} התראות`})}.`};var Zu={};Zu={close:"Zatvori",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} obavijest`,other:()=>`${e.number(t.count)} obavijesti`})}.`};var Wu={};Wu={close:"Bezárás",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} értesítés`,other:()=>`${e.number(t.count)} értesítés`})}.`};var Gu={};Gu={close:"Chiudi",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} notifica`,other:()=>`${e.number(t.count)} notifiche`})}.`};var qu={};qu={close:"閉じる",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} 個の通知`,other:()=>`${e.number(t.count)} 個の通知`})}。`};var Yu={};Yu={close:"닫기",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)}개 알림`,other:()=>`${e.number(t.count)}개 알림`})}.`};var Xu={};Xu={close:"Uždaryti",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} pranešimas`,other:()=>`${e.number(t.count)} pranešimai`})}.`};var Ju={};Ju={close:"Aizvērt",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} paziņojums`,other:()=>`${e.number(t.count)} paziņojumi`})}.`};var Qu={};Qu={close:"Lukk",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} varsling`,other:()=>`${e.number(t.count)} varsler`})}.`};var ec={};ec={close:"Sluiten",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} melding`,other:()=>`${e.number(t.count)} meldingen`})}.`};var tc={};tc={close:"Zamknij",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} powiadomienie`,few:()=>`${e.number(t.count)} powiadomienia`,many:()=>`${e.number(t.count)} powiadomień`,other:()=>`${e.number(t.count)} powiadomienia`})}.`};var nc={};nc={close:"Fechar",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} notificação`,other:()=>`${e.number(t.count)} notificações`})}.`};var rc={};rc={close:"Fechar",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} notificação`,other:()=>`${e.number(t.count)} notificações`})}.`};var oc={};oc={close:"Închideţi",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} notificare`,other:()=>`${e.number(t.count)} notificări`})}.`};var ic={};ic={close:"Закрыть",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} уведомление`,other:()=>`${e.number(t.count)} уведомления`})}.`};var sc={};sc={close:"Zatvoriť",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} oznámenie`,few:()=>`${e.number(t.count)} oznámenia`,other:()=>`${e.number(t.count)} oznámení`})}.`};var lc={};lc={close:"Zapri",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} obvestilo`,two:()=>`${e.number(t.count)} obvestili`,few:()=>`${e.number(t.count)} obvestila`,other:()=>`${e.number(t.count)} obvestil`})}.`};var ac={};ac={close:"Zatvori",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} obaveštenje`,other:()=>`${e.number(t.count)} obaveštenja`})}.`};var uc={};uc={close:"Stäng",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} meddelande`,other:()=>`${e.number(t.count)} meddelanden`})}.`};var cc={};cc={close:"Kapat",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} bildirim`,other:()=>`${e.number(t.count)} bildirim`})}.`};var dc={};dc={close:"Закрити",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} сповіщення`,other:()=>`${e.number(t.count)} сповіщення`})}.`};var fc={};fc={close:"关闭",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} 个通知`,other:()=>`${e.number(t.count)} 个通知`})}。`};var pc={};pc={close:"關閉",notifications:(t,e)=>`${e.plural(t.count,{one:()=>`${e.number(t.count)} 個通知`,other:()=>`${e.number(t.count)} 個通知`})}。`};var Lo={};Lo={"ar-AE":Fu,"bg-BG":Ku,"cs-CZ":Du,"da-DK":Nu,"de-DE":Ou,"el-GR":Vu,"en-US":Bu,"es-ES":_u,"et-EE":Hu,"fi-FI":ju,"fr-FR":zu,"he-IL":Uu,"hr-HR":Zu,"hu-HU":Wu,"it-IT":Gu,"ja-JP":qu,"ko-KR":Yu,"lt-LT":Xu,"lv-LV":Ju,"nb-NO":Qu,"nl-NL":ec,"pl-PL":tc,"pt-BR":nc,"pt-PT":rc,"ro-RO":oc,"ru-RU":ic,"sk-SK":sc,"sl-SI":lc,"sr-SP":ac,"sv-SE":uc,"tr-TR":cc,"uk-UA":dc,"zh-CN":fc,"zh-TW":pc};function zb(t){return t&&t.__esModule?t.default:t}function Ub(t,e,n){let{key:r,timer:o,timeout:i}=t.toast;a.useEffect(()=>{if(!(o==null||i==null))return o.reset(i),()=>{o.pause()}},[o,i]);let s=xe(),l=Ye(),u=sr(zb(Lo),"@react-aria/toast"),[c,d]=a.useState(!1);return J(()=>{d(!0)},[]),{toastProps:{...de(t,{labelable:!0}),role:"alertdialog","aria-modal":"false","aria-labelledby":t["aria-labelledby"]||s,"aria-describedby":t["aria-describedby"]||l,tabIndex:0},contentProps:{role:"alert","aria-atomic":"true","aria-hidden":c?void 0:"true"},titleProps:{id:s},descriptionProps:{id:l},closeButtonProps:{"aria-label":u.format("close"),onPress:()=>e.close(r)}}}const hc=1,Lr=Symbol.for("react-aria-landmark-manager");function Zb(t){return document.addEventListener("react-aria-landmark-manager-change",t),()=>document.removeEventListener("react-aria-landmark-manager-change",t)}function Qi(){if(typeof document>"u")return null;let t=document[Lr];return t&&t.version>=hc?t:(document[Lr]=new Gb,document.dispatchEvent(new CustomEvent("react-aria-landmark-manager-change")),document[Lr])}function Wb(){return Co.useSyncExternalStore(Zb,Qi,Qi)}class Gb{constructor(){this.landmarks=[],this.isListening=!1,this.refCount=0,this.version=hc,this.f6Handler=this.f6Handler.bind(this),this.focusinHandler=this.focusinHandler.bind(this),this.focusoutHandler=this.focusoutHandler.bind(this)}setupIfNeeded(){this.isListening||(document.addEventListener("keydown",this.f6Handler,{capture:!0}),document.addEventListener("focusin",this.focusinHandler,{capture:!0}),document.addEventListener("focusout",this.focusoutHandler,{capture:!0}),this.isListening=!0)}teardownIfNeeded(){!this.isListening||this.landmarks.length>0||this.refCount>0||(document.removeEventListener("keydown",this.f6Handler,{capture:!0}),document.removeEventListener("focusin",this.focusinHandler,{capture:!0}),document.removeEventListener("focusout",this.focusoutHandler,{capture:!0}),this.isListening=!1)}focusLandmark(e,n){this.landmarks.find(r=>r.ref.current===e)?.focus?.(n)}getLandmarksByRole(e){return new Set(this.landmarks.filter(n=>n.role===e))}getLandmarkByRole(e){return this.landmarks.find(n=>n.role===e)}addLandmark(e){if(this.setupIfNeeded(),this.landmarks.find(o=>o.ref===e.ref)||!e.ref.current)return;if(this.landmarks.filter(o=>o.role==="main").length>1,this.landmarks.length===0){this.landmarks=[e],this.checkLabels(e.role);return}let n=0,r=this.landmarks.length-1;for(;n<=r;){let o=Math.floor((n+r)/2),i=e.ref.current.compareDocumentPosition(this.landmarks[o].ref.current);!!(i&Node.DOCUMENT_POSITION_PRECEDING||i&Node.DOCUMENT_POSITION_CONTAINS)?n=o+1:r=o-1}this.landmarks.splice(n,0,e),this.checkLabels(e.role)}updateLandmark(e){let n=this.landmarks.findIndex(r=>r.ref===e.ref);n>=0&&(this.landmarks[n]={...this.landmarks[n],...e},this.checkLabels(this.landmarks[n].role))}removeLandmark(e){this.landmarks=this.landmarks.filter(n=>n.ref!==e),this.teardownIfNeeded()}checkLabels(e){let n=this.getLandmarksByRole(e);n.size>1&&[...n].filter(r=>!r.label)}closestLandmark(e){let n=new Map(this.landmarks.map(o=>[o.ref.current,o])),r=e;for(;r&&!n.has(r)&&r!==document.body&&r.parentElement;)r=r.parentElement;return n.get(r)}getNextLandmark(e,{backward:n}){let r=this.closestLandmark(e),o=n?this.landmarks.length-1:0;r&&(o=this.landmarks.indexOf(r)+(n?-1:1));let i=()=>{if(o<0){if(!e.dispatchEvent(new CustomEvent("react-aria-landmark-navigation",{detail:{direction:"backward"},bubbles:!0,cancelable:!0})))return!0;o=this.landmarks.length-1}else if(o>=this.landmarks.length){if(!e.dispatchEvent(new CustomEvent("react-aria-landmark-navigation",{detail:{direction:"forward"},bubbles:!0,cancelable:!0})))return!0;o=0}return o<0||o>=this.landmarks.length};if(i())return;let s=o;for(;this.landmarks[o].ref.current?.closest("[aria-hidden=true]");){if(o+=n?-1:1,i())return;if(o===s)break}return this.landmarks[o]}f6Handler(e){e.key==="F6"&&(e.altKey?this.focusMain():this.navigate(V(e),e.shiftKey))&&(e.preventDefault(),e.stopPropagation())}focusMain(){let e=this.getLandmarkByRole("main");return e&&e.ref.current&&e.ref.current.isConnected?(this.focusLandmark(e.ref.current,"forward"),!0):!1}navigate(e,n){let r=this.getNextLandmark(e,{backward:n});if(!r)return!1;if(r.lastFocused){let o=r.lastFocused;if(Y(document.body,o))return o.focus(),!0}return r.ref.current&&r.ref.current.isConnected?(this.focusLandmark(r.ref.current,n?"backward":"forward"),!0):!1}focusinHandler(e){let n=this.closestLandmark(V(e));n&&n.ref.current!==V(e)&&this.updateLandmark({ref:n.ref,lastFocused:V(e)});let r=e.relatedTarget;if(r){let o=this.closestLandmark(r);o&&o.ref.current===r&&o.blur()}}focusoutHandler(e){let n=V(e),r=e.relatedTarget;if(!r||r===document){let o=this.closestLandmark(n);o&&o.ref.current===n&&o.blur()}}createLandmarkController(){let e=this;return e.refCount++,e.setupIfNeeded(),{navigate(n,r){let o=r?.from||document.activeElement;return e.navigate(o,n==="backward")},focusNext(n){let r=n?.from||document.activeElement;return e.navigate(r,!1)},focusPrevious(n){let r=n?.from||document.activeElement;return e.navigate(r,!0)},focusMain(){return e.focusMain()},dispose(){e&&(e.refCount--,e.teardownIfNeeded(),e=null)}}}registerLandmark(e){return this.landmarks.find(n=>n.ref===e.ref)?this.updateLandmark(e):this.addLandmark(e),()=>this.removeLandmark(e.ref)}}function qb(t,e){const{role:n,"aria-label":r,"aria-labelledby":o,focus:i}=t;let s=Wb(),l=r||o,[u,c]=a.useState(!1),d=a.useCallback(()=>{c(!0)},[c]),f=a.useCallback(()=>{c(!1)},[c]);return J(()=>{if(s)return s.registerLandmark({ref:e,label:l,role:n,focus:i||d,blur:f})},[s,l,e,n,i,d,f]),a.useEffect(()=>{u&&e.current?.focus()},[u,e]),{landmarkProps:{role:n,tabIndex:u?-1:void 0,"aria-label":r,"aria-labelledby":o}}}function Yb(t){return t&&t.__esModule?t.default:t}function Xb(t,e,n){let r=sr(Yb(Lo),"@react-aria/toast"),{landmarkProps:o}=qb({role:"region","aria-label":t["aria-label"]||r.format("notifications",{count:e.visibleToasts.length})},n),i=a.useRef(!1),s=a.useRef(!1),l=a.useCallback(()=>{i.current||s.current?e.pauseAll():e.resumeAll()},[e]),{hoverProps:u}=Ue({onHoverStart:()=>{i.current=!0,l()},onHoverEnd:()=>{i.current=!1,l()}}),c=a.useRef([]),d=a.useRef(e.visibleToasts),f=a.useRef(null);J(()=>{if(f.current===-1||e.visibleToasts.length===0||!n.current){c.current=[],d.current=e.visibleToasts;return}if(c.current=[...n.current.querySelectorAll('[role="alertdialog"]')],d.current.length===e.visibleToasts.length&&e.visibleToasts.every((b,y)=>b.key===d.current[y].key)){d.current=e.visibleToasts;return}let h=d.current.map((b,y)=>({...b,i:y,isRemoved:!e.visibleToasts.some(v=>b.key===v.key)})),g=h.findIndex(b=>b.i===f.current&&b.isRemoved);if(g>-1)if(Je()==="pointer"&&p.current?.isConnected)ce(p.current);else{let b=0,y,v;for(;b<=g;)h[b].isRemoved||(v=Math.max(0,b-1)),b++;for(;b=0&&v=0&&y{s.current=!0,p.current=h.relatedTarget,l()},onBlurWithin:()=>{s.current=!1,p.current=null,l()}});return a.useEffect(()=>{e.visibleToasts.length===0&&p.current?.isConnected&&(Je()==="pointer"?ce(p.current):p.current.focus(),p.current=null)},[n,e.visibleToasts.length]),a.useEffect(()=>()=>{p.current?.isConnected&&(Je()==="pointer"?ce(p.current):p.current.focus(),p.current=null)},[n]),{regionProps:Q(o,u,m,{tabIndex:-1,"data-react-aria-top-layer":!0,onFocus:h=>{let g=V(h).closest('[role="alertdialog"]');f.current=c.current.findIndex(b=>b===g)},onBlur:()=>{f.current=-1}})}}function Jb(t){let e=a.useCallback(o=>t.subscribe(o),[t]),n=a.useCallback(()=>t.visibleToasts,[t]);return{visibleToasts:Co.useSyncExternalStore(e,n,n),add:(o,i)=>t.add(o,i),close:o=>t.close(o),pauseAll:()=>t.pauseAll(),resumeAll:()=>t.resumeAll()}}class Qb{constructor(e){this.queue=[],this.subscriptions=new Set,this.visibleToasts=[],this.maxVisibleToasts=e?.maxVisibleToasts??1/0,this.wrapUpdate=e?.wrapUpdate}runWithWrapUpdate(e,n){this.wrapUpdate?this.wrapUpdate(e,n):e()}subscribe(e){return this.subscriptions.add(e),()=>this.subscriptions.delete(e)}add(e,n={}){let r="_"+Math.random().toString(36).slice(2),o={...n,content:e,key:r,timer:n.timeout?new e1(()=>this.close(r),n.timeout):void 0};return this.queue.unshift(o),this.updateVisibleToasts("add"),r}close(e){let n=this.queue.findIndex(r=>r.key===e);n>=0&&(this.queue[n].onClose?.(),this.queue.splice(n,1)),this.updateVisibleToasts("remove")}updateVisibleToasts(e){this.visibleToasts=this.queue.slice(0,this.maxVisibleToasts),this.runWithWrapUpdate(()=>{for(let n of this.subscriptions)n()},e)}pauseAll(){for(let e of this.visibleToasts)e.timer&&e.timer.pause()}resumeAll(){for(let e of this.visibleToasts)e.timer&&e.timer.resume()}clear(){this.queue=[],this.updateVisibleToasts("clear")}}class e1{constructor(e,n){this.startTime=null,this.remaining=n,this.callback=e}reset(e){this.remaining=e,this.resume()}pause(){this.timerId!=null&&(clearTimeout(this.timerId),this.timerId=null,this.remaining-=Date.now()-this.startTime)}resume(){this.remaining<=0||(this.startTime=Date.now(),this.timerId=setTimeout(()=>{this.timerId=null,this.remaining=0,this.callback()},this.remaining))}}const cr=a.createContext(null),t1=a.forwardRef(function(e,n){let r=nt(),o=Jb(e.queue),i=rt(n),{regionProps:s}=Xb(e,o,i),{focusProps:l,isFocused:u,isFocusVisible:c}=Lt(),{hoverProps:d,isHovered:f}=Ue({}),p=ve({...e,children:void 0,defaultClassName:"react-aria-ToastRegion",values:{visibleToasts:o.visibleToasts,isHovered:f,isFocused:u,isFocusVisible:c}}),{direction:m}=it(),h,{getContainer:g}=sa();r||(h=document.body,g&&(h=g()));let b=de(e,{global:!0}),y=$.createElement(cr.Provider,{value:o},$.createElement(ue.div,{...Q(b,p,s,l,d),dir:m,ref:i,"data-hovered":f||void 0,"data-focused":u||void 0,"data-focus-visible":c||void 0},typeof e.children=="function"?$.createElement(n1,{...e,render:void 0,className:void 0,style:{display:"contents"}},e.children):e.children));return o.visibleToasts.length>0&&h?Qe.createPortal(y,h):null}),n1=a.forwardRef(function(e,n){let r=a.useContext(cr),{hoverProps:o,isHovered:i}=Ue({}),s=ve({...e,children:void 0,defaultClassName:"react-aria-ToastList",values:{visibleToasts:r.visibleToasts,isFocused:!1,isFocusVisible:!1,isHovered:i}});return $.createElement(ue.ol,{...o,...s,ref:n},r.visibleToasts.map(l=>$.createElement("li",{key:l.key,style:{display:"contents"}},e.children({toast:l}))))}),r1=a.forwardRef(function(e,n){let r=a.useContext(cr),o=rt(n),{toastProps:i,contentProps:s,titleProps:l,descriptionProps:u,closeButtonProps:c}=Ub(e,r,o),{focusProps:d,isFocused:f,isFocusVisible:p}=Lt(),m=ve({...e,defaultClassName:"react-aria-Toast",values:{toast:e.toast,isFocused:f,isFocusVisible:p}}),h=de(e,{global:!0});return $.createElement(ue.div,{...Q(h,m,i,d),ref:o,"data-focused":f||void 0,"data-focus-visible":p||void 0},$.createElement(ze,{values:[[mc,s],[an,{slots:{[dt]:{},title:l,description:u}}],[$o,{slots:{[dt]:{},close:c}}]]},m.children))}),mc=a.createContext({}),o1=a.forwardRef(function(e,n){return[e,n]=me(e,n,mc),$.createElement(ue.div,{render:e.render,className:"react-aria-ToastContent",...e,ref:n},e.children)}),wt={blue:"\x1B[34m",green:"\x1B[32m",magenta:"\x1B[35m",red:"\x1B[31m",reset:"\x1B[0m",yellow:"\x1B[33m"},i1={debug:wt.magenta,error:wt.red,info:wt.blue,success:wt.green,warn:wt.yellow},s1={debug:"🔍",error:"❌",info:"ℹ️",success:"✅",warn:"⚠️"};class l1{constructor(e={}){this.enabled=e.enabled??!0,this.prefix=e.prefix??"HeroUI"}formatMessage(e,n){const r=i1[e],o=s1[e];return`${r}[${this.prefix}]${wt.reset} ${o} ${n}`}log(e,n,...r){if(!this.enabled)return;const o=this.formatMessage(e,n);switch(e){case"error":console.error(o,...r);break;case"warn":console.warn(o,...r);break;default:console.log(o,...r)}}info(e,...n){this.log("info",e,...n)}success(e,...n){this.log("success",e,...n)}warn(e,...n){this.log("warn",e,...n)}error(e,...n){this.log("error",e,...n)}debug(e,...n){this.log("debug",e,...n)}divider(e="=",n=80){this.enabled&&console.log(e.repeat(n))}newline(){this.enabled&&console.log()}}new l1({prefix:"HeroUI"});const a1=t=>{const e=`$$heroui.collection.${t}`,n=a.createContext(void 0),r=(u,c)=>a.Children.map(u,d=>a.isValidElement(d)?$.cloneElement(d,{[e]:c}):d),o=u=>{const{[e]:c,...d}=u;return[c,d]},i=u=>{const[c,d]=o(u),f=a.useContext(n);return[c??f,d]},s=({children:u,...c})=>E.jsx(n.Provider,{value:c,children:r(u,c)});return s.displayName=`HeroUI.CollectionSlot(${t})`,{key:e,inject:r,consume:o,useSlot:i,Injector:s,withSlot:(u,c)=>{const d=a.forwardRef(function(p,m){const[h,g]=i(p),{children:b,...y}=g;if(!h)return E.jsx(u,{...y,ref:m,children:b});if(c)return c({...y,children:b},h,m);const{className:v,render:w,...x}=h;return E.jsx(u,{...y,ref:m,children:w?w({children:b,className:v,...x}):E.jsx("div",{className:v,"data-slot":`${t}-container`,...x,children:b})})});return d.displayName=`HeroUI.withCollectionSlot(${u.displayName||u.name||t})`,d},Context:n}},u1=a.createContext({});a1("combo-box.inputGroup");const bc=12,gc=3,c1=Number.MAX_SAFE_INTEGER,vc=.05,d1=460,Fn=4e3,yc=300,f1=["altKey","KeyT"],Io=a.createContext({}),p1=({children:t,className:e,variant:n,...r})=>{const o=$.useMemo(()=>L0({variant:n}),[n]);return E.jsx(Om,{className:Se(e,o.item()),"data-slot":"menu-item",...r,children:i=>E.jsx(Io,{value:{slots:o,state:i},children:typeof t=="function"?t(i):t})})},h1=({children:t,className:e,type:n="checkmark",...r})=>{const{slots:o,state:i}=a.use(Io),s=i?.isSelected,l=typeof t=="function"?t(i??{}):t||(n==="dot"?E.jsx("svg",{"aria-hidden":"true","data-slot":"menu-item-indicator--dot",fill:"currentColor",fillRule:"evenodd",role:"presentation",viewBox:"0 0 16 16",xmlns:"http://www.w3.org/2000/svg",children:E.jsx("path",{clipRule:"evenodd",d:"M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14",fillRule:"evenodd"})}):E.jsx("svg",{"aria-hidden":"true","data-slot":"menu-item-indicator--checkmark",fill:"none",role:"presentation",stroke:"currentColor",strokeDasharray:22,strokeDashoffset:s?44:66,strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,viewBox:"0 0 17 18",children:E.jsx("polyline",{points:"1 9 7 14 15 4"})}));return E.jsx(ge.span,{"aria-hidden":"true",className:be(o?.indicator,e),"data-slot":"menu-item-indicator","data-type":n,"data-visible":s||void 0,...r,children:l})},m1=({children:t,className:e,...n})=>{const{slots:r,state:o}=a.use(Io);if(!o?.hasSubmenu)return null;const l=t??E.jsx(kp,{});return E.jsx(ge.span,{"aria-hidden":"true",className:be(r?.submenuIndicator,e),"data-slot":"submenu-indicator",...n,children:l})},b1=({children:t,className:e,...n})=>{const r=$.useMemo(()=>I0({class:typeof e=="string"?e:void 0}),[e]);return E.jsx(Nm,{className:r,...n,children:t})},dr=a.createContext({}),es=({children:t,...e})=>{const n=$.useMemo(()=>k0(),[]),r=$.useMemo(()=>({slots:n}),[n]);return E.jsx(dr,{value:r,children:E.jsx(Rm,{...e,children:t})})},g1=({children:t,className:e,...n})=>{const{slots:r}=a.use(dr);return E.jsx(xo,{className:Se(e,r?.trigger()),"data-slot":"dropdown-trigger",...n,children:o=>E.jsx(E.Fragment,{children:typeof t=="function"?t(o):t})})},v1=({children:t,className:e,placement:n,...r})=>{const{slots:o}=a.use(dr);return E.jsx(Eo,{value:{variant:"default"},children:E.jsx(Sh,{...r,className:Se(e,o?.popover()),"data-slot":"dropdown-popover",placement:n,children:t})})};function y1({className:t,...e}){const{slots:n}=a.use(dr);return E.jsx(Fm,{className:Se(t,n?.menu()),"data-selection-mode":e.selectionMode,"data-slot":"dropdown-menu",...e})}const $1=t=>E.jsx(p1,{...t}),x1=t=>E.jsx(m1,{...t}),E1=({children:t,...e})=>E.jsx(Im,{"data-slot":"dropdown-submenu-trigger",...e,children:t}),w1=t=>E.jsx(h1,{...t}),C1=t=>E.jsx(b1,{...t}),fg=Object.assign(es,{Root:es,Trigger:g1,Popover:v1,Menu:y1,Section:C1,Item:$1,ItemIndicator:w1,SubmenuIndicator:x1,SubmenuTrigger:E1}),$c=a.createContext({}),ts=({children:t,className:e,fullWidth:n,variant:r,...o})=>{const i=$.useMemo(()=>D0({fullWidth:n}),[n]),s=$.useMemo(()=>({variant:r}),[r]);return E.jsx(jb,{"data-slot":"textfield",...o,className:Se(e,i),children:l=>E.jsx($c,{value:s,children:E.jsx(E.Fragment,{children:typeof t=="function"?t(l):t})})})},ns=({className:t,fullWidth:e,variant:n,...r})=>{const o=a.use($c),i=a.use(u1),s=n??o.variant??i.variant;return E.jsx(Vb,{className:Se(t,M0({fullWidth:e,variant:s})),"data-slot":"input",...r})},pg=Object.assign(ns,{Root:ns}),rs=({children:t,className:e,isDisabled:n,isInvalid:r,isRequired:o,...i})=>E.jsx(_0,{className:A0({isRequired:o,isDisabled:n,isInvalid:r,className:e}),"data-slot":"label",...i,children:t}),hg=Object.assign(rs,{Root:rs}),xc=globalThis?.document?a.useLayoutEffect:a.useEffect,Ir=new Map;function S1(t,e,n=!0){const r=nt();return a.useMemo(()=>{if(e!==void 0)return e;if(!r){if(n&&Ir.has(t))return Ir.get(t);try{const o=document.documentElement,i=getComputedStyle(o).getPropertyValue(t).trim()||void 0;return n&&Ir.set(t,i),i}catch{return}}},[t,e,r,n])}const os=({animationType:t,className:e,...n})=>{const r=S1("--skeleton-animation",t),o=$.useMemo(()=>F0({animationType:r}),[r]);return E.jsx(ge.div,{className:o.base({className:e}),...n})},mg=Object.assign(os,{Root:os}),P1=({...t})=>{const e=a.useId();return E.jsxs("svg",{"data-slot":"spinner-icon",viewBox:"0 0 24 24",...t,children:[E.jsxs("defs",{children:[E.jsxs("linearGradient",{id:`«data-slot-icon-def-1»-${e}`,x1:"50%",x2:"50%",y1:"5.271%",y2:"91.793%",children:[E.jsx("stop",{offset:"0%",stopColor:"currentColor"}),E.jsx("stop",{offset:"100%",stopColor:"currentColor",stopOpacity:.55})]}),E.jsxs("linearGradient",{id:`«data-slot-icon-def-2»-${e}`,x1:"50%",x2:"50%",y1:"15.24%",y2:"87.15%",children:[E.jsx("stop",{offset:"0%",stopColor:"currentColor",stopOpacity:0}),E.jsx("stop",{offset:"100%",stopColor:"currentColor",stopOpacity:.55})]})]}),E.jsxs("g",{fill:"none",children:[E.jsx("path",{d:"m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"}),E.jsx("path",{d:"M8.749.021a1.5 1.5 0 0 1 .497 2.958A7.5 7.5 0 0 0 3 10.375a7.5 7.5 0 0 0 7.5 7.5v3c-5.799 0-10.5-4.7-10.5-10.5C0 5.23 3.726.865 8.749.021",fill:`url(#«data-slot-icon-def-1»-${e})`,transform:"translate(1.5 1.625)"}),E.jsx("path",{d:"M15.392 2.673a1.5 1.5 0 0 1 2.119-.115A10.48 10.48 0 0 1 21 10.375c0 5.8-4.701 10.5-10.5 10.5v-3a7.5 7.5 0 0 0 5.007-13.084a1.5 1.5 0 0 1-.115-2.118",fill:`url(#«data-slot-icon-def-2»-${e})`,transform:"translate(1.5 1.625)"})]})]})},is=({className:t,color:e,size:n,...r})=>E.jsx(ge.span,{"aria-label":"Loading","data-slot":"spinner",role:"status",...r,className:K0({className:t,color:e,size:n}),children:E.jsx(P1,{"aria-hidden":!0})}),T1=Object.assign(is,{Root:is}),bg=Object.assign(ts,{Root:ts}),k1=t=>{const[e,n]=a.useState(void 0),r=a.useCallback(()=>{const o=t.current;if(!o)return;const i=o.style.height;o.style.height="auto";const s=o.scrollHeight;o.style.height=i,n(l=>l!==s?s:l)},[t]);return xc(()=>{r()},[r]),a.useEffect(()=>{const o=t.current;if(!o)return;let i=0;const s=()=>{i||(i=requestAnimationFrame(()=>{i=0,r()}))},l=new MutationObserver(s);l.observe(o,{attributeFilter:["class"],attributes:!0,characterData:!0,childList:!0,subtree:!0});let u=o.getBoundingClientRect().width;const c=new ResizeObserver(d=>{const f=d[0]?.contentRect.width;f===void 0||Math.abs(f-u)<.5||(u=f,s())});return c.observe(o),()=>{l.disconnect(),c.disconnect(),cancelAnimationFrame(i)}},[t,r]),{height:e}},M1=typeof window<"u"?a.useLayoutEffect:a.useEffect,A1=typeof window>"u";function R1(t,{defaultValue:e=!1,initializeWithValue:n=!0}={}){const r=l=>A1?e:window.matchMedia(l).matches,[o,i]=a.useState(()=>n?r(t):e);function s(){i(r(t))}return M1(()=>{const l=window.matchMedia(t);return s(),l.addListener?l.addListener(s):l.addEventListener("change",s),()=>{l.removeListener?l.removeListener(s):l.removeEventListener("change",s)}},[t]),o}const Fr=typeof performance<"u"&&typeof performance.now=="function"?()=>performance.now():()=>Date.now(),ss=new WeakSet;class L1{startTime=null;constructor(e,n){this.remaining=n,this.callback=e}reset(e){this.remaining=e,this.resume()}pause(){this.timerId!=null&&(clearTimeout(this.timerId),this.timerId=null,this.remaining-=Fr()-(this.startTime??Fr()))}resume(){this.remaining<=0||(this.startTime=Fr(),this.timerId=setTimeout(()=>{this.timerId=null,this.remaining=0,this.callback()},this.remaining))}}class en extends Qb{static didWarnMissingNotify=!1;exitTimers=new Map;exitingSnapshot=new Set;exitSubscriptions=new Set;suspensions=new Set;isBatchingExits=!1;constructor(e,n=yc){super(e),this.exitDuration=n}suspendTimers(e){const n=this.suspensions.size===0;this.suspensions.add(e),n&&super.pauseAll()}resumeTimers(e){this.suspensions.delete(e),this.suspensions.size===0&&super.resumeAll()}pauseAll(){this.suspendTimers("external")}resumeAll(){this.resumeTimers("external")}guardTimer(e){if(ss.has(e))return;ss.add(e);const n=e.resume.bind(e),r=e.reset.bind(e);e.resume=()=>{this.suspensions.size>0||e.timerId!=null||n()},e.reset=o=>{e.pause(),r(o)}}add(e,n){const r=super.add(e,n),o=this.visibleToasts.find(i=>i.key===r)?.timer;return o&&this.guardTimer(o),r}updateToast(e,n,r){if(this.exitTimers.has(e))return!1;const o=this.visibleToasts.find(s=>s.key===e);if(!o)return!1;if(o.content=n,r&&"onClose"in r&&(o.onClose=r.onClose),r?.timeout!==void 0)if(o.timer?.pause(),o.timeout=r.timeout,r.timeout>0){const s=new L1(()=>this.close(e),r.timeout);this.guardTimer(s),o.timer=s}else o.timer=void 0;const i=this.updateVisibleToasts;return typeof i=="function"?i.call(this,"add"):(en.didWarnMissingNotify||(en.didWarnMissingNotify=!0,console.warn("[HeroUI Toast] react-stately no longer exposes updateVisibleToasts; toast updates fall back to a partial re-render.")),this.exitingSnapshot=new Set(this.exitTimers.keys()),this.notifyExitSubscribers()),!0}getExitingKeys=()=>this.exitingSnapshot;subscribeExiting=e=>(this.exitSubscriptions.add(e),()=>{this.exitSubscriptions.delete(e)});notifyExitSubscribers(){for(const e of this.exitSubscriptions)e()}close(e){const n=this.visibleToasts.find(i=>i.key===e);if(this.exitTimers.has(e)||!n)return;if(!(this.exitDuration>0&&typeof window<"u"&&!window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches)){super.close(e);return}const o=n.onClose;n.onClose=void 0,this.exitTimers.set(e,setTimeout(()=>{this.exitTimers.delete(e),this.exitingSnapshot=new Set(this.exitTimers.keys()),super.close(e),this.isBatchingExits||this.notifyExitSubscribers()},this.exitDuration)),this.isBatchingExits||(this.exitingSnapshot=new Set(this.exitTimers.keys()),this.notifyExitSubscribers()),o?.()}clear(){this.isBatchingExits=!0;try{for(const e of[...this.visibleToasts])this.close(e.key)}finally{this.isBatchingExits=!1,this.exitingSnapshot=new Set(this.exitTimers.keys()),this.notifyExitSubscribers()}}destroy(){for(const e of this.exitTimers.values())clearTimeout(e);this.exitTimers.clear(),this.exitingSnapshot=new Set,this.exitSubscriptions.clear(),this.suspensions.clear();for(const e of this.visibleToasts)e.timer?.pause()}}const I1=t=>t instanceof en;class Ec{constructor(e){this.maxVisibleToasts=e?.maxVisibleToasts,this.queue=new en({maxVisibleToasts:c1,wrapUpdate:e?.wrapUpdate},e?.exitDuration??yc)}add(e,n){return this.queue.add(e,{...n,timeout:n?.timeout??Fn})}close(e){this.queue.close(e)}update(e,n,r){return this.queue.updateToast(e,n,r)}pauseAll(){this.queue.pauseAll()}resumeAll(){this.queue.resumeAll()}clear(){this.queue.clear()}subscribe(e){return this.queue.subscribe(e)}get visibleToasts(){return this.queue.visibleToasts}getQueue(){return this.queue}destroy(){this.queue.destroy()}}function F1(t){const e=(i,s)=>({title:i,description:s?.description,indicator:s?.indicator,variant:s?.variant||"default",actionProps:s?.actionProps,isLoading:s?.isLoading}),n=i=>({timeout:i?.timeout??Fn,onClose:i?.onClose}),r=i=>({...i&&"timeout"in i?{timeout:i.timeout}:null,...i&&"onClose"in i?{onClose:i.onClose}:null}),o=(i,s)=>t.add(e(i,s),n(s));return o.success=(i,s)=>o(i,{...s,variant:"success"}),o.danger=(i,s)=>o(i,{...s,variant:"danger"}),o.info=(i,s)=>o(i,{...s,variant:"accent"}),o.warning=(i,s)=>o(i,{...s,variant:"warning"}),o.update=(i,s,l)=>{const u=e(s,l);return t.update(i,u,r(l))?i:t.add(u,n(l))},o.promise=(i,s)=>{const l=typeof i=="function"?i():i,u=t.add(e(s.loading,{isLoading:!0}),{timeout:0});return l.then(c=>{const d=typeof s.success=="function"?s.success(c):s.success;return o.update(u,d,{isLoading:!1,timeout:Fn,variant:"success"})}).catch(c=>{const d=typeof s.error=="function"?s.error(c):s.error;return o.update(u,d,{isLoading:!1,timeout:Fn,variant:"danger"})}),u},o.getQueue=()=>t.getQueue(),o.close=i=>t.close(i),o.pauseAll=()=>t.pauseAll(),o.resumeAll=()=>t.resumeAll(),o.clear=()=>t.clear(),o}const K1=new Ec,Wn=F1(K1),st=a.createContext({}),ls=["altKey","ctrlKey","metaKey","shiftKey"],D1=new Set,as=()=>D1,N1=()=>()=>{},O1=(t,e)=>{if(!e){document.activeElement?.blur();return}const n=i=>i instanceof HTMLElement&&i.matches('[data-slot="toast"]:not([data-exiting="true"]):not([data-hidden="true"])');let r=null,o=t.previousElementSibling;for(;o;){if(n(o)){r=o;break}o=o.previousElementSibling}if(!r)for(o=t.nextElementSibling;o;){if(n(o)){r=o;break}o=o.nextElementSibling}if(!r){const i=t.closest('[data-slot="toast-region"]');if(i){for(const s of i.querySelectorAll('[data-slot="toast"]:not([data-exiting="true"]):not([data-hidden="true"])'))if(s!==t){r=s;break}}}r?r.focus():document.activeElement?.blur()},Fo=({children:t,className:e,placement:n,ref:r,scaleFactor:o,style:i,toast:s,variant:l,...u})=>{const{exitingKeys:c,gap:d=bc,heightsByKey:f,isExpanded:p=!1,maxVisibleToasts:m=gc,onToastHeightChange:h,onToastHeightRemove:g,placement:b,scaleFactor:y,slots:v}=a.use(st),w=n??b,x=o??y??vc,K=a.use(cr).visibleToasts,P=s?.key,S=P!=null&&(c?.has(P)??!1),I=a.useMemo(()=>!c||c.size===0?K:K.filter(W=>!c.has(W.key)||W.key===P),[c,P,K]),M=K.indexOf(s),O=I.indexOf(s),z=O<=0,U=!S&&O>=m,C=a.useRef(null),R=a.useMemo(()=>nn(C,r),[r]),{height:H}=k1(C),{isFocusVisible:L}=sp(),[q,T]=a.useState(!0);a.useEffect(()=>{let W=0;const ne=requestAnimationFrame(()=>{W=requestAnimationFrame(()=>{T(!1)})});return()=>{cancelAnimationFrame(ne),cancelAnimationFrame(W)}},[]),a.useLayoutEffect(()=>{P&&typeof H=="number"&&h?.(P,H)},[P,H,h]),a.useEffect(()=>{if(P)return()=>{g?.(P)}},[P,g]),a.useLayoutEffect(()=>{const W=C.current;if(W)if(W.tabIndex=z||p&&!U&&!S?0:-1,U||S)if(W.contains(document.activeElement)&&O1(W,L),W.setAttribute("aria-hidden","true"),S)for(const ne of W.querySelectorAll("button, a"))ne.tabIndex=-1;else W.setAttribute("inert","");else W.removeAttribute("aria-hidden"),W.removeAttribute("inert")},[z,p,U,S,L]);const A=a.useMemo(()=>{const W=I[0]?.key,ne=(W?f?.[W]:void 0)??H??0;let te=0;for(let k=0;k({...A,...W}));return E.jsx(r1,{...u,ref:R,className:Se(e,v?.toast({variant:l})),"data-entering":Ge(q),"data-exiting":Ge(S),"data-expanded":Ge(p),"data-frontmost":Ge(z),"data-hidden":Ge(U),"data-index":O,"data-placement":w,"data-slot":"toast",style:j,toast:s,children:t})};Fo.displayName="HeroUI.Toast";const wc=({children:t,className:e,...n})=>{const{slots:r}=a.use(st);return E.jsx(o1,{...n,className:be(r?.content,e),"data-slot":"toast-content",children:t})},Gn=({children:t,className:e,variant:n,...r})=>{const{slots:o}=a.use(st),i=$.isValidElement(t)?t.type:t??"default",s=a.useRef(i),[l,u]=a.useState(!1);xc(()=>{s.current!==i&&(s.current=i,u(!0))},[i]);const c=a.useCallback(()=>{switch(n){case"accent":return E.jsx(jn,{"data-slot":"toast-default-icon"});case"success":return E.jsx(gl,{"data-slot":"toast-default-icon"});case"warning":return E.jsx(ml,{"data-slot":"toast-default-icon"});case"danger":return E.jsx(bl,{"data-slot":"toast-default-icon"});default:return E.jsx(jn,{"data-slot":"toast-default-icon"})}},[n]);return E.jsx(ge.div,{...r,className:be(o?.indicator,e),"data-slot":"toast-indicator","data-swapped":Ge(l),children:t??c()})};Gn.displayName="HeroUI.ToastIndicator";const Ko=({children:t,className:e,...n})=>{const{slots:r}=a.use(st);return E.jsx(wa,{...n,className:be(r?.title,e),"data-slot":"toast-title",slot:"title",children:t})};Ko.displayName="HeroUI.ToastTitle";const Do=({children:t,className:e,...n})=>{const{slots:r}=a.use(st);return E.jsx(wa,{...n,className:be(r?.description,e),"data-slot":"toast-description",slot:"description",children:t})};Do.displayName="HeroUI.ToastDescription";const No=({className:t,...e})=>{const{slots:n}=a.use(st);return E.jsx(Bm,{...e,className:Se(t,n?.close()),"data-slot":"toast-close",slot:"close"})};No.displayName="HeroUI.ToastCloseButton";const qn=({children:t,className:e,...n})=>{const{slots:r}=a.use(st);return E.jsx(Ro,{...n,className:Se(e,r?.action?.()),"data-slot":"toast-action-button",children:t})};qn.displayName="HeroUI.ToastActionButton";const Cc=({children:t,className:e,gap:n=bc,hotkey:r=f1,isExpanded:o=!1,maxVisibleToasts:i,placement:s="bottom",queue:l,ref:u,scaleFactor:c=vc,style:d,width:f=d1,...p})=>{const m=a.useMemo(()=>N0({placement:s}),[s]),h=R1("(max-width: 768px)"),[g,b]=a.useState({}),y=a.useRef(r);a.useEffect(()=>{y.current=r},[r]);const v=a.useMemo(()=>l?l.getQueue():Wn.getQueue(),[l]),w=I1(v)?v:null,x=a.useSyncExternalStore(w?w.subscribeExiting:N1,w?w.getExitingKeys:as,as),F=a.useCallback(T=>v.subscribe(T),[v]),K=a.useSyncExternalStore(F,()=>v.visibleToasts.length,()=>0),[P,S]=a.useState(!1),I=a.useRef(!1),M=a.useCallback(T=>{const A=G=>{typeof u=="function"?u(G):u&&typeof u=="object"&&(u.current=G)};if(A(T),!T)return;const j=()=>{!I.current&&!T.contains(document.activeElement)&&S(!1)},W=G=>{G.pointerType!=="touch"&&(I.current=!0,S(!0))},ne=G=>{G.pointerType!=="touch"&&(I.current=!1,j())},te=()=>{S(!0)},k=G=>{G.relatedTarget instanceof Node&&T.contains(G.relatedTarget)||j()},Z=G=>{G.pointerType==="touch"||T.contains(G.target)||(I.current=!1,j())},oe=G=>{G.key==="Escape"&&(T.contains(document.activeElement)&&document.activeElement.blur(),I.current=!1,S(!1))},ye=G=>{const Fe=y.current;if(Fe.length===0)return;const D=ls.every(ie=>G[ie]===Fe.includes(ie)),N=Fe.every(ie=>ls.includes(ie)||G.code===ie);!D||!N||(G.preventDefault(),T.focus())};return T.addEventListener("pointerenter",W),T.addEventListener("pointermove",W),T.addEventListener("pointerleave",ne),T.addEventListener("focusin",te),T.addEventListener("focusout",k),T.addEventListener("keydown",oe),document.addEventListener("pointerover",Z,!0),document.addEventListener("keydown",ye),()=>{T.removeEventListener("pointerenter",W),T.removeEventListener("pointermove",W),T.removeEventListener("pointerleave",ne),T.removeEventListener("focusin",te),T.removeEventListener("focusout",k),T.removeEventListener("keydown",oe),document.removeEventListener("pointerover",Z,!0),document.removeEventListener("keydown",ye),I.current=!1,S(!1),A(null)}},[u]);a.useEffect(()=>{if(!(!w||!P))return w.suspendTimers("interaction"),()=>{w.resumeTimers("interaction")}},[w,P]),a.useEffect(()=>{if(typeof document>"u"||!w)return;const T=()=>{document.hidden?w.suspendTimers("visibility"):w.resumeTimers("visibility")};return document.addEventListener("visibilitychange",T),()=>{document.removeEventListener("visibilitychange",T),w.resumeTimers("visibility")}},[w]);const O=K-x.size,z=(o||P)&&O>1,U=a.useMemo(()=>{const T=l&&"maxVisibleToasts"in l?l.maxVisibleToasts:void 0;return i??T??gc},[i,l]),C=a.useCallback((T,A)=>{b(j=>j[T]===A?j:{...j,[T]:A})},[]),R=a.useCallback(T=>{b(A=>{if(!(T in A))return A;const j={...A};return delete j[T],j})},[]),H=a.useCallback(T=>{const{actionProps:A,description:j,indicator:W,isLoading:ne,title:te,variant:k}=T.toast.content??{};return E.jsxs(Fo,{placement:s,scaleFactor:c,toast:T.toast,variant:k,children:[W===null?null:ne?E.jsx(Gn,{variant:k,children:E.jsx(T1,{color:"current",size:"sm"})}):E.jsx(Gn,{variant:k,children:W}),E.jsxs(wc,{children:[!!te&&E.jsx(Ko,{children:te}),!!j&&E.jsx(Do,{children:j}),h&&A?.children?E.jsx(qn,{...A,children:A.children}):null]}),!h&&A?.children?E.jsx(qn,{...A,children:A.children}):null,E.jsx(No,{})]})},[h,s,c]),L=a.useMemo(()=>({exitingKeys:x,gap:n,heightsByKey:g,isExpanded:z,maxVisibleToasts:U,onToastHeightChange:C,onToastHeightRemove:R,placement:s,scaleFactor:c,slots:m}),[x,n,C,R,z,s,U,c,m,g]),q=ho(d,T=>({"--gap":`${n}px`,"--placement":s,"--scale-factor":c,"--toast-width":typeof f=="number"?`${f}px`:f,...T}));return E.jsx(t1,{...p,ref:M,className:Se(e,m?.region()),"data-expanded":Ge(z),"data-slot":"toast-region",queue:v,style:q,children:T=>{const A=T.toast.content,j={...T,isLoading:A?.isLoading??!1};return E.jsx(st,{value:L,children:typeof t>"u"?H(T):typeof t=="function"?t(j):t})}})};Cc.displayName="HeroUI.ToastProvider";const V1=Object.assign(Fo,{Provider:Cc,Content:wc,Indicator:Gn,Title:Ko,Description:Do,ActionButton:qn,CloseButton:No,Queue:Ec,toast:Wn}),B1=new Map([["bold",a.createElement(a.Fragment,null,a.createElement("path",{d:"M87.5,151.52l64-64a12,12,0,0,1,17,17l-64,64a12,12,0,0,1-17-17Zm131-114a60.08,60.08,0,0,0-84.87,0L103.51,67.61a12,12,0,0,0,17,17l30.07-30.06a36,36,0,0,1,50.93,50.92L171.4,135.52a12,12,0,1,0,17,17l30.08-30.06A60.09,60.09,0,0,0,218.45,37.55ZM135.52,171.4l-30.07,30.08a36,36,0,0,1-50.92-50.93l30.06-30.07a12,12,0,0,0-17-17L37.55,133.58a60,60,0,0,0,84.88,84.87l30.06-30.07a12,12,0,0,0-17-17Z"}))],["duotone",a.createElement(a.Fragment,null,a.createElement("path",{d:"M209.94,113.94l-96,96a48,48,0,0,1-67.88-67.88l96-96a48,48,0,0,1,67.88,67.88Z",opacity:"0.2"}),a.createElement("path",{d:"M165.66,90.34a8,8,0,0,1,0,11.32l-64,64a8,8,0,0,1-11.32-11.32l64-64A8,8,0,0,1,165.66,90.34ZM215.6,40.4a56,56,0,0,0-79.2,0L106.34,70.45a8,8,0,0,0,11.32,11.32l30.06-30a40,40,0,0,1,56.57,56.56l-30.07,30.06a8,8,0,0,0,11.31,11.32L215.6,119.6a56,56,0,0,0,0-79.2ZM138.34,174.22l-30.06,30.06a40,40,0,1,1-56.56-56.57l30.05-30.05a8,8,0,0,0-11.32-11.32L40.4,136.4a56,56,0,0,0,79.2,79.2l30.06-30.07a8,8,0,0,0-11.32-11.31Z"}))],["fill",a.createElement(a.Fragment,null,a.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM144.56,173.66l-21.45,21.45a44,44,0,0,1-62.22-62.22l21.45-21.46a8,8,0,0,1,11.32,11.31L72.2,144.2a28,28,0,0,0,39.6,39.6l21.45-21.46a8,8,0,0,1,11.31,11.32Zm-34.9-16a8,8,0,0,1-11.32-11.32l48-48a8,8,0,0,1,11.32,11.32Zm85.45-34.55-21.45,21.45a8,8,0,0,1-11.32-11.31L183.8,111.8a28,28,0,0,0-39.6-39.6L122.74,93.66a8,8,0,0,1-11.31-11.32l21.46-21.45a44,44,0,0,1,62.22,62.22Z"}))],["light",a.createElement(a.Fragment,null,a.createElement("path",{d:"M164.25,91.75a6,6,0,0,1,0,8.49l-64,64a6,6,0,0,1-8.49-8.48l64-64A6,6,0,0,1,164.25,91.75ZM214.2,41.8a54.07,54.07,0,0,0-76.38,0L107.75,71.85a6,6,0,0,0,8.49,8.49l30.07-30.06a42,42,0,0,1,59.41,59.41l-30.08,30.07a6,6,0,1,0,8.49,8.49l30.07-30.07A54,54,0,0,0,214.2,41.8ZM139.76,175.64l-30.07,30.08a42,42,0,0,1-59.41-59.41l30.06-30.07a6,6,0,0,0-8.49-8.49l-30,30.07a54,54,0,0,0,76.38,76.39l30.07-30.08a6,6,0,0,0-8.49-8.49Z"}))],["regular",a.createElement(a.Fragment,null,a.createElement("path",{d:"M165.66,90.34a8,8,0,0,1,0,11.32l-64,64a8,8,0,0,1-11.32-11.32l64-64A8,8,0,0,1,165.66,90.34ZM215.6,40.4a56,56,0,0,0-79.2,0L106.34,70.45a8,8,0,0,0,11.32,11.32l30.06-30a40,40,0,0,1,56.57,56.56l-30.07,30.06a8,8,0,0,0,11.31,11.32L215.6,119.6a56,56,0,0,0,0-79.2ZM138.34,174.22l-30.06,30.06a40,40,0,1,1-56.56-56.57l30.05-30.05a8,8,0,0,0-11.32-11.32L40.4,136.4a56,56,0,0,0,79.2,79.2l30.06-30.07a8,8,0,0,0-11.32-11.31Z"}))],["thin",a.createElement(a.Fragment,null,a.createElement("path",{d:"M162.84,93.16a4,4,0,0,1,0,5.66l-64,64a4,4,0,0,1-5.66-5.66l64-64A4,4,0,0,1,162.84,93.16Zm49.95-49.95a52.07,52.07,0,0,0-73.56,0L109.17,73.27a4,4,0,0,0,5.65,5.66l30.07-30.06a44,44,0,0,1,62.24,62.24l-30.07,30.06a4,4,0,0,0,5.66,5.66l30.07-30.06A52.07,52.07,0,0,0,212.79,43.21ZM141.17,177.06l-30.06,30.07a44,44,0,0,1-62.24-62.24l30.06-30.06a4,4,0,0,0-5.66-5.66L43.21,139.23a52,52,0,0,0,73.56,73.56l30.06-30.07a4,4,0,1,0-5.66-5.66Z"}))]]),_1=new Map([["bold",a.createElement(a.Fragment,null,a.createElement("path",{d:"M208,76H180V56A52,52,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-76-92a32,32,0,0,0-12,61.66V180a12,12,0,0,0,24,0v-6.34A32,32,0,0,0,128,112Zm0,24a8,8,0,1,1-8,8A8,8,0,0,1,128,136Z"}))],["duotone",a.createElement(a.Fragment,null,a.createElement("path",{d:"M208,88H48a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8H208a8,8,0,0,0,8-8V96A8,8,0,0,0,208,88Zm-80,72a20,20,0,1,1,20-20A20,20,0,0,1,128,160Z",opacity:"0.2"}),a.createElement("path",{d:"M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-80-96a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Z"}))],["fill",a.createElement(a.Fragment,null,a.createElement("path",{d:"M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm-72,78.63V184a8,8,0,0,1-16,0V158.63a24,24,0,1,1,16,0ZM160,80H96V56a32,32,0,0,1,64,0Z"}))],["light",a.createElement(a.Fragment,null,a.createElement("path",{d:"M208,82H174V56a46,46,0,0,0-92,0V82H48A14,14,0,0,0,34,96V208a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V96A14,14,0,0,0,208,82ZM94,56a34,34,0,0,1,68,0V82H94ZM210,208a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V96a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2Zm-82-94a26,26,0,0,0-6,51.29V184a6,6,0,0,0,12,0V165.29A26,26,0,0,0,128,114Zm0,40a14,14,0,1,1,14-14A14,14,0,0,1,128,154Z"}))],["regular",a.createElement(a.Fragment,null,a.createElement("path",{d:"M128,112a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Zm80-72H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Z"}))],["thin",a.createElement(a.Fragment,null,a.createElement("path",{d:"M208,84H172V56a44,44,0,0,0-88,0V84H48A12,12,0,0,0,36,96V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V96A12,12,0,0,0,208,84ZM92,56a36,36,0,0,1,72,0V84H92ZM212,208a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V96a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-84-92a24,24,0,0,0-4,47.66V184a4,4,0,0,0,8,0V163.66A24,24,0,0,0,128,116Zm0,40a16,16,0,1,1,16-16A16,16,0,0,1,128,156Z"}))]]),H1=new Map([["bold",a.createElement(a.Fragment,null,a.createElement("path",{d:"M236.37,139.4a12,12,0,0,0-12-3A84.07,84.07,0,0,1,119.6,31.59a12,12,0,0,0-15-15A108.86,108.86,0,0,0,49.69,55.07,108,108,0,0,0,136,228a107.09,107.09,0,0,0,64.93-21.69,108.86,108.86,0,0,0,38.44-54.94A12,12,0,0,0,236.37,139.4Zm-49.88,47.74A84,84,0,0,1,68.86,69.51,84.93,84.93,0,0,1,92.27,48.29Q92,52.13,92,56A108.12,108.12,0,0,0,200,164q3.87,0,7.71-.27A84.79,84.79,0,0,1,186.49,187.14Z"}))],["duotone",a.createElement(a.Fragment,null,a.createElement("path",{d:"M227.89,147.89A96,96,0,1,1,108.11,28.11,96.09,96.09,0,0,0,227.89,147.89Z",opacity:"0.2"}),a.createElement("path",{d:"M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56,104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"}))],["fill",a.createElement(a.Fragment,null,a.createElement("path",{d:"M235.54,150.21a104.84,104.84,0,0,1-37,52.91A104,104,0,0,1,32,120,103.09,103.09,0,0,1,52.88,57.48a104.84,104.84,0,0,1,52.91-37,8,8,0,0,1,10,10,88.08,88.08,0,0,0,109.8,109.8,8,8,0,0,1,10,10Z"}))],["light",a.createElement(a.Fragment,null,a.createElement("path",{d:"M232.13,143.64a6,6,0,0,0-6-1.49A90.07,90.07,0,0,1,113.86,29.85a6,6,0,0,0-7.49-7.48A102.88,102.88,0,0,0,54.48,58.68,102,102,0,0,0,197.32,201.52a102.88,102.88,0,0,0,36.31-51.89A6,6,0,0,0,232.13,143.64Zm-42,48.29a90,90,0,0,1-126-126A90.9,90.9,0,0,1,99.65,37.66,102.06,102.06,0,0,0,218.34,156.35,90.9,90.9,0,0,1,190.1,191.93Z"}))],["regular",a.createElement(a.Fragment,null,a.createElement("path",{d:"M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56,104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"}))],["thin",a.createElement(a.Fragment,null,a.createElement("path",{d:"M230.72,145.06a4,4,0,0,0-4-1A92.08,92.08,0,0,1,111.94,29.27a4,4,0,0,0-5-5A100.78,100.78,0,0,0,56.08,59.88a100,100,0,0,0,140,140,100.78,100.78,0,0,0,35.59-50.87A4,4,0,0,0,230.72,145.06ZM191.3,193.53A92,92,0,0,1,62.47,64.7a93,93,0,0,1,39.88-30.35,100.09,100.09,0,0,0,119.3,119.3A93,93,0,0,1,191.3,193.53Z"}))]]),j1=new Map([["bold",a.createElement(a.Fragment,null,a.createElement("path",{d:"M116,36V20a12,12,0,0,1,24,0V36a12,12,0,0,1-24,0Zm80,92a68,68,0,1,1-68-68A68.07,68.07,0,0,1,196,128Zm-24,0a44,44,0,1,0-44,44A44.05,44.05,0,0,0,172,128ZM51.51,68.49a12,12,0,1,0,17-17l-12-12a12,12,0,0,0-17,17Zm0,119-12,12a12,12,0,0,0,17,17l12-12a12,12,0,1,0-17-17ZM196,72a12,12,0,0,0,8.49-3.51l12-12a12,12,0,0,0-17-17l-12,12A12,12,0,0,0,196,72Zm8.49,115.51a12,12,0,0,0-17,17l12,12a12,12,0,0,0,17-17ZM48,128a12,12,0,0,0-12-12H20a12,12,0,0,0,0,24H36A12,12,0,0,0,48,128Zm80,80a12,12,0,0,0-12,12v16a12,12,0,0,0,24,0V220A12,12,0,0,0,128,208Zm108-92H220a12,12,0,0,0,0,24h16a12,12,0,0,0,0-24Z"}))],["duotone",a.createElement(a.Fragment,null,a.createElement("path",{d:"M184,128a56,56,0,1,1-56-56A56,56,0,0,1,184,128Z",opacity:"0.2"}),a.createElement("path",{d:"M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"}))],["fill",a.createElement(a.Fragment,null,a.createElement("path",{d:"M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm8,24a64,64,0,1,0,64,64A64.07,64.07,0,0,0,128,64ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"}))],["light",a.createElement(a.Fragment,null,a.createElement("path",{d:"M122,40V16a6,6,0,0,1,12,0V40a6,6,0,0,1-12,0Zm68,88a62,62,0,1,1-62-62A62.07,62.07,0,0,1,190,128Zm-12,0a50,50,0,1,0-50,50A50.06,50.06,0,0,0,178,128ZM59.76,68.24a6,6,0,1,0,8.48-8.48l-16-16a6,6,0,0,0-8.48,8.48Zm0,119.52-16,16a6,6,0,1,0,8.48,8.48l16-16a6,6,0,1,0-8.48-8.48ZM192,70a6,6,0,0,0,4.24-1.76l16-16a6,6,0,0,0-8.48-8.48l-16,16A6,6,0,0,0,192,70Zm4.24,117.76a6,6,0,0,0-8.48,8.48l16,16a6,6,0,0,0,8.48-8.48ZM46,128a6,6,0,0,0-6-6H16a6,6,0,0,0,0,12H40A6,6,0,0,0,46,128Zm82,82a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V216A6,6,0,0,0,128,210Zm112-88H216a6,6,0,0,0,0,12h24a6,6,0,0,0,0-12Z"}))],["regular",a.createElement(a.Fragment,null,a.createElement("path",{d:"M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"}))],["thin",a.createElement(a.Fragment,null,a.createElement("path",{d:"M124,40V16a4,4,0,0,1,8,0V40a4,4,0,0,1-8,0Zm64,88a60,60,0,1,1-60-60A60.07,60.07,0,0,1,188,128Zm-8,0a52,52,0,1,0-52,52A52.06,52.06,0,0,0,180,128ZM61.17,66.83a4,4,0,0,0,5.66-5.66l-16-16a4,4,0,0,0-5.66,5.66Zm0,122.34-16,16a4,4,0,0,0,5.66,5.66l16-16a4,4,0,0,0-5.66-5.66ZM192,68a4,4,0,0,0,2.83-1.17l16-16a4,4,0,1,0-5.66-5.66l-16,16A4,4,0,0,0,192,68Zm2.83,121.17a4,4,0,0,0-5.66,5.66l16,16a4,4,0,0,0,5.66-5.66ZM40,124H16a4,4,0,0,0,0,8H40a4,4,0,0,0,0-8Zm88,88a4,4,0,0,0-4,4v24a4,4,0,0,0,8,0V216A4,4,0,0,0,128,212Zm112-88H216a4,4,0,0,0,0,8h24a4,4,0,0,0,0-8Z"}))]]),z1=new Map([["bold",a.createElement(a.Fragment,null,a.createElement("path",{d:"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm-12-80V80a12,12,0,0,1,24,0v52a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,172Z"}))],["duotone",a.createElement(a.Fragment,null,a.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),a.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm-8-80V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z"}))],["fill",a.createElement(a.Fragment,null,a.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm-8,56a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm8,104a12,12,0,1,1,12-12A12,12,0,0,1,128,184Z"}))],["light",a.createElement(a.Fragment,null,a.createElement("path",{d:"M128,26A102,102,0,1,0,230,128,102.12,102.12,0,0,0,128,26Zm0,192a90,90,0,1,1,90-90A90.1,90.1,0,0,1,128,218Zm-6-82V80a6,6,0,0,1,12,0v56a6,6,0,0,1-12,0Zm16,36a10,10,0,1,1-10-10A10,10,0,0,1,138,172Z"}))],["regular",a.createElement(a.Fragment,null,a.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm-8-80V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z"}))],["thin",a.createElement(a.Fragment,null,a.createElement("path",{d:"M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm-4-84V80a4,4,0,0,1,8,0v56a4,4,0,0,1-8,0Zm12,36a8,8,0,1,1-8-8A8,8,0,0,1,136,172Z"}))]]),U1=a.createContext({color:"currentColor",size:"1em",weight:"regular",mirrored:!1}),Kt=a.forwardRef((t,e)=>{const{alt:n,color:r,size:o,weight:i,mirrored:s,children:l,weights:u,...c}=t,{color:d="currentColor",size:f,weight:p="regular",mirrored:m=!1,...h}=a.useContext(U1);return a.createElement("svg",{ref:e,xmlns:"http://www.w3.org/2000/svg",width:o??f,height:o??f,fill:r??d,viewBox:"0 0 256 256",transform:s||m?"scale(-1, 1)":void 0,...h,...c},!!n&&a.createElement("title",null,n),l,u.get(i??p))});Kt.displayName="IconBase";const Sc=a.forwardRef((t,e)=>a.createElement(Kt,{ref:e,...t,weights:B1}));Sc.displayName="LinkSimpleIcon";const Z1=a.forwardRef((t,e)=>a.createElement(Kt,{ref:e,...t,weights:_1}));Z1.displayName="LockKeyIcon";const Pc=a.forwardRef((t,e)=>a.createElement(Kt,{ref:e,...t,weights:H1}));Pc.displayName="MoonIcon";const Tc=a.forwardRef((t,e)=>a.createElement(Kt,{ref:e,...t,weights:j1}));Tc.displayName="SunIcon";const kc=a.forwardRef((t,e)=>a.createElement(Kt,{ref:e,...t,weights:z1}));kc.displayName="WarningCircleIcon";function gg({sidebar:t=!1}){const[e,n]=a.useState(!1);a.useEffect(()=>{const o=()=>n(document.documentElement.dataset.theme==="dark");o();const i=new MutationObserver(o);return i.observe(document.documentElement,{attributes:!0,attributeFilter:["data-theme"]}),()=>i.disconnect()},[]);const r=e?"Light mode":"Dark mode";return E.jsxs(Ro,{variant:"ghost",isIconOnly:!t,"aria-label":`Switch to ${r.toLowerCase()}`,className:t?"h-10 w-full justify-start gap-3 px-3 text-[13px] text-muted-foreground":"size-9 min-w-9 shrink-0",onPress:()=>{const o=e?"light":"dark";document.documentElement.dataset.theme=o,document.documentElement.classList.toggle("dark",o==="dark"),document.documentElement.style.colorScheme=o;try{localStorage.setItem("recordly.theme",o)}catch{}},children:[e?E.jsx(Tc,{className:"size-[18px]"}):E.jsx(Pc,{className:"size-[18px]"}),t&&r]})}function W1(){return E.jsxs(Lb,{href:"https://github.com/webadderallorg/Recordly",target:"_blank",rel:"noopener noreferrer",className:"brand",children:[E.jsx("img",{src:"/icon-64.png",alt:"",width:"28",height:"28"}),E.jsx("span",{children:"Recordly"})]})}function vg({children:t}){return E.jsxs(En,{status:"danger",role:"alert",children:[E.jsx(En.Indicator,{children:E.jsx(kc,{})}),E.jsx(En.Content,{children:E.jsx(En.Description,{children:t})})]})}function yg({children:t}){return E.jsx("main",{className:"gate-page",children:E.jsxs(Rr,{className:"gate-card",children:[E.jsx(Rr.Header,{children:E.jsx(W1,{})}),E.jsx(Rr.Content,{className:"gate-content",children:t})]})})}async function G1(t=0,e){const n=new URL(e?`/s/${e}`:window.location.pathname,window.location.origin);t>0&&n.searchParams.set("t",String(Math.floor(t)));try{await navigator.clipboard.writeText(n.href),Wn.success("Link copied")}catch{Wn.danger("Could not copy the link. Copy the address from your browser.")}}function $g({time:t=0,code:e}){return E.jsxs(Ro,{size:"sm",variant:"secondary",onPress:()=>{G1(t,e)},children:[E.jsx(Sc,{size:16}),"Copy link"]})}function xg(){return E.jsx(V1.Provider,{placement:"bottom"})}export{nt as $,nr as A,E as B,lg as C,Bm as D,Se as E,ge as F,be as G,Q1 as H,ag as I,Kt as J,Ro as K,cg as L,fg as M,xg as N,pg as O,Rr as P,vg as Q,mg as R,Eo as S,gg as T,yg as U,Z1 as V,bg as W,hg as X,Wn as Y,uu as Z,Tm as _,gt as a,G1 as a$,ur as a0,Jn as a1,Ce as a2,ii as a3,he as a4,vp as a5,Xs as a6,it as a7,Wt as a8,Ln as a9,zh as aA,It as aB,Xh as aC,Zh as aD,sa as aE,Sd as aF,Je as aG,At as aH,J1 as aI,Gp as aJ,vl as aK,X1 as aL,xc as aM,Ge as aN,ig as aO,tg as aP,ng as aQ,eg as aR,kp as aS,a1 as aT,$c as aU,dg as aV,S1 as aW,rg as aX,T1 as aY,Lb as aZ,Sc as a_,ce as aa,tt as ab,Ed as ac,Y1 as ad,rr as ae,Y0 as af,mo as ag,go as ah,er as ai,Lt as aj,Ue as ak,$h as al,J as am,Ve as an,pm as ao,ds as ap,$s as aq,am as ar,xe as as,Kh as at,rn as au,ma as av,Ea as aw,_h as ax,Cd as ay,ar as az,pt as b,$g as b0,br as c,V as d,re as e,sh as f,lh as g,Ch as h,Np as i,Q as j,cn as k,nn as l,rt as m,Pd as n,ve as o,ue as p,de as q,Mi as r,me as s,Ee as t,aa as u,Td as v,Mt as w,Ai as x,ze as y,on as z}; diff --git a/services/recordly-share/worker/web/dist/_astro/ShareUI.DkgCTqfk.css b/services/recordly-share/worker/web/dist/_astro/ShareUI.DkgCTqfk.css new file mode 100644 index 000000000..bc731a81a --- /dev/null +++ b/services/recordly-share/worker/web/dist/_astro/ShareUI.DkgCTqfk.css @@ -0,0 +1 @@ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-content:"";--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-outline-style:solid;--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-space-x-reverse:0}}}@layer theme{:root,:host{--font-sans:"DM Sans", sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--leading-relaxed:1.625;--radius-xl:calc(var(--radius) * 1.5);--radius-2xl:calc(var(--radius) * 2);--radius-3xl:calc(var(--radius) * 3);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--border-width-field:var(--field-border-width,var(--border-width));--ease-smooth:ease;--ease-out-quad:cubic-bezier(.25, .46, .45, .94);--ease-out-quart:cubic-bezier(.165, .84, .44, 1);--ease-out-fluid:cubic-bezier(.32, .72, 0, 1);--ease-linear:linear}@layer theme.base{:root,:host{--white:oklch(100% 0 0);--black:oklch(0% 0 0);--snow:oklch(99.11% 0 0);--eclipse:oklch(21.03% .0059 285.89);--spacing:.25rem;--border-width:1px;--field-border-width:0px;--disabled-opacity:.5;--ring-offset-width:2px;--cursor-interactive:pointer;--cursor-disabled:not-allowed;--radius:.5rem;--field-radius:calc(var(--radius) * 1.5);--accent:oklch(62.04% .195 253.83);--accent-foreground:var(--snow);--success:oklch(73.29% .1935 150.81);--success-foreground:var(--eclipse);--field-border:transparent;--skeleton-animation:shimmer;--tooltip-delay:1.5s;--tooltip-close-delay:.5s}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){color-scheme:light;--background:oklch(97.02% 0 0);--foreground:var(--eclipse);--surface:var(--white);--surface-foreground:var(--foreground);--surface-secondary:oklch(95.24% .0013 286.37);--surface-secondary-foreground:var(--foreground);--surface-tertiary:oklch(93.73% .0013 286.37);--surface-tertiary-foreground:var(--foreground);--overlay:var(--white);--overlay-foreground:var(--foreground);--muted:oklch(55.17% .0138 285.94);--scrollbar:var(--scrollbar-thumb);--scrollbar-thumb:var(--foreground)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--scrollbar-thumb:color-mix(in oklch, var(--foreground) 15%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--scrollbar-track:transparent;--scrollbar-gutter:auto;--scrollbar-width:thin;--scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);--default:oklch(94% .001 286.375);--default-foreground:var(--eclipse);--field-background:var(--white);--field-foreground:oklch(21.03% .0059 285.89);--field-placeholder:var(--muted);--warning:oklch(78.19% .1585 72.33);--warning-foreground:var(--eclipse);--danger:oklch(65.32% .2328 25.74);--danger-foreground:var(--snow);--segment:var(--white);--segment-foreground:var(--eclipse);--border:oklch(90% .004 286.32);--separator:oklch(92% .004 286.32);--focus:var(--accent);--link:var(--foreground);--backdrop:#00000080;--surface-hover:var(--surface)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--surface-hover:color-mix(in oklab, var(--surface) 92%, var(--surface-foreground) 8%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--background-secondary:var(--background)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--background-secondary:color-mix(in oklab, var(--background) 96%, var(--foreground) 4%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--background-tertiary:var(--background)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--background-tertiary:color-mix(in oklab, var(--background) 92%, var(--foreground) 8%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--background-inverse:var(--foreground);--default-hover:var(--default)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--default-hover:color-mix(in oklab, var(--default) 96%, var(--default-foreground) 4%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-hover:var(--accent)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-hover:color-mix(in oklab, var(--accent) 90%, var(--accent-foreground) 10%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-hover:var(--success)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-hover:color-mix(in oklab, var(--success) 90%, var(--success-foreground) 10%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-hover:var(--warning)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-hover:color-mix(in oklab, var(--warning) 90%, var(--warning-foreground) 10%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-hover:var(--danger)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-hover:color-mix(in oklab, var(--danger) 90%, var(--danger-foreground) 10%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--field-hover:var(--field-background,var(--default))}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--field-hover:color-mix(in oklab, var(--field-background,var(--default)) 90%, var(--field-foreground,var(--foreground)) 2%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--field-focus:var(--field-background,var(--default));--field-border-hover:var(--field-border,var(--border))}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--field-border-hover:color-mix(in oklab, var(--field-border,var(--border)) 88%, var(--field-foreground,var(--foreground)) 10%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--field-border-focus:var(--field-border,var(--border))}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--field-border-focus:color-mix(in oklab, var(--field-border,var(--border)) 74%, var(--field-foreground,var(--foreground)) 22%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--default-soft:var(--default)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--default-soft:color-mix(in oklab, var(--default) 50%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--default-soft-foreground:var(--default-foreground);--default-soft-hover:var(--default)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--default-soft-hover:color-mix(in oklab, var(--default) 60%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-soft:var(--accent)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-soft:color-mix(in oklab, var(--accent) 15%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-soft-foreground:var(--accent)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-soft-foreground:color-mix(in oklab, var(--accent) 70%, var(--foreground) 30%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-soft-hover:var(--accent)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--accent-soft-hover:color-mix(in oklab, var(--accent) 20%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-soft:var(--danger)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-soft:color-mix(in oklab, var(--danger) 15%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-soft-foreground:var(--danger)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-soft-foreground:color-mix(in oklab, var(--danger) 70%, var(--foreground) 40%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-soft-hover:var(--danger)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--danger-soft-hover:color-mix(in oklab, var(--danger) 20%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-soft:var(--warning)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-soft:color-mix(in oklab, var(--warning) 15%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-soft-foreground:var(--warning)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-soft-foreground:color-mix(in oklab, var(--warning) 80%, var(--foreground) 70%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-soft-hover:var(--warning)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--warning-soft-hover:color-mix(in oklab, var(--warning) 20%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-soft:var(--success)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-soft:color-mix(in oklab, var(--success) 15%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-soft-foreground:var(--success)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-soft-foreground:color-mix(in oklab, var(--success) 80%, var(--foreground) 60%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-soft-hover:var(--success)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--success-soft-hover:color-mix(in oklab, var(--success) 20%, transparent)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--separator-secondary:var(--surface)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--separator-secondary:color-mix(in oklab, var(--surface) 85%, var(--surface-foreground) 15%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--separator-tertiary:var(--surface)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--separator-tertiary:color-mix(in oklab, var(--surface) 81%, var(--surface-foreground) 19%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--border-secondary:var(--surface)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--border-secondary:color-mix(in oklab, var(--surface) 78%, var(--surface-foreground) 22%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--border-tertiary:var(--surface)}@supports (color:color-mix(in lab,red,red)){:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--border-tertiary:color-mix(in oklab, var(--surface) 66%, var(--surface-foreground) 34%)}}:root,.light,.default,[data-theme=light],[data-theme=default],:host(.light),:host(.default),:host([data-theme=light]),:host([data-theme=default]){--surface-shadow:0 2px 4px 0 #0000000a, 0 1px 2px 0 #0000000f, 0 0 1px 0 #0000000f;--overlay-shadow:0 2px 8px 0 #0000000f, 0 -6px 12px 0 #00000008, 0 14px 28px 0 #00000014;--field-shadow:0 2px 4px 0 #0000000a, 0 1px 2px 0 #0000000f, 0 0 1px 0 #0000000f}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){color-scheme:dark;--background:oklch(12% .005 285.823);--foreground:var(--snow);--surface:oklch(21.03% .0059 285.89);--surface-foreground:var(--foreground);--surface-secondary:oklch(25.7% .0037 286.14);--surface-secondary-foreground:var(--foreground);--surface-tertiary:oklch(27.21% .0024 247.91);--surface-tertiary-foreground:var(--foreground);--overlay:oklch(21.03% .0059 285.89);--overlay-foreground:var(--foreground);--muted:oklch(70.5% .015 286.067);--scrollbar:var(--scrollbar-thumb);--scrollbar-thumb:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--scrollbar-thumb:color-mix(in oklch, var(--foreground) 15%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--scrollbar-track:transparent;--scrollbar-gutter:auto;--scrollbar-width:thin;--scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);--default:oklch(27.4% .006 286.033);--default-foreground:var(--snow);--field-background:oklch(21.03% .0059 285.89);--field-foreground:var(--foreground);--field-placeholder:var(--muted);--warning:oklch(82.03% .1388 76.34);--warning-foreground:var(--eclipse);--danger:oklch(59.4% .1967 24.63);--danger-foreground:var(--snow);--segment:oklch(39.64% .01 285.93);--segment-foreground:var(--foreground);--border:oklch(28% .006 286.033);--separator:oklch(25% .006 286.033);--focus:var(--accent);--link:var(--foreground);--backdrop:#0009;--surface-shadow:0 0 0 0 transparent inset;--overlay-shadow:0 0 1px 0 #ffffff4d inset;--field-shadow:0 0 0 0 transparent inset;--surface-hover:var(--surface)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--surface-hover:color-mix(in oklab, var(--surface) 92%, var(--surface-foreground) 8%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--background-secondary:var(--background)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--background-secondary:color-mix(in oklab, var(--background) 96%, var(--foreground) 4%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--background-tertiary:var(--background)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--background-tertiary:color-mix(in oklab, var(--background) 92%, var(--foreground) 8%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--background-inverse:var(--foreground);--default-hover:var(--default)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--default-hover:color-mix(in oklab, var(--default) 96%, var(--default-foreground) 4%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-hover:var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-hover:color-mix(in oklab, var(--accent) 90%, var(--accent-foreground) 10%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-hover:var(--success)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-hover:color-mix(in oklab, var(--success) 90%, var(--success-foreground) 10%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-hover:var(--warning)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-hover:color-mix(in oklab, var(--warning) 90%, var(--warning-foreground) 10%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-hover:var(--danger)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-hover:color-mix(in oklab, var(--danger) 90%, var(--danger-foreground) 10%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--field-hover:var(--field-background,var(--default))}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--field-hover:color-mix(in oklab, var(--field-background,var(--default)) 90%, var(--field-foreground,var(--foreground)) 2%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--field-focus:var(--field-background,var(--default));--field-border-hover:var(--field-border,var(--border))}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--field-border-hover:color-mix(in oklab, var(--field-border,var(--border)) 88%, var(--field-foreground,var(--foreground)) 10%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--field-border-focus:var(--field-border,var(--border))}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--field-border-focus:color-mix(in oklab, var(--field-border,var(--border)) 74%, var(--field-foreground,var(--foreground)) 22%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--default-soft:var(--default)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--default-soft:color-mix(in oklab, var(--default) 50%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--default-soft-foreground:var(--default-foreground);--default-soft-hover:var(--default)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--default-soft-hover:color-mix(in oklab, var(--default) 60%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-soft:var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-soft:color-mix(in oklab, var(--accent) 12%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-soft-foreground:var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-soft-foreground:color-mix(in oklab, var(--accent) 80%, var(--foreground) 30%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-soft-hover:var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--accent-soft-hover:color-mix(in oklab, var(--accent) 16%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-soft:var(--danger)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-soft:color-mix(in oklab, var(--danger) 15%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-soft-foreground:var(--danger)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-soft-foreground:color-mix(in oklab, var(--danger) 80%, var(--foreground) 30%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-soft-hover:var(--danger)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--danger-soft-hover:color-mix(in oklab, var(--danger) 20%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-soft:var(--warning)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-soft:color-mix(in oklab, var(--warning) 12%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-soft-foreground:var(--warning)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-soft-foreground:color-mix(in oklab, var(--warning) 80%, var(--foreground) 30%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-soft-hover:var(--warning)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--warning-soft-hover:color-mix(in oklab, var(--warning) 16%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-soft:var(--success)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-soft:color-mix(in oklab, var(--success) 12%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-soft-foreground:var(--success)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-soft-foreground:color-mix(in oklab, var(--success) 80%, var(--foreground) 30%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-soft-hover:var(--success)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--success-soft-hover:color-mix(in oklab, var(--success) 16%, transparent)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--separator-secondary:var(--surface)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--separator-secondary:color-mix(in oklab, var(--surface) 85%, var(--surface-foreground) 15%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--separator-tertiary:var(--surface)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--separator-tertiary:color-mix(in oklab, var(--surface) 81%, var(--surface-foreground) 19%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--border-secondary:var(--surface)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--border-secondary:color-mix(in oklab, var(--surface) 78%, var(--surface-foreground) 22%)}}.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--border-tertiary:var(--surface)}@supports (color:color-mix(in lab,red,red)){.dark,[data-theme=dark],:host(.dark),:host([data-theme=dark]){--border-tertiary:color-mix(in oklab, var(--surface) 66%, var(--surface-foreground) 34%)}}[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--accent-soft-foreground:var(--accent)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--accent-soft-foreground:color-mix(in oklab, var(--accent) 92%, var(--foreground) 8%)}}[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--danger-soft-foreground:var(--danger)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--danger-soft-foreground:color-mix(in oklab, var(--danger) 92%, var(--foreground) 8%)}}[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--warning-soft-foreground:var(--warning)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--warning-soft-foreground:color-mix(in oklab, var(--warning) 92%, var(--foreground) 8%)}}[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--success-soft-foreground:var(--success)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true]:not(.dark):not([data-theme=dark]){--success-soft-foreground:color-mix(in oklab, var(--success) 92%, var(--foreground) 8%)}}[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--accent-soft-foreground:var(--accent)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--accent-soft-foreground:color-mix(in oklab, var(--accent) 92%, var(--foreground) 8%)}}[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--danger-soft-foreground:var(--danger)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--danger-soft-foreground:color-mix(in oklab, var(--danger) 92%, var(--foreground) 8%)}}[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--warning-soft-foreground:var(--warning)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--warning-soft-foreground:color-mix(in oklab, var(--warning) 92%, var(--foreground) 8%)}}[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--success-soft-foreground:var(--success)}@supports (color:color-mix(in lab,red,red)){[data-vibrant-palette=true].dark,[data-vibrant-palette=true][data-theme=dark]{--success-soft-foreground:color-mix(in oklab, var(--success) 92%, var(--foreground) 8%)}}}@layer components;}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*,:after,:before,::backdrop{border-color:var(--border,currentColor)}::file-selector-button{border-color:var(--border,currentColor)}:root{view-transition-name:none;--z-index-overlay:100000;--z-index-toast:calc(var(--z-index-overlay) + 1)}::view-transition{pointer-events:none}[data-scrollbar=thin]{--scrollbar-width:thin;--scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);--scrollbar-gutter:auto}[data-scrollbar=default]{--scrollbar-width:auto;--scrollbar-color:auto;--scrollbar-gutter:auto}[data-scrollbar=none]{--scrollbar-width:none;--scrollbar-color:auto;--scrollbar-gutter:auto}:root{font-family:DM Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}html{background:var(--background);color:var(--foreground);color-scheme:light dark}body{min-width:320px;margin:0}button,input,textarea{font:inherit}svg{flex-shrink:0}::selection{background:var(--accent-soft);color:var(--accent-soft-foreground)}}@layer components{.close-button{isolation:isolate;height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);transform-origin:50%;border-radius:calc(var(--radius) * 1.5);padding:var(--spacing);-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);transition:transform .25s var(--ease-out-quart),color .15s var(--ease-out),background-color .1s var(--ease-out),box-shadow .15s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex;position:relative}.close-button:is([data-reduce-motion=true],[data-reduce-motion=true] *),.close-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.close-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.close-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.close-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.close-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.close-button:focus-visible:not([data-focused]),.close-button[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.close-button:disabled,.close-button[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.close-button[data-pending=true]{pointer-events:none}.close-button svg{pointer-events:none;width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);margin-inline:calc(var(--spacing) * -.5);margin-block:calc(var(--spacing) * .5);flex-shrink:0;align-self:center}.close-button--default{background-color:var(--default);color:var(--muted)}@media(hover:hover){.close-button--default:hover,.close-button--default[data-hovered=true]{background-color:var(--default-hover)}}.close-button--default:active,.close-button--default[data-pressed=true]{transform:scale(.93)}.description{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));text-wrap:wrap;overflow-wrap:break-word;color:var(--muted)}.error-message{height:auto;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));overflow-wrap:break-word;color:var(--danger);transition:opacity .15s var(--ease-out),height .35s var(--ease-smooth)}.error-message:is([data-reduce-motion=true],[data-reduce-motion=true] *),.error-message:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.error-message:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.error-message:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.error-message:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.error-message:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.field-error{height:0;padding-inline:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));overflow-wrap:break-word;color:var(--danger);opacity:0}.field-error[data-visible]{opacity:1;height:auto}.field-error{transition:opacity .15s var(--ease-out),height .35s var(--ease-smooth)}.field-error:is([data-reduce-motion=true],[data-reduce-motion=true] *),.field-error:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.field-error:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.field-error:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.field-error:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.field-error:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.checkbox>.field-error,.checkbox>[data-slot=field-error],.switch>.field-error,.switch>[data-slot=field-error],.radio>.field-error,.radio>[data-slot=field-error]{height:auto;min-height:0;color:var(--muted);opacity:1;margin:0;padding:0;transition:none}.label{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}:is(.label--required,[data-required=true]:not([role=group]):not([role=radiogroup]):not([role=checkboxgroup])>.label,[data-required=true]:not([data-slot=radio]):not([data-slot=checkbox])>.label):after{color:var(--danger);--tw-content:"*";content:var(--tw-content);margin-inline-start:calc(var(--spacing) * .5)}.label--disabled,[data-disabled=true] .label{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.label--invalid,[data-invalid=true] .label,[aria-invalid=true] .label{color:var(--danger)}.accordion{contain:layout style;width:100%}.accordion__body{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.accordion__body-inner{padding-inline:calc(var(--spacing) * 4);padding-top:0;padding-bottom:calc(var(--spacing) * 4);color:var(--muted)}.accordion__indicator{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);color:var(--muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.25s;flex-shrink:0;margin-inline-start:auto;transition-duration:.25s}.accordion__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.accordion__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.accordion__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.accordion__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.accordion__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.accordion__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.accordion__indicator[data-expanded=true]{rotate:-180deg}.accordion__item{--tw-border-style:none;border-style:none;position:relative}.accordion__item:after{content:"";border-radius:calc(var(--radius) * .25);background-color:var(--separator);inset-inline-start:calc(var(--spacing) * 0);width:100%;height:1px;position:absolute;bottom:0}.accordion__item:last-child:after{content:none}.accordion__item[data-hide-separator=true]:after{display:none}.accordion__trigger{cursor:var(--cursor-interactive);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);text-align:start;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);-webkit-tap-highlight-color:transparent;transition:opacity .15s var(--ease-out),box-shadow .15s var(--ease-out);flex:1;justify-content:space-between;align-items:center;display:flex}.accordion__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.accordion__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.accordion__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.accordion__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.accordion__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.accordion__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.accordion__trigger:hover:not([aria-expanded=true]){background-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.accordion__trigger:hover:not([aria-expanded=true]){background-color:color-mix(in oklab,var(--foreground) 3%,transparent 90%)}}}.accordion__trigger:focus-visible:not([data-focused]),.accordion__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.accordion__trigger:disabled,.accordion__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.accordion__panel{opacity:0;height:var(--disclosure-panel-height);transition:height .2s var(--ease-out-quad),opacity .2s var(--ease-out);overflow:clip}.accordion__panel:is([data-reduce-motion=true],[data-reduce-motion=true] *),.accordion__panel:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.accordion__panel:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.accordion__panel:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.accordion__panel:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.accordion__panel:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.accordion__panel[data-expanded=true]{will-change:height,opacity;opacity:1}.accordion--surface{background-color:var(--surface);border-radius:min(32px,var(--radius-3xl))}@media(hover:hover){.accordion--surface .accordion__trigger:hover:not([aria-expanded=true]){background-color:var(--default)}}.accordion--surface .accordion__item:after{background-color:var(--surface-foreground)}@supports (color:color-mix(in lab,red,red)){.accordion--surface .accordion__item:after{background-color:color-mix(in oklab,var(--surface-foreground) 6%,transparent)}}.accordion--surface .accordion__item:after{width:94%;inset-inline-start:3%}.accordion--surface .accordion__item:first-child [data-slot=accordion-trigger]{border-start-start-radius:min(32px,var(--radius-3xl));border-start-end-radius:min(32px,var(--radius-3xl))}.accordion--surface .accordion__item:last-child:not(:has([data-slot=accordion-trigger][aria-expanded=true])) [data-slot=accordion-trigger]{border-end-end-radius:min(32px,var(--radius-3xl));border-end-start-radius:min(32px,var(--radius-3xl))}.breadcrumbs{align-items:center;gap:calc(var(--spacing) * 1.5);display:flex}.breadcrumbs .breadcrumbs__link{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted);opacity:1;text-decoration-line:none;position:relative}.breadcrumbs .breadcrumbs__link:hover,.breadcrumbs .breadcrumbs__link[data-hovered=true]{text-decoration-line:underline}.breadcrumbs .breadcrumbs__link[data-current=true]{color:var(--link);opacity:1}.breadcrumbs .breadcrumbs__item{justify-content:center;align-items:center;gap:var(--spacing);flex-shrink:0;display:flex}.breadcrumbs .breadcrumbs__separator{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3);color:var(--muted)}.breadcrumbs .breadcrumbs__separator:where(:dir(rtl),[dir=rtl],[dir=rtl] *){rotate:180deg}.disclosure-group{contain:layout style;width:100%}.disclosure{position:relative}.accordion__heading{display:flex}.disclosure__trigger{cursor:var(--cursor-interactive);-webkit-tap-highlight-color:transparent;display:inline-block}.disclosure__trigger:focus-visible:not([data-focused]),.disclosure__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.disclosure__trigger:disabled,.disclosure__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.disclosure__indicator{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);color:inherit;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.25s;flex-shrink:0;margin-inline-start:auto;transition-duration:.25s}.disclosure__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.disclosure__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.disclosure__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.disclosure__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.disclosure__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.disclosure__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.disclosure__indicator[data-expanded=true]{rotate:-180deg}.disclosure__content{opacity:0;height:var(--disclosure-panel-height);transition:height .2s var(--ease-out-quad),opacity .2s var(--ease-out);overflow:clip}.disclosure__content:is([data-reduce-motion=true],[data-reduce-motion=true] *),.disclosure__content:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.disclosure__content:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.disclosure__content:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.disclosure__content:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.disclosure__content:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.disclosure__content[data-expanded=true]{will-change:height,opacity;opacity:1}.disclosure__body{padding:calc(var(--spacing) * 2)}.link{border-radius:calc(var(--radius) * 1.5);--tw-font-weight:var(--font-weight-medium);width:fit-content;height:fit-content;font-weight:var(--font-weight-medium);color:var(--link);text-decoration-line:none;-webkit-text-decoration-color:var(--separator-tertiary);text-decoration-color:var(--separator-tertiary);text-underline-offset:4px;-webkit-tap-highlight-color:transparent;transition:color .1s var(--ease-smooth),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out),opacity .1s var(--ease-out);align-items:center;text-decoration-thickness:1.5px;display:inline-flex;position:relative}.link:is([data-reduce-motion=true],[data-reduce-motion=true] *),.link:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.link:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.link:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.link:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.link:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.link{cursor:var(--cursor-interactive)}@media(hover:hover){.link:hover,.link[data-hovered=true]{text-decoration-line:underline;-webkit-text-decoration-color:var(--muted);text-decoration-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.link:hover,.link[data-hovered=true]{-webkit-text-decoration-color:color-mix(in oklab,var(--muted) 50%,transparent);text-decoration-color:color-mix(in oklab,var(--muted) 50%,transparent)}}:is(.link:hover,.link[data-hovered=true]) .link__icon{opacity:1}}.link:active,.link[data-pressed=true]{text-decoration-line:underline;-webkit-text-decoration-color:var(--muted);text-decoration-color:var(--muted)}:is(.link:active,.link[data-pressed=true]) .link__icon{opacity:1}.link:focus-visible:not([data-focused]),.link[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}:is(.link:focus-visible:not([data-focused]),.link[data-focus-visible=true]) .link__icon{opacity:1}.link[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.link .link__icon{pointer-events:none;color:currentColor;opacity:.6;width:.75em;height:.75em;transition:opacity .15s var(--ease-out);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.link .link__icon:is([data-reduce-motion=true],[data-reduce-motion=true] *),.link .link__icon:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.link .link__icon:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.link .link__icon:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.link .link__icon:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.link .link__icon:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.link .link__icon svg{transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.link .link__icon[data-default-icon=true]{padding-bottom:calc(var(--spacing) * 1.5);margin-inline-start:var(--spacing)}.link.button{gap:0;text-decoration-line:none}.pagination{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 4);flex-direction:column;width:100%;display:flex}@media(min-width:40rem){.pagination{flex-direction:row}}.pagination__summary{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted);align-self:flex-start;display:flex}@media(min-width:40rem){.pagination__summary{align-self:center}}.pagination__content{align-items:center;gap:var(--spacing);align-self:flex-start;display:flex}@media(min-width:40rem){.pagination__content{align-self:center}}.pagination__item{display:inline-flex}.pagination__link{isolation:isolate;width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9);transform-origin:50%;border-radius:calc(var(--radius) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline-style:none;justify-content:center;align-items:center;display:inline-flex;position:relative}@media(min-width:48rem){.pagination__link{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}}.pagination__link{cursor:var(--cursor-interactive);transition:transform .25s var(--ease-smooth),background-color .1s var(--ease-out),box-shadow .1s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.pagination__link:is([data-reduce-motion=true],[data-reduce-motion=true] *),.pagination__link:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.pagination__link:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.pagination__link:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.pagination__link:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.pagination__link:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.pagination__link{--pagination-link-bg:transparent;--pagination-link-bg-hover:var(--default-hover);--pagination-link-bg-pressed:var(--default-hover);--pagination-link-fg:var(--default-foreground);background-color:var(--pagination-link-bg);color:var(--pagination-link-fg)}.pagination__link:focus-visible,.pagination__link[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.pagination__link:disabled,.pagination__link[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}@media(hover:hover){.pagination__link:hover,.pagination__link[data-hovered=true]{background-color:var(--pagination-link-bg-hover)}}.pagination__link:active,.pagination__link[data-pressed=true]{background-color:var(--pagination-link-bg-pressed);transform:scale(.97)}.pagination__link[data-active=true]{--pagination-link-bg:var(--default);--pagination-link-bg-hover:var(--default-hover);--pagination-link-bg-pressed:var(--default-hover)}.pagination__ellipsis{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted);-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;display:inline-flex}@media(min-width:48rem){.pagination__ellipsis{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}}.pagination__link--nav{gap:calc(var(--spacing) * 1.5);width:auto;padding-inline:calc(var(--spacing) * 2.5)}:is(.pagination__link--nav [data-slot=pagination-previous-icon],.pagination__link--nav [data-slot=pagination-next-icon]):where(:dir(rtl),[dir=rtl],[dir=rtl] *){rotate:180deg}.pagination--sm .pagination__link{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}@media(min-width:48rem){.pagination--sm .pagination__link{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}}.pagination--sm .pagination__link:active,.pagination--sm .pagination__link[data-pressed=true]{transform:scale(.98)}.pagination--sm .pagination__link--nav{width:auto;padding-inline:calc(var(--spacing) * 2)}.pagination--sm .pagination__ellipsis{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}@media(min-width:48rem){.pagination--sm .pagination__ellipsis{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}}.pagination--sm .pagination__summary{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.pagination--lg .pagination__link{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}@media(min-width:48rem){.pagination--lg .pagination__link{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}}.pagination--lg .pagination__link:active,.pagination--lg .pagination__link[data-pressed=true]{transform:scale(.96)}.pagination--lg .pagination__link--nav{width:auto;padding-inline:calc(var(--spacing) * 3)}.pagination--lg .pagination__ellipsis{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}@media(min-width:48rem){.pagination--lg .pagination__ellipsis{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}}.pagination--lg .pagination__summary{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.tabs{gap:calc(var(--spacing) * 2);display:flex}.tabs[data-orientation=horizontal]{flex-direction:column}.tabs[data-orientation=vertical]{flex-direction:row}.tabs__list-container{background-color:var(--default);border-radius:calc(var(--radius) * 2.5);position:relative}.tabs__list-container>.tabs__list-container__scroller[data-orientation=vertical]{height:100%}.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next{z-index:2;width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);cursor:var(--cursor-interactive);border-style:var(--tw-border-style);color:var(--foreground);--tw-outline-style:none;transition:opacity .15s var(--ease-smooth);background-color:#0000;border-width:0;border-radius:3.40282e38px;outline-style:none;justify-content:center;align-items:center;padding:0;display:none;position:absolute}:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):hover{opacity:.7}}:is(.tabs__list-container>.tabs__list-container__scroll-prev,.tabs__list-container>.tabs__list-container__scroll-next):focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.tabs__list-container:has([data-orientation=horizontal])>.tabs__list-container__scroll-prev{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);inset-inline-start:calc(var(--spacing) * 1);top:50%}.tabs__list-container:has([data-orientation=horizontal])>.tabs__list-container__scroll-next{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);inset-inline-end:calc(var(--spacing) * 1);top:50%}:is(.tabs__list-container:has([data-orientation=horizontal])>.tabs__list-container__scroll-prev,.tabs__list-container:has([data-orientation=horizontal])>.tabs__list-container__scroll-next):where(:dir(rtl),[dir=rtl],[dir=rtl] *){rotate:180deg}.tabs__list-container:has([data-orientation=vertical])>.tabs__list-container__scroll-prev{top:var(--spacing);--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);left:50%}.tabs__list-container:has([data-orientation=vertical])>.tabs__list-container__scroll-next{bottom:var(--spacing);--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);left:50%}.tabs__list-container:has(>:is([data-left-scroll=true],[data-left-right-scroll=true],[data-top-scroll=true],[data-top-bottom-scroll=true]))>.tabs__list-container__scroll-prev,.tabs__list-container:has(>:is([data-right-scroll=true],[data-left-right-scroll=true],[data-bottom-scroll=true],[data-top-bottom-scroll=true]))>.tabs__list-container__scroll-next{display:inline-flex}.tabs__list{padding:var(--spacing);display:inline-flex}.tabs__list[data-orientation=horizontal]{flex-direction:row;width:max-content;min-width:100%}.tabs__list[data-orientation=vertical]{gap:var(--spacing);flex-direction:column}.tabs__list[data-orientation=vertical] .tabs__tab{min-width:calc(var(--spacing) * 20)}.tabs__tab{height:calc(var(--spacing) * 8);border-radius:calc(var(--radius) * 3);width:100%;padding-inline:calc(var(--spacing) * 4);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;z-index:1;cursor:var(--cursor-interactive);transition:color .15s var(--ease-smooth),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out),opacity .15s var(--ease-smooth);outline-style:none;justify-content:center;align-items:center;display:flex;position:relative}.tabs__tab:is([data-reduce-motion=true],[data-reduce-motion=true] *),.tabs__tab:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.tabs__tab:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.tabs__tab:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.tabs__tab:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.tabs__tab:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.tabs__tab[data-selected=true]{color:var(--segment-foreground)}.tabs__tab[data-selected=true] .tabs__separator,.tabs__tab[data-selected=true]+.tabs__tab .tabs__separator{opacity:0}.tabs__tab:disabled,.tabs__tab[data-disabled=true],.tabs__tab[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}@media(hover:hover){.tabs__tab:not([data-selected=true]):not([data-disabled=true]):hover,.tabs__tab[data-hovered=true]:not([data-selected=true]):not([data-disabled=true]){opacity:.7}}.tabs__tab:focus-visible:not([data-focused]),.tabs__tab[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.tabs--align-start>:is(.tabs__list-container,.tabs__list) .tabs__tab{text-align:start;justify-content:flex-start}.tabs--align-end>:is(.tabs__list-container,.tabs__list) .tabs__tab{text-align:end;justify-content:flex-end}.tabs__separator{pointer-events:none;border-radius:calc(var(--radius) * .5);background-color:var(--muted);position:absolute}@supports (color:color-mix(in lab,red,red)){.tabs__separator{background-color:color-mix(in oklab,var(--muted) 25%,transparent)}}.tabs__separator{transition:opacity .15s var(--ease-smooth)}.tabs__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.tabs__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.tabs__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.tabs__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.tabs__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.tabs__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.tabs__list[data-orientation=horizontal] .tabs__separator{inset-inline-start:calc(var(--spacing) * 0);width:1px;height:50%;top:25%}.tabs__list[data-orientation=vertical] .tabs__separator{inset-inline-start:5%;width:90%;height:1px;top:0}.tabs__panel{width:100%;padding:calc(var(--spacing) * 2);--tw-outline-style:none;outline-style:none}.tabs__panel[data-exiting=true]{inset-inline-start:calc(var(--spacing) * 0);width:100%;position:absolute;top:0}.tabs__panel[data-orientation=horizontal]{margin-top:calc(var(--spacing) * 4)}.tabs__panel[data-orientation=vertical]{margin-inline-start:calc(var(--spacing) * 4)}.tabs__indicator{border-radius:calc(var(--radius) * 3);background-color:var(--segment);--tw-shadow:var(--surface-shadow);width:100%;height:100%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);z-index:-1;transition-property:translate,width,height;transition-duration:.25s;transition-timing-function:var(--ease-out-fluid);inset-inline-start:calc(var(--spacing) * 0);position:absolute;top:0}.tabs__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.tabs__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.tabs__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.tabs__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.tabs__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.tabs__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.tabs--secondary>.tabs__list-container{background-color:#0000;border-radius:0}.tabs--secondary>.tabs__list-container .tabs__list{padding:0}.tabs--secondary>.tabs__list-container .tabs__separator{display:none}.tabs--secondary>.tabs__list-container .tabs__tab{border-radius:0}.tabs--secondary>.tabs__list-container .tabs__tab[data-selected=true]{color:var(--foreground)}.tabs--secondary>.tabs__list-container .tabs__indicator{background-color:var(--accent);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:0}.tabs--secondary[data-orientation=horizontal]>.tabs__list-container{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border)}.tabs--secondary[data-orientation=horizontal]>.tabs__list-container .tabs__indicator{height:calc(var(--spacing) * .5);top:auto;bottom:0}.tabs--secondary[data-orientation=vertical]>.tabs__list-container{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px;border-color:var(--border)}.tabs--secondary[data-orientation=vertical]>.tabs__list-container .tabs__indicator{height:100%;width:calc(var(--spacing) * .5);inset-inline-start:calc(var(--spacing) * 0);top:0}.button{isolation:isolate;height:calc(var(--spacing) * 10);transform-origin:50%;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:calc(var(--radius) * 3);width:fit-content;padding-inline:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline-style:none;display:inline-flex;position:relative}@media(min-width:48rem){.button{height:calc(var(--spacing) * 9)}}.button{transition:transform .25s var(--ease-smooth),background-color .1s var(--ease-out),box-shadow .1s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.button:is([data-reduce-motion=true],[data-reduce-motion=true] *),.button:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.button:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.button{cursor:var(--cursor-interactive);--button-bg:transparent;--button-bg-hover:var(--button-bg);--button-bg-pressed:var(--button-bg-hover);--button-fg:currentColor;background-color:var(--button-bg);color:var(--button-fg)}.button:focus-visible:not([data-focused]),.button[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.button:disabled,.button[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.button[data-pending=true]{pointer-events:none}.button:active,.button[data-pressed=true]{will-change:transform;background-color:var(--button-bg-pressed);transform:scale(.97)}@media(hover:hover){.button:hover,.button[data-hovered=true]{background-color:var(--button-bg-hover)}}.button svg:not([data-slot=spinner] svg,[data-slot=link-icon] svg){pointer-events:none;margin-inline:calc(var(--spacing) * -.5);margin-block:calc(var(--spacing) * .5);width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5);flex-shrink:0;align-self:center}@media(min-width:40rem){.button svg:not([data-slot=spinner] svg,[data-slot=link-icon] svg){margin-block:var(--spacing);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}}.button--sm{height:calc(var(--spacing) * 9);padding-inline:calc(var(--spacing) * 3)}@media(min-width:48rem){.button--sm{height:calc(var(--spacing) * 8)}}.button--sm svg:not([data-slot=spinner] svg,[data-slot=link-icon] svg){width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.button--sm:active,.button--sm[data-pressed=true]{transform:scale(.98)}.button--lg{height:calc(var(--spacing) * 11);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}@media(min-width:48rem){.button--lg{height:calc(var(--spacing) * 10)}}.button--lg:active,.button--lg[data-pressed=true]{transform:scale(.96)}.button--primary{--button-bg:var(--accent);--button-bg-hover:var(--accent-hover);--button-bg-pressed:var(--accent-hover);--button-fg:var(--accent-foreground)}.button--secondary{--button-bg:var(--default);--button-bg-hover:var(--default-hover);--button-bg-pressed:var(--default-hover);--button-fg:var(--accent-soft-foreground)}.button--tertiary{--button-bg:var(--default);--button-bg-hover:var(--default-hover);--button-bg-pressed:var(--default-hover)}.button--ghost,.button--outline{--button-bg:transparent;--button-bg-hover:var(--default);--button-bg-pressed:var(--default);--button-fg:var(--default-foreground)}.button--outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);--button-bg-hover:var(--default)}@supports (color:color-mix(in lab,red,red)){.button--outline{--button-bg-hover:color-mix(in srgb, var(--default) 60%, transparent)}}.button--danger{--button-bg:var(--danger);--button-bg-hover:var(--danger-hover);--button-bg-pressed:var(--danger-hover);--button-fg:var(--danger-foreground)}.button--danger-soft{--button-bg:var(--danger-soft);--button-bg-hover:var(--danger-soft-hover);--button-bg-pressed:var(--danger-soft-hover);--button-fg:var(--danger-soft-foreground)}.button--icon-only{width:calc(var(--spacing) * 10);padding:0}@media(min-width:48rem){.button--icon-only{width:calc(var(--spacing) * 9)}}.button--icon-only.button--sm{width:calc(var(--spacing) * 9)}@media(min-width:48rem){.button--icon-only.button--sm{width:calc(var(--spacing) * 8)}}.button--icon-only.button--lg{width:calc(var(--spacing) * 11)}@media(min-width:48rem){.button--icon-only.button--lg{width:calc(var(--spacing) * 10)}}.button--full-width{width:100%}.button-group{justify-content:center;align-items:center;gap:0;height:auto;display:inline-flex}.button-group--horizontal{flex-direction:row}.button-group--vertical{flex-direction:column}.button-group .button{border-radius:0}.button-group--horizontal .button:first-child{border-start-start-radius:calc(var(--radius) * 3);border-end-start-radius:calc(var(--radius) * 3)}.button-group--horizontal .button:last-child{border-start-end-radius:calc(var(--radius) * 3);border-end-end-radius:calc(var(--radius) * 3)}.button-group--horizontal .button:first-child:last-child{border-radius:calc(var(--radius) * 3)}.button-group--vertical .button:first-child{border-top-left-radius:calc(var(--radius) * 3);border-top-right-radius:calc(var(--radius) * 3)}.button-group--vertical .button:last-child{border-bottom-right-radius:calc(var(--radius) * 3);border-bottom-left-radius:calc(var(--radius) * 3)}.button-group--vertical .button:first-child:last-child{border-radius:calc(var(--radius) * 3)}.button-group .button:active,.button-group .button[data-pressed=true]{transform:none}.button-group .button:focus-visible:not([data-focused]),.button-group .button[data-focus-visible=true]{z-index:10}.button-group__separator{border-radius:calc(var(--radius) * .5);opacity:.15;pointer-events:none;transition:opacity .15s var(--ease-smooth);background-color:currentColor;position:absolute}.button-group__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.button-group__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.button-group__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.button-group__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.button-group__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.button-group__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.button-group--horizontal .button-group__separator{inset-inline-start:-1px;width:1px;height:50%;top:25%}.button-group--vertical .button-group__separator{inset-inline-start:25%;width:50%;height:1px;top:-1px}.button-group--horizontal .button--outline:first-child{border-inline-end-style:var(--tw-border-style);border-inline-end-width:0}.button-group--horizontal .button--outline:last-child{border-inline-start-style:var(--tw-border-style);border-inline-start-width:0}.button-group--horizontal .button--outline:not(:first-child):not(:last-child){border-inline-style:var(--tw-border-style);border-inline-width:0}.button-group--vertical .button--outline:first-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.button-group--vertical .button--outline:last-child{border-top-style:var(--tw-border-style);border-top-width:0}.button-group--vertical .button--outline:not(:first-child):not(:last-child){border-block-style:var(--tw-border-style);border-block-width:0}.button-group--full-width{width:100%}.toggle-button{isolation:isolate;height:calc(var(--spacing) * 10);transform-origin:50%;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:calc(var(--radius) * 3);width:fit-content;padding-inline:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline-style:none;display:inline-flex;position:relative}@media(min-width:48rem){.toggle-button{height:calc(var(--spacing) * 9)}}.toggle-button{transition:transform .25s var(--ease-smooth),background-color .1s var(--ease-out),box-shadow .1s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.toggle-button:is([data-reduce-motion=true],[data-reduce-motion=true] *),.toggle-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.toggle-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.toggle-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.toggle-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.toggle-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.toggle-button{cursor:var(--cursor-interactive);--toggle-button-bg:var(--default);--toggle-button-bg-hover:var(--default-hover);--toggle-button-bg-pressed:var(--default-hover);--toggle-button-fg:currentColor;--toggle-button-bg-selected:var(--accent-soft);--toggle-button-bg-selected-hover:var(--accent-soft-hover);--toggle-button-bg-selected-pressed:var(--accent-soft-hover);--toggle-button-fg-selected:var(--accent-soft-foreground);background-color:var(--toggle-button-bg);color:var(--toggle-button-fg)}.toggle-button:focus-visible:not([data-focused]),.toggle-button[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.toggle-button:disabled,.toggle-button[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}@media(hover:hover){.toggle-button:hover,.toggle-button[data-hovered=true]{background-color:var(--toggle-button-bg-hover)}}.toggle-button:active,.toggle-button[data-pressed=true]{background-color:var(--toggle-button-bg-pressed);transform:scale(.97)}.toggle-button[data-selected=true]{background-color:var(--toggle-button-bg-selected);color:var(--toggle-button-fg-selected)}@media(hover:hover){.toggle-button[data-selected=true]:hover,.toggle-button[data-selected=true][data-hovered=true]{background-color:var(--toggle-button-bg-selected-hover)}}.toggle-button[data-selected=true]:active,.toggle-button[data-selected=true][data-pressed=true]{background-color:var(--toggle-button-bg-selected-pressed)}.toggle-button svg{pointer-events:none;margin-inline:calc(var(--spacing) * -.5);margin-block:calc(var(--spacing) * .5);width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5);flex-shrink:0;align-self:center}@media(min-width:40rem){.toggle-button svg{margin-block:var(--spacing);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}}.toggle-button--sm{height:calc(var(--spacing) * 9);padding-inline:calc(var(--spacing) * 3)}@media(min-width:48rem){.toggle-button--sm{height:calc(var(--spacing) * 8)}}.toggle-button--sm svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.toggle-button--sm:active,.toggle-button--sm[data-pressed=true]{transform:scale(.98)}.toggle-button--lg{height:calc(var(--spacing) * 11);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}@media(min-width:48rem){.toggle-button--lg{height:calc(var(--spacing) * 10)}}.toggle-button--lg:active,.toggle-button--lg[data-pressed=true]{transform:scale(.96)}.toggle-button--default{--toggle-button-bg:var(--default);--toggle-button-bg-hover:var(--default-hover);--toggle-button-bg-pressed:var(--default-hover)}.toggle-button--ghost{--toggle-button-bg:transparent;--toggle-button-bg-hover:var(--default);--toggle-button-bg-pressed:var(--default);--toggle-button-fg:var(--default-foreground)}.toggle-button--icon-only{width:calc(var(--spacing) * 10);padding:0}@media(min-width:48rem){.toggle-button--icon-only{width:calc(var(--spacing) * 9)}}.toggle-button--icon-only.toggle-button--sm{width:calc(var(--spacing) * 9)}@media(min-width:48rem){.toggle-button--icon-only.toggle-button--sm{width:calc(var(--spacing) * 8)}}.toggle-button--icon-only.toggle-button--lg{width:calc(var(--spacing) * 11)}@media(min-width:48rem){.toggle-button--icon-only.toggle-button--lg{width:calc(var(--spacing) * 10)}}.toggle-button-group{justify-content:center;align-items:center;gap:0;width:fit-content;height:auto;display:inline-flex}.toggle-button-group--horizontal{flex-direction:row}.toggle-button-group--vertical{flex-direction:column}.toggle-button-group--full-width{width:100%}.toggle-button-group .toggle-button{border-radius:0}.toggle-button-group--horizontal .toggle-button:first-child{border-start-start-radius:calc(var(--radius) * 3);border-end-start-radius:calc(var(--radius) * 3)}.toggle-button-group--horizontal .toggle-button:last-child{border-start-end-radius:calc(var(--radius) * 3);border-end-end-radius:calc(var(--radius) * 3)}.toggle-button-group--horizontal .toggle-button:first-child:last-child{border-radius:calc(var(--radius) * 3)}.toggle-button-group--vertical .toggle-button:first-child{border-top-left-radius:calc(var(--radius) * 3);border-top-right-radius:calc(var(--radius) * 3)}.toggle-button-group--vertical .toggle-button:last-child{border-bottom-right-radius:calc(var(--radius) * 3);border-bottom-left-radius:calc(var(--radius) * 3)}.toggle-button-group--vertical .toggle-button:first-child:last-child{border-radius:calc(var(--radius) * 3)}.toggle-button-group .toggle-button:active,.toggle-button-group .toggle-button[data-pressed=true]{transform:none}.toggle-button-group .toggle-button:focus-visible:not([data-focused]),.toggle-button-group .toggle-button[data-focus-visible=true]{--tw-ring-offset-width:0px;--tw-ring-inset:inset}.toggle-button-group--full-width .toggle-button{flex:1}.toggle-button-group__separator{border-radius:calc(var(--radius) * .5);opacity:.15;pointer-events:none;transition:opacity .15s var(--ease-smooth);background-color:currentColor;position:absolute}.toggle-button-group__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.toggle-button-group__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.toggle-button-group__separator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.toggle-button-group__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.toggle-button-group__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.toggle-button-group__separator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.toggle-button-group--horizontal .toggle-button-group__separator{inset-inline-start:-1px;width:1px;height:50%;top:25%}.toggle-button-group--vertical .toggle-button-group__separator{inset-inline-start:25%;width:50%;height:1px;top:-1px}.toggle-button-group--detached{gap:var(--spacing)}.toggle-button-group--detached .toggle-button{border-radius:calc(var(--radius) * 3)}.toggle-button-group--detached .toggle-button-group__separator{display:none}.toolbar{align-items:center;gap:calc(var(--spacing) * 2);grid-auto-flow:column;width:fit-content;display:grid}.toolbar .separator--vertical{align-self:center;height:50%}.toolbar .separator--horizontal{justify-content:center;justify-self:center;width:50%}.toolbar--vertical{grid-auto-flow:row;justify-content:flex-start;align-items:flex-start}.toolbar--vertical .button-group{justify-content:flex-start}.toolbar--attached{border-radius:calc(var(--radius) * 3);background-color:var(--surface);padding:var(--spacing);--tw-shadow:var(--overlay-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dropdown{gap:var(--spacing);flex-direction:column;display:flex}.dropdown__trigger{--tw-outline-style:none;transition:transform .25s var(--ease-out-quart),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);outline-style:none;display:inline-block}.dropdown__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.dropdown__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.dropdown__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.dropdown__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.dropdown__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.dropdown__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.dropdown__trigger{cursor:var(--cursor-interactive)}.dropdown__trigger:focus-visible:not([data-focused]),.dropdown__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.dropdown__trigger:disabled,.dropdown__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.dropdown__trigger[data-pending=true]{pointer-events:none}.dropdown__trigger:active,.dropdown__trigger[data-pressed=true]{transform:scale(.97)}.dropdown__popover{max-width:48svw;transform-origin:var(--trigger-anchor-point);scroll-padding-block:var(--spacing);scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);overscroll-behavior:contain;background-color:var(--overlay);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));padding:0;overflow-y:auto}@media(min-width:48rem){.dropdown__popover{min-width:calc(var(--spacing) * 55)}}.dropdown__popover{border-radius:min(32px,var(--radius-3xl));box-shadow:var(--overlay-shadow)}.dropdown__popover:focus-visible:not([data-focused]),.dropdown__popover[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.dropdown__popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.9;transition-duration:.15s;transition-timing-function:ease}.dropdown__popover[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.dropdown__popover[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.dropdown__popover[data-entering=true][data-placement=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.dropdown__popover[data-entering=true][data-placement=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.dropdown__popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.dropdown__popover[data-exiting=true],.dropdown__popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.dropdown__popover [data-slot=popover-overlay-arrow]{fill:var(--overlay)}.dropdown__popover[data-placement=bottom] [data-slot=popover-overlay-arrow]{rotate:180deg}.dropdown__popover[data-placement=left] [data-slot=popover-overlay-arrow]{rotate:-90deg}.dropdown__popover[data-placement=right] [data-slot=popover-overlay-arrow]{rotate:90deg}.dropdown__popover [data-slot=dropdown-menu]{padding:calc(var(--spacing) * 1.5);--tw-outline-style:none;outline-style:none}.dropdown__popover [data-slot=menu-item]{padding-inline:calc(var(--spacing) * 2.5)}.dropdown__menu{gap:calc(var(--spacing) * .5);width:100%;padding:var(--spacing);flex-direction:column;display:flex;position:relative;overflow:clip}.dropdown__menu [data-slot=separator]{width:94%;margin-inline-start:3%}.list-box-item{min-height:calc(var(--spacing) * 9);justify-content:flex-start;align-items:center;gap:calc(var(--spacing) * 3);border-radius:calc(var(--radius) * 2);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;transition:transform .25s var(--ease-out-quart),box-shadow .15s var(--ease-out);outline-style:none;display:flex;position:relative}.list-box-item:is([data-reduce-motion=true],[data-reduce-motion=true] *),.list-box-item:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.list-box-item:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.list-box-item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.list-box-item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.list-box-item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.list-box-item{cursor:var(--cursor-interactive)}.list-box-item [data-slot=label]{pointer-events:none;-webkit-user-select:none;user-select:none;width:fit-content}.list-box-item [data-slot=description]{pointer-events:none;text-wrap:wrap;-webkit-user-select:none;user-select:none}.list-box-item:has(.list-box-item__indicator){padding-inline-end:calc(var(--spacing) * 7)}.list-box-item:focus-visible:not([data-focused]),.list-box-item[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.list-box-item:active,.list-box-item[data-pressed=true]{transform:scale(.98)}@media(hover:hover){.list-box-item:hover,.list-box-item[data-hovered=true]{background-color:var(--default)}}.list-box-item[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.list-box-item__indicator{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);color:var(--default-foreground);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.25s;inset-inline-end:calc(var(--spacing) * 2);flex-shrink:0;justify-content:center;align-items:center;transition-duration:.25s;display:flex;position:absolute;top:50%}.list-box-item__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.list-box-item__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.list-box-item__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.list-box-item__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.list-box-item__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.list-box-item__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.list-box-item__indicator [data-slot=list-box-item-indicator--checkmark]{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;transition-duration:.3s}.list-box-item__indicator [data-slot=list-box-item-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *),.list-box-item__indicator [data-slot=list-box-item-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.list-box-item__indicator [data-slot=list-box-item-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.list-box-item__indicator [data-slot=list-box-item-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.list-box-item__indicator [data-slot=list-box-item-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.list-box-item__indicator [data-slot=list-box-item-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.list-box-item__indicator .list-box-item[aria-selected=true] [data-slot=list-box-item-indicator--checkmark],.list-box-item__indicator .list-box-item[data-selected=true] [data-slot=list-box-item-indicator--checkmark]{transition:stroke-dashoffset .25s linear}:is(.list-box-item__indicator .list-box-item[aria-selected=true] [data-slot=list-box-item-indicator--checkmark],.list-box-item__indicator .list-box-item[data-selected=true] [data-slot=list-box-item-indicator--checkmark]):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.list-box-item__indicator .list-box-item[aria-selected=true] [data-slot=list-box-item-indicator--checkmark],.list-box-item__indicator .list-box-item[data-selected=true] [data-slot=list-box-item-indicator--checkmark]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.list-box-item__indicator .list-box-item[aria-selected=true] [data-slot=list-box-item-indicator--checkmark],.list-box-item__indicator .list-box-item[data-selected=true] [data-slot=list-box-item-indicator--checkmark]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){:is(.list-box-item__indicator .list-box-item[aria-selected=true] [data-slot=list-box-item-indicator--checkmark],.list-box-item__indicator .list-box-item[data-selected=true] [data-slot=list-box-item-indicator--checkmark]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.list-box-item__indicator .list-box-item[aria-selected=true] [data-slot=list-box-item-indicator--checkmark],.list-box-item__indicator .list-box-item[data-selected=true] [data-slot=list-box-item-indicator--checkmark]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.list-box-item__indicator .list-box-item[aria-selected=true] [data-slot=list-box-item-indicator--checkmark],.list-box-item__indicator .list-box-item[data-selected=true] [data-slot=list-box-item-indicator--checkmark]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.list-box-item--danger .list-box-item__indicator,.list-box-item--danger [data-slot=label]{color:var(--danger)}.list-box-section{flex-direction:column;align-items:flex-start;gap:0;display:flex}.list-box{width:100%;padding:var(--spacing);position:relative;overflow:clip}.list-box>*+*{margin-top:var(--spacing)}.list-box [data-slot=separator][data-orientation=horizontal]{width:94%;margin-inline-start:3%}.menu-item{min-height:calc(var(--spacing) * 9);justify-content:flex-start;align-items:center;gap:calc(var(--spacing) * 3);border-radius:calc(var(--radius) * 2);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;transition:transform .25s var(--ease-out-quart),box-shadow .15s var(--ease-out);outline-style:none;display:flex;position:relative}.menu-item:is([data-reduce-motion=true],[data-reduce-motion=true] *),.menu-item:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.menu-item:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.menu-item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.menu-item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.menu-item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.menu-item{cursor:var(--cursor-interactive)}.menu-item [data-slot=label]{pointer-events:none;-webkit-user-select:none;user-select:none;width:fit-content}.menu-item [data-slot=description]{pointer-events:none;text-wrap:wrap;-webkit-user-select:none;user-select:none}.menu-item [data-slot=submenu-indicator] svg{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.menu-item:has(.menu-item__indicator){padding-inline-start:calc(var(--spacing) * 7)}.menu-item[data-has-submenu=true]:has(.menu-item__indicator){padding-inline-start:calc(var(--spacing) * 2);padding-inline-end:calc(var(--spacing) * 7)}.menu-item:focus-visible:not([data-focused]),.menu-item[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.menu-item:active,.menu-item[data-pressed=true]{will-change:transform;transform:scale(.98)}@media(hover:hover){.menu-item:hover,.menu-item[data-hovered=true]{background-color:var(--default)}}:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--checkmark]{transition:stroke-dashoffset .1s linear}:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}:is(.menu-item[aria-checked=true],.menu-item[aria-selected=true],.menu-item[data-selected=true]) [data-slot=menu-item-indicator--dot]{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:1}.menu-item[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.menu-item__indicator{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);color:var(--muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.25s;inset-inline-start:calc(var(--spacing) * 2);flex-shrink:0;justify-content:center;align-items:center;transition-duration:.25s;display:flex;position:absolute;top:50%}.menu-item__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.menu-item__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.menu-item__indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.menu-item__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.menu-item__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.menu-item__indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.menu-item[data-has-submenu=true] .menu-item__indicator{inset-inline-start:auto;inset-inline-end:calc(var(--spacing) * 2)}.menu-item__indicator [data-slot=menu-item-indicator--checkmark]{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--checkmark]){transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;transition-duration:.3s}.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--checkmark]):is([data-reduce-motion=true],[data-reduce-motion=true] *),.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--checkmark]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--checkmark]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--checkmark]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--checkmark]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--checkmark]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.menu-item__indicator [data-slot=menu-item-indicator--dot]{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--dot]){transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.25s;transition-duration:.25s}.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--dot]):is([data-reduce-motion=true],[data-reduce-motion=true] *),.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--dot]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--dot]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--dot]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--dot]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.menu-item[data-selection-mode=multiple] :is(.menu-item__indicator [data-slot=menu-item-indicator--dot]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.menu-item__indicator [data-slot=menu-item-indicator--dot]{--tw-scale-x:70%;--tw-scale-y:70%;--tw-scale-z:70%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:0}.menu-item__indicator--submenu{color:var(--muted)}.menu-item__indicator--submenu svg{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.menu-item--danger .menu-item__indicator,.menu-item--danger [data-slot=label]{color:var(--danger)}.menu-section{flex-direction:column;align-items:flex-start;gap:0;display:flex}.menu{gap:var(--spacing);width:100%;padding:var(--spacing);flex-direction:column;display:flex;position:relative;overflow:clip}.menu [data-slot=separator]{width:94%;margin-inline-start:3%}.tag-group{gap:var(--spacing);flex-direction:column;display:flex;position:relative}.tag-group__list{gap:calc(var(--spacing) * 1.5);flex-wrap:wrap;display:flex;position:relative}.tag-group [slot=description],.tag-group [data-slot=description],.tag-group [slot=errorMessage],.tag-group [data-slot=error-message]{padding:var(--spacing)}.tag{--optical-offset:.031em;align-items:center;gap:var(--spacing);border-radius:calc(var(--radius) * 1.5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));transition:color .1s var(--ease-smooth),scale .1s var(--ease-smooth),opacity .1s var(--ease-smooth),background-color .1s var(--ease-smooth),box-shadow .1s var(--ease-out);transform-origin:50%;transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);display:inline-flex;position:relative}.tag:is([data-reduce-motion=true],[data-reduce-motion=true] *),.tag:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.tag:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.tag:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.tag:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.tag:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.tag{cursor:var(--cursor-interactive)}.tag svg{pointer-events:none;width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3);color:currentColor;flex-shrink:0;align-self:center}.tag:is([data-disabled=true],[aria-disabled=true]){opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.tag:is(:focus-visible,[data-focus-visible]){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.tag:is([data-selected=true],[aria-selected=true]){background-color:var(--accent-soft);color:var(--accent-soft-foreground)}@media(hover:hover){.tag:is([data-selected=true],[aria-selected=true]):is(:hover,[data-hovered=true]){background-color:var(--accent-soft-hover)}}.tag--sm{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.tag--lg{border-radius:calc(var(--radius) * 2);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.tag--default{background-color:var(--default);color:var(--default-foreground)}@media(hover:hover){.tag--default:is(:hover,[data-hovered=true]):not([data-selected=true]):not([data-disabled=true]){background-color:var(--default-hover)}}.tag--surface{background-color:var(--surface);color:var(--surface-foreground)}@media(hover:hover){.tag--surface:is(:hover,[data-hovered=true]):not([data-selected=true]):not([data-disabled=true]){background-color:var(--surface-hover)}}.tag__remove-button:after{content:"";width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6);--tw-translate-x: -50% ;--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);position:absolute;top:50%;left:50%}.tag__remove-button{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3);color:inherit}.tag__remove-button svg{width:inherit;height:inherit;color:currentColor;flex-shrink:0;align-self:center}.color-area{width:100%;max-width:calc(var(--spacing) * 56);border-radius:calc(var(--radius) * 2);-webkit-tap-highlight-color:transparent;aspect-ratio:1;background:var(--color-area-background);flex-shrink:0;position:relative;box-shadow:inset 0 0 0 1px #0000001a}.color-area[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.color-area--show-dots:after{content:"";pointer-events:none;border-radius:inherit;background-image:radial-gradient(circle,#fff3 1px,#0000 1px);background-size:8px 8px;position:absolute;inset:0}.color-area__thumb{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * 1.5);will-change:width,height;background-color:var(--color-area-thumb-color);transition:width .15s var(--ease-out),height .15s var(--ease-out);border:3px solid #fff;box-shadow:0 0 0 1px #0000001a,inset 0 0 0 1px #0000001a}.color-area__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *),.color-area__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.color-area__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.color-area__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.color-area__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.color-area__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.color-area__thumb[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.color-area__thumb[data-dragging=true]{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.color-area__thumb[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.color-picker{display:inline-flex}.color-picker__trigger{align-items:center;gap:calc(var(--spacing) * 3);border-radius:calc(var(--radius) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);transition:background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);display:inline-flex}.color-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.color-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.color-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.color-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.color-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.color-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.color-picker__trigger [data-slot=label]{cursor:var(--cursor-interactive)}.color-picker__trigger:focus-visible:not([data-focused]),.color-picker__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.color-picker__trigger:disabled,.color-picker__trigger[data-disabled=true],.color-picker__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.color-picker__popover{min-width:calc(var(--spacing) * 62);transform-origin:var(--trigger-anchor-point);scrollbar-color:auto;scrollbar-gutter:auto;-ms-overflow-style:none;scrollbar-width:none;overscroll-behavior:contain;background-color:var(--overlay);padding-inline:calc(var(--spacing) * 2);padding-top:calc(var(--spacing) * 2);padding-bottom:calc(var(--spacing) * 3);box-shadow:var(--overlay-shadow);border-radius:min(32px,calc(var(--radius) * 2.5));gap:calc(var(--spacing) * 3);flex-direction:column;display:flex;overflow:hidden auto}.color-picker__popover:focus-visible:not([data-focused]),.color-picker__popover[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.color-picker__popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.95;transition-duration:.15s;transition-timing-function:ease}.color-picker__popover[data-entering=true][data-placement^=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.color-picker__popover[data-entering=true][data-placement^=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.color-picker__popover[data-entering=true][data-placement^=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.color-picker__popover[data-entering=true][data-placement^=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.color-picker__popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.color-picker__popover[data-exiting=true],.color-picker__popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.color-slider{gap:var(--spacing);grid-template:"label output""track track"/1fr auto;width:100%;display:grid}.color-slider:not(:has([data-slot=label])):not(:has(.color-slider__output)){grid-template:"track"/1fr;gap:0}.color-slider:has([data-slot=label]):not(:has(.color-slider__output)){grid-template-columns:1fr;grid-template-areas:"label""track"}.color-slider:not(:has([data-slot=label])):has(.color-slider__output){grid-template-columns:1fr;grid-template-areas:"output""track"}.color-slider:not(:has([data-slot=label])):has(.color-slider__output) .color-slider__output{justify-self:end}.color-slider [data-slot=label]{width:fit-content;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);grid-area:label}.color-slider .color-slider__output{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);grid-area:output}.color-slider .color-slider__track{border-radius:calc(var(--radius) * 2);grid-area:track;position:relative}.color-slider .color-slider__track:before,.color-slider .color-slider__track:after{content:"";z-index:0;pointer-events:none;position:absolute}.color-slider .color-slider__thumb{cursor:grab;border-radius:calc(var(--radius) * 2);-webkit-tap-highlight-color:transparent;border-style:var(--tw-border-style);border-width:3px;border-color:var(--color-white);--tw-shadow:var(--overlay-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);z-index:1;transition:transform .25s var(--ease-out),box-shadow .15s var(--ease-out);justify-content:center;align-items:center;display:flex;position:absolute}.color-slider .color-slider__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *),.color-slider .color-slider__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.color-slider .color-slider__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.color-slider .color-slider__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.color-slider .color-slider__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.color-slider .color-slider__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.color-slider .color-slider__thumb[data-dragging=true]{cursor:grabbing}.color-slider .color-slider__thumb[data-focus-visible=true]{z-index:10;--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.color-slider .color-slider__thumb[data-disabled=true]{cursor:default;background-color:var(--default)}.color-slider:disabled,.color-slider[data-disabled=true],.color-slider[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:is(.color-slider:disabled,.color-slider[data-disabled=true],.color-slider[aria-disabled=true]) [data-slot=label]{opacity:1}.color-slider[data-orientation=horizontal]{flex-direction:column}.color-slider[data-orientation=horizontal] .color-slider__track{height:calc(var(--spacing) * 5);border-radius:0;justify-self:center;width:calc(100% - 1.25rem);box-shadow:inset 0 1px #0000001a,inset 0 -1px #0000001a}.color-slider[data-orientation=horizontal] .color-slider__track:before,.color-slider[data-orientation=horizontal] .color-slider__track:after{width:.625rem;height:100%;top:0}.color-slider[data-orientation=horizontal] .color-slider__track:before{background:linear-gradient(var(--track-start-color,transparent)),repeating-conic-gradient(#efefef 0% 25%,#f7f7f7 0% 50%) 50% / 16px 16px;border-start-start-radius:calc(var(--radius) * 2);border-end-start-radius:calc(var(--radius) * 2);inset-inline-start:-.625rem;box-shadow:inset 1px 0 #0000001a,inset 0 1px #0000001a,inset 0 -1px #0000001a}.color-slider[data-orientation=horizontal] .color-slider__track:after{background-color:var(--track-end-color,transparent);border-start-end-radius:calc(var(--radius) * 2);border-end-end-radius:calc(var(--radius) * 2);inset-inline-end:-.625rem;box-shadow:inset -1px 0 #0000001a,inset 0 1px #0000001a,inset 0 -1px #0000001a}.color-slider[data-orientation=horizontal] .color-slider__thumb{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);top:50%}.color-slider[data-orientation=vertical]{gap:calc(var(--spacing) * 2);flex-direction:row;grid-template:"output""track"1fr"label"/1fr;place-items:center;height:100%}.color-slider[data-orientation=vertical]:not(:has([data-slot=label])):not(:has(.color-slider__output)){grid-template-rows:1fr;grid-template-areas:"track";gap:0}.color-slider[data-orientation=vertical]:has([data-slot=label]):not(:has(.color-slider__output)){grid-template-rows:1fr auto;grid-template-areas:"track""label"}.color-slider[data-orientation=vertical]:not(:has([data-slot=label])):has(.color-slider__output){grid-template-rows:auto 1fr;grid-template-areas:"output""track"}.color-slider[data-orientation=vertical] .color-slider__output,.color-slider[data-orientation=vertical] [data-slot=label]{text-align:center}.color-slider[data-orientation=vertical] .color-slider__track{width:calc(var(--spacing) * 5);border-radius:0;justify-self:center;height:calc(100% - 1.25rem);box-shadow:inset 1px 0 #0000001a,inset -1px 0 #0000001a}.color-slider[data-orientation=vertical] .color-slider__track:before,.color-slider[data-orientation=vertical] .color-slider__track:after{width:100%;height:.625rem;inset-inline-start:calc(var(--spacing) * 0)}.color-slider[data-orientation=vertical] .color-slider__track:before{background:linear-gradient(var(--track-start-color,transparent)),repeating-conic-gradient(#efefef 0% 25%,#f7f7f7 0% 50%) 50% / 16px 16px;border-end-end-radius:999px;border-end-start-radius:999px;bottom:-.625rem;box-shadow:inset 1px 0 #0000001a,inset -1px 0 #0000001a,inset 0 -1px #0000001a}.color-slider[data-orientation=vertical] .color-slider__track:after{background-color:var(--track-end-color,transparent);border-start-start-radius:999px;border-start-end-radius:999px;top:-.625rem;box-shadow:inset 1px 0 #0000001a,inset -1px 0 #0000001a,inset 0 1px #0000001a}.color-slider[data-orientation=vertical] .color-slider__thumb{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);left:50%}.color-swatch{box-sizing:border-box;width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8);background:linear-gradient(var(--color-swatch-current),var(--color-swatch-current)),repeating-conic-gradient(#efefef 0% 25%,#f7f7f7 0% 50%) 50% / 16px 16px;flex-shrink:0;position:relative;box-shadow:inset 0 0 0 1px #0000001a}.color-swatch--circle{border-radius:calc(var(--radius) * 2)}.color-swatch--square{border-radius:calc(var(--radius) * .75)}.color-swatch--xs{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.color-swatch--xs.color-swatch--circle{border-radius:calc(var(--radius) * 1)}.color-swatch--sm{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.color-swatch--sm.color-swatch--circle{border-radius:calc(var(--radius) * 1.5)}.color-swatch--lg{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.color-swatch--lg.color-swatch--circle{border-radius:calc(var(--radius) * 3)}.color-swatch--xl{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10)}.color-swatch--xl.color-swatch--circle{border-radius:calc(var(--radius) * 3)}.color-swatch-picker{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.color-swatch-picker__item{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8);border-radius:calc(var(--radius) * 2);border-style:var(--tw-border-style);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);transition:border-color .1s var(--ease-out),box-shadow .1s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);border-width:2px;border-color:#0000;outline-style:none;justify-content:center;align-items:center;display:flex;position:relative}.color-swatch-picker__item:is([data-reduce-motion=true],[data-reduce-motion=true] *),.color-swatch-picker__item:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.color-swatch-picker__item:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.color-swatch-picker__item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.color-swatch-picker__item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.color-swatch-picker__item:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.color-swatch-picker__item:focus-visible,.color-swatch-picker__item[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.color-swatch-picker__item[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.color-swatch-picker__item[data-selected=true]{border-color:var(--color-swatch-current);box-shadow:var(--field-shadow)}.color-swatch-picker__item[data-selected=true] .color-swatch-picker__swatch{transform:scale(.77)}.color-swatch-picker__swatch{border-radius:inherit;width:100%;height:100%;transition:transform .1s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);display:block}.color-swatch-picker__swatch:is([data-reduce-motion=true],[data-reduce-motion=true] *),.color-swatch-picker__swatch:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.color-swatch-picker__swatch:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.color-swatch-picker__swatch:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.color-swatch-picker__swatch:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.color-swatch-picker__swatch:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.color-swatch-picker__swatch:hover{transform:scale(1.1)}}.color-swatch-picker__indicator{pointer-events:none;z-index:10;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.color-swatch-picker__indicator>*{width:33.3333%;height:33.3333%;color:var(--color-white);transition:transform .15s var(--ease-out);transform:scale(0)translateZ(0)}.color-swatch-picker__indicator>:is([data-reduce-motion=true],[data-reduce-motion=true] *),.color-swatch-picker__indicator>:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.color-swatch-picker__indicator>:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.color-swatch-picker__indicator>:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.color-swatch-picker__indicator>:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.color-swatch-picker__indicator>:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.color-swatch-picker__indicator[data-light-color=true] .color-swatch-picker__indicator>*{color:var(--color-black)}.color-swatch-picker__item[data-selected=true] .color-swatch-picker__indicator>*{transform:scale(1)translateZ(0)}.color-swatch-picker--stack{flex-direction:column}.color-swatch-picker--xs .color-swatch-picker__item{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * 1);border-style:var(--tw-border-style);border-width:1px}.color-swatch-picker--sm .color-swatch-picker__item{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6);border-radius:calc(var(--radius) * 1.5);border-style:var(--tw-border-style);border-width:2px}.color-swatch-picker--lg .color-swatch-picker__item{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9);border-radius:calc(var(--radius) * 3);border-style:var(--tw-border-style);border-width:3px}.color-swatch-picker--xl .color-swatch-picker__item{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10);border-radius:calc(var(--radius) * 3);border-style:var(--tw-border-style);border-width:3px}.color-swatch-picker--square .color-swatch-picker__item{border-radius:calc(var(--radius) * 1.5)}.color-swatch-picker--square .color-swatch-picker__item .color-swatch-picker__swatch,.color-swatch-picker--square .color-swatch-picker__item[data-selected=true] .color-swatch-picker__swatch{border-radius:calc(var(--radius) * 1)}.color-swatch-picker--square.color-swatch-picker--xs .color-swatch-picker__item,.color-swatch-picker--square.color-swatch-picker--xs .color-swatch-picker__item .color-swatch-picker__swatch,.color-swatch-picker--square.color-swatch-picker--xs .color-swatch-picker__item[data-selected=true] .color-swatch-picker__swatch{border-radius:calc(var(--radius) * .75)}.color-swatch-picker--square.color-swatch-picker--sm .color-swatch-picker__item,.color-swatch-picker--square.color-swatch-picker--sm .color-swatch-picker__item .color-swatch-picker__swatch{border-radius:calc(var(--radius) * 1)}.color-swatch-picker--square.color-swatch-picker--sm .color-swatch-picker__item[data-selected=true] .color-swatch-picker__swatch{border-radius:calc(var(--radius) * .75)}.color-swatch-picker--square.color-swatch-picker--lg .color-swatch-picker__item{border-radius:calc(var(--radius) * 1.5)}.color-swatch-picker--square.color-swatch-picker--lg .color-swatch-picker__item .color-swatch-picker__swatch,.color-swatch-picker--square.color-swatch-picker--lg .color-swatch-picker__item[data-selected=true] .color-swatch-picker__swatch{border-radius:calc(var(--radius) * 1)}.color-swatch-picker--square.color-swatch-picker--xl .color-swatch-picker__item{border-radius:calc(var(--radius) * 1.5)}.color-swatch-picker--square.color-swatch-picker--xl .color-swatch-picker__item .color-swatch-picker__swatch,.color-swatch-picker--square.color-swatch-picker--xl .color-swatch-picker__item[data-selected=true] .color-swatch-picker__swatch{border-radius:calc(var(--radius) * 1)}.color-input-group{height:calc(var(--spacing) * 9);border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border);transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);outline-style:none;align-items:center;display:inline-flex;overflow:hidden}.color-input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *),.color-input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.color-input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.color-input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.color-input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.color-input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.color-input-group:hover:not(:focus-within),.color-input-group[data-hovered=true]:not([data-focus-within=true]){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.color-input-group[data-focus-within=true],.color-input-group:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.color-input-group[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.color-input-group[data-invalid=true]:focus,.color-input-group[data-invalid=true]:focus-visible,.color-input-group[data-invalid=true][data-focused=true],.color-input-group[data-invalid=true][data-focus-visible=true],.color-input-group[data-invalid=true]:focus-within,.color-input-group[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.color-input-group[data-invalid=true]{background-color:var(--field-focus);border-color:var(--color-field-border-invalid)}.color-input-group[data-disabled=true],.color-input-group[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.color-input-group__input{cursor:text;border-style:var(--tw-border-style);height:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;background-color:#0000;border-width:0;border-radius:0;outline-style:none;flex:1;align-items:center;display:flex}@media(min-width:40rem){.color-input-group__input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.color-input-group__input::placeholder{color:var(--field-placeholder,var(--muted))}.color-input-group:has([data-slot=color-input-group-prefix]) .color-input-group__input{border-start-start-radius:0;border-end-start-radius:0;padding-inline-start:calc(var(--spacing) * 2)}.color-input-group:has([data-slot=color-input-group-suffix]) .color-input-group__input{border-start-end-radius:0;border-end-end-radius:0;padding-inline-end:calc(var(--spacing) * 2)}.color-input-group__input:focus,.color-input-group__input:focus-visible{--tw-outline-style:none;outline-style:none}.color-input-group__prefix{color:var(--field-placeholder,var(--muted));flex-shrink:0;align-items:center;margin-inline-start:calc(var(--spacing) * 3);margin-inline-end:0;display:flex}.color-input-group__suffix{color:var(--field-placeholder,var(--muted));flex-shrink:0;align-items:center;margin-inline-end:calc(var(--spacing) * 3);display:flex}.color-input-group--secondary{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--color-input-group-bg);--color-input-group-bg:var(--default);--color-input-group-bg-hover:var(--default-hover);--color-input-group-bg-focus:var(--default)}@media(hover:hover){.color-input-group--secondary:hover:not(:focus-within),.color-input-group--secondary[data-hovered=true]:not([data-focus-within=true]){background-color:var(--color-input-group-bg-hover)}}.color-input-group--secondary:focus-within,.color-input-group--secondary[data-focus-within=true]{background-color:var(--color-input-group-bg-focus)}.color-input-group--secondary[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.color-input-group--secondary[data-invalid=true]:focus,.color-input-group--secondary[data-invalid=true]:focus-visible,.color-input-group--secondary[data-invalid=true][data-focused=true],.color-input-group--secondary[data-invalid=true][data-focus-visible=true],.color-input-group--secondary[data-invalid=true]:focus-within,.color-input-group--secondary[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.color-input-group--secondary[data-invalid=true]{border-color:var(--color-field-border-invalid);background-color:var(--color-input-group-bg-focus)}.color-input-group--secondary [data-slot=color-input-group-input]{background-color:#0000}.color-input-group--full-width{width:100%}.color-field{gap:var(--spacing);flex-direction:column;display:flex}:is(.color-field[data-invalid=true],.color-field[aria-invalid=true]) [data-slot=description]{display:none}.color-field [data-slot=label]{width:fit-content}.color-field--full-width{width:100%}.slider{gap:var(--spacing);grid-template-columns:1fr auto;grid-template-areas:"label output""track track";width:100%;display:grid}.slider [data-slot=label]{width:fit-content;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);grid-area:label}.slider .slider__output{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);grid-area:output}.slider .slider__track{border-radius:calc(var(--radius) * 1.5);background-color:var(--default);grid-area:track;position:relative}.slider .slider__fill{pointer-events:none;background-color:var(--accent);position:absolute}.slider .slider__thumb{cursor:grab;border-radius:calc(var(--radius) * 1.5);background-color:var(--accent);-webkit-tap-highlight-color:transparent;transition:background-color .25s var(--ease-smooth),transform .25s var(--ease-out),box-shadow .15s var(--ease-out);justify-content:center;align-items:center;display:flex;position:absolute}.slider .slider__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *),.slider .slider__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.slider .slider__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.slider .slider__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.slider .slider__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.slider .slider__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.slider .slider__thumb:after{z-index:10;border-radius:calc(var(--radius) * 1);background-color:var(--accent-foreground);color:var(--color-black);--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);content:"";transform-origin:50%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));position:relative}@media(prefers-reduced-motion:reduce){.slider .slider__thumb:after:not(:is()){transition-property:none}}.slider .slider__thumb[data-dragging=true]{cursor:grabbing}.slider .slider__thumb[data-dragging=true]:after{scale:.9}@media(prefers-reduced-motion:reduce){.slider .slider__thumb[data-dragging=true]:after:not(:is()){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}}.slider .slider__thumb[data-focus-visible=true]{z-index:10;--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.slider .slider__thumb[data-disabled=true]{cursor:default}.slider:disabled,.slider[data-disabled=true],.slider[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:is(.slider:disabled,.slider[data-disabled=true],.slider[aria-disabled=true]) [data-slot=label]{opacity:1}.slider[data-orientation=horizontal]{flex-direction:column}.slider[data-orientation=horizontal] .slider__track{height:calc(var(--spacing) * 5);border-inline-style:var(--tw-border-style);border-inline-width:.75rem;border-inline-color:#0000;width:100%}.slider[data-orientation=horizontal] .slider__track[data-fill-start=true]{border-inline-start-color:var(--accent)}.slider[data-orientation=horizontal] .slider__track[data-fill-end=true]{border-inline-end-color:var(--accent)}.slider[data-orientation=horizontal] .slider__fill,.slider[data-orientation=horizontal] .slider__thumb{height:100%}.slider[data-orientation=horizontal] .slider__thumb{width:1.75rem;top:50%}.slider[data-orientation=horizontal] .slider__thumb:after{width:1.5rem;height:1rem}.slider[data-orientation=vertical]{gap:calc(var(--spacing) * 2);flex-direction:row;grid-template:"output""track"1fr"label"/1fr;height:100%}.slider[data-orientation=vertical] .slider__output,.slider[data-orientation=vertical] [data-slot=label]{text-align:center}.slider[data-orientation=vertical] .slider__track{height:100%;width:calc(var(--spacing) * 5);border-block-style:var(--tw-border-style);border-block-width:.75rem;border-block-color:#0000;justify-self:center}.slider[data-orientation=vertical] .slider__track[data-fill-start=true]{border-bottom-color:var(--accent)}.slider[data-orientation=vertical] .slider__track[data-fill-end=true]{border-top-color:var(--accent)}.slider[data-orientation=vertical] .slider__fill,.slider[data-orientation=vertical] .slider__thumb{width:100%}.slider[data-orientation=vertical] .slider__thumb{height:1.75rem;left:50%}.slider[data-orientation=vertical] .slider__thumb:after{width:1rem;height:1.5rem}.switch{align-items:flex-start;gap:var(--spacing);-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);--switch-control-bg:var(--default);--switch-control-bg-hover:var(--switch-control-bg);flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.switch{--switch-control-bg-hover:color-mix(in oklab, var(--switch-control-bg), transparent 20%)}}.switch{--switch-control-bg-pressed:var(--switch-control-bg-hover);--switch-control-bg-checked:var(--accent);--switch-control-bg-checked-hover:var(--accent-hover)}.switch[data-disabled=true],.switch[data-disabled=true] [data-slot=description],.switch[data-disabled=true] [data-slot=field-error]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.switch[data-disabled=true] .switch__thumb{background-color:var(--default-foreground)}@supports (color:color-mix(in lab,red,red)){.switch[data-disabled=true] .switch__thumb{background-color:color-mix(in oklab,var(--default-foreground) 20%,transparent)}}.switch>[data-slot=description]{width:100%;min-width:0;padding-inline-start:3.25rem}.switch>[data-slot=field-error]{width:100%;min-width:0;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);padding-inline-start:3.25rem}.switch.switch--sm>[data-slot=description],.switch.switch--sm>[data-slot=field-error]{padding-inline-start:2.75rem}.switch.switch--lg>[data-slot=description],.switch.switch--lg>[data-slot=field-error]{padding-inline-start:3.75rem}:is(.switch:disabled[aria-checked=true],.switch:disabled[data-selected=true],.switch[data-disabled=true][aria-checked=true],.switch[data-disabled=true][data-selected=true],.switch[aria-disabled=true][aria-checked=true],.switch[aria-disabled=true][data-selected=true]) .switch__thumb{opacity:.4}.switch__control{border-radius:calc(var(--radius) * 1.5);background-color:var(--switch-control-bg);width:2.5rem;height:1.25rem;transition:background-color .25s var(--ease-smooth),box-shadow .15s var(--ease-out);flex-shrink:0;align-items:center;display:flex;position:relative;overflow:hidden}.switch__control:is([data-reduce-motion=true],[data-reduce-motion=true] *),.switch__control:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.switch__control:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.switch__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.switch__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.switch__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.switch:focus-visible .switch__control,.switch:has([data-slot=switch-content][data-focus-visible=true]) .switch__control,.switch [data-slot=switch-content][data-focus-visible=true] .switch__control{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.switch:hover .switch__control,.switch:has([data-slot=switch-content][data-hovered=true]) .switch__control,.switch [data-slot=switch-content][data-hovered=true] .switch__control{background-color:var(--switch-control-bg-hover)}.switch:active .switch__control,.switch:has([data-slot=switch-content][data-pressed=true]) .switch__control,.switch [data-slot=switch-content][data-pressed=true] .switch__control{background-color:var(--switch-control-bg-pressed)}:is(.switch:active .switch__control,.switch:has([data-slot=switch-content][data-pressed=true]) .switch__control,.switch [data-slot=switch-content][data-pressed=true] .switch__control):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.switch:active .switch__control,.switch:has([data-slot=switch-content][data-pressed=true]) .switch__control,.switch [data-slot=switch-content][data-pressed=true] .switch__control):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.switch:active .switch__control,.switch:has([data-slot=switch-content][data-pressed=true]) .switch__control,.switch [data-slot=switch-content][data-pressed=true] .switch__control):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transform:none}@media(prefers-reduced-motion:reduce){:is(.switch:active .switch__control,.switch:has([data-slot=switch-content][data-pressed=true]) .switch__control,.switch [data-slot=switch-content][data-pressed=true] .switch__control):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.switch:active .switch__control,.switch:has([data-slot=switch-content][data-pressed=true]) .switch__control,.switch [data-slot=switch-content][data-pressed=true] .switch__control):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.switch:active .switch__control,.switch:has([data-slot=switch-content][data-pressed=true]) .switch__control,.switch [data-slot=switch-content][data-pressed=true] .switch__control):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transform:none}}.switch[aria-checked=true] .switch__control,.switch[data-selected=true] .switch__control{background-color:var(--switch-control-bg-checked)}.switch[aria-checked=true]:hover .switch__control,.switch[data-selected=true]:hover .switch__control,.switch[aria-checked=true][data-hovered=true] .switch__control,.switch[data-selected=true][data-hovered=true] .switch__control,.switch:has([data-slot=switch-content][data-hovered=true])[data-selected=true] .switch__control,.switch[aria-checked=true]:active .switch__control,.switch[data-selected=true]:active .switch__control,.switch[aria-checked=true][data-pressed=true] .switch__control,.switch[data-selected=true][data-pressed=true] .switch__control,.switch:has([data-slot=switch-content][data-pressed=true])[data-selected=true] .switch__control{background-color:var(--switch-control-bg-checked-hover)}.switch__content{cursor:inherit;align-items:center;gap:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline-style:none;display:inline-flex;position:relative}.switch--sm .switch__control{border-radius:calc(var(--radius) * 1);width:2rem;height:1rem}.switch--lg .switch__control{width:3rem;height:1.5rem}.switch__thumb{transform-origin:50%;border-radius:calc(var(--radius) * 1);background-color:var(--color-white);color:var(--color-black);--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);width:1.375rem;height:1rem;transition:margin .3s var(--ease-out-fluid),background-color .2s var(--ease-out);margin-inline-start:calc(var(--spacing) * .5);display:flex}.switch__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *),.switch__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.switch__thumb:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.switch__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.switch__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.switch__thumb:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.switch[aria-checked=true] .switch__thumb,.switch[data-selected=true] .switch__thumb{background-color:var(--accent-foreground);color:var(--accent);margin-inline-start:calc(100% - 1.5rem);box-shadow:0 0 5px #00000005,0 2px 10px #0000000f,0 0 1px #0000004d}.switch--sm .switch__thumb{border-radius:calc(var(--radius) * .75);width:1.03125rem;height:.75rem}.switch[aria-checked=true] :is(.switch--sm .switch__thumb),.switch[data-selected=true] :is(.switch--sm .switch__thumb){margin-inline-start:calc(100% - 1.15625rem)}.switch--lg .switch__thumb{border-radius:calc(var(--radius) * 1.5);width:1.71875rem;height:1.25rem}.switch[aria-checked=true] :is(.switch--lg .switch__thumb),.switch[data-selected=true] :is(.switch--lg .switch__thumb){margin-inline-start:calc(100% - 1.84375rem)}.switch__thumb>*{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.switch [data-slot=label]{-webkit-user-select:none;user-select:none}.switch__content [data-slot=label]{cursor:var(--cursor-interactive)}.switch [data-slot=description]{cursor:default;-webkit-user-select:none;user-select:none}.switch-group{gap:calc(var(--spacing) * 6);flex-direction:column;display:flex}.switch-group__items{gap:calc(var(--spacing) * 4);display:flex}.switch-group--horizontal .switch-group__items{flex-direction:row}.switch-group--vertical .switch-group__items{flex-direction:column}.badge{justify-content:center;align-items:center;gap:calc(var(--spacing) * .5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);min-height:calc(var(--spacing) * 7);min-width:calc(var(--spacing) * 7);border-radius:calc(var(--radius) * 3);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:1.34;--badge-bg:var(--default);--badge-fg:var(--default-foreground);--badge-border:var(--background);background-color:var(--badge-bg);color:var(--badge-fg);border:1px solid var(--badge-border);background-clip:padding-box;flex-shrink:0;line-height:1.34;display:inline-flex}.badge__label{padding-inline:calc(var(--spacing) * .5)}.badge-anchor{flex-shrink:0;display:inline-flex;position:relative}.badge--lg{min-height:calc(var(--spacing) * 8);min-width:calc(var(--spacing) * 8);border-radius:calc(var(--radius) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.43;line-height:1.43}.badge--sm{min-height:calc(var(--spacing) * 4);min-width:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * 1.5);--tw-leading:1.34;font-size:10px;line-height:1.34}.badge--accent{--badge-fg:var(--accent-soft-foreground)}.badge--default{--badge-fg:var(--default-foreground)}.badge--success{--badge-fg:var(--success-soft-foreground)}.badge--warning{--badge-fg:var(--warning-soft-foreground)}.badge--danger{--badge-fg:var(--danger-soft-foreground)}.badge--top-right{position:absolute;top:0;right:0;transform:translate(25%,-25%)}.badge--top-left{position:absolute;top:0;left:0;transform:translate(-25%,-25%)}.badge--bottom-right{position:absolute;bottom:0;right:0;transform:translate(25%,25%)}.badge--bottom-left{position:absolute;bottom:0;left:0;transform:translate(-25%,25%)}.badge--primary.badge--accent{--badge-bg:var(--accent);--badge-fg:var(--accent-foreground)}.badge--primary.badge--default{--badge-bg:var(--default);--badge-fg:var(--default-foreground)}.badge--primary.badge--success{--badge-bg:var(--success);--badge-fg:var(--success-foreground)}.badge--primary.badge--warning{--badge-bg:var(--warning);--badge-fg:var(--warning-foreground)}.badge--primary.badge--danger{--badge-bg:var(--danger);--badge-fg:var(--danger-foreground)}.badge--soft.badge--accent{--badge-bg:var(--accent-soft);--badge-fg:var(--accent-soft-foreground)}.badge--soft.badge--default{--badge-bg:var(--default-soft);--badge-fg:var(--default-soft-foreground)}.badge--soft.badge--success{--badge-bg:var(--success-soft);--badge-fg:var(--success-soft-foreground)}.badge--soft.badge--warning{--badge-bg:var(--warning-soft);--badge-fg:var(--warning-soft-foreground)}.badge--soft.badge--danger{--badge-bg:var(--danger-soft);--badge-fg:var(--danger-soft-foreground)}.chip{align-items:center;gap:calc(var(--spacing) * .5);border-radius:calc(var(--radius) * 2);width:fit-content;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--chip-bg:var(--default);--chip-fg:currentColor;background-color:var(--chip-bg);color:var(--chip-fg);flex-shrink:0;display:inline-flex}.chip__label{padding-inline:calc(var(--spacing) * .5)}.chip--accent{--chip-fg:var(--accent-soft-foreground)}.chip--danger{--chip-fg:var(--danger-soft-foreground)}.chip--default{--chip-fg:var(--default-foreground)}.chip--success{--chip-fg:var(--success-soft-foreground)}.chip--warning{--chip-fg:var(--warning-soft-foreground)}.chip--tertiary{--chip-bg:transparent}.chip--sm{padding-inline:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:0}.chip--md{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.chip--lg{padding-inline:calc(var(--spacing) * 3);padding-block:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.chip--primary.chip--accent{--chip-bg:var(--accent);--chip-fg:var(--accent-foreground)}.chip--primary.chip--success{--chip-bg:var(--success);--chip-fg:var(--success-foreground)}.chip--primary.chip--warning{--chip-bg:var(--warning);--chip-fg:var(--warning-foreground)}.chip--primary.chip--danger{--chip-bg:var(--danger);--chip-fg:var(--danger-foreground)}.chip--accent.chip--soft{--chip-bg:var(--accent-soft);--chip-fg:var(--accent-soft-foreground)}.chip--success.chip--soft{--chip-bg:var(--success-soft);--chip-fg:var(--success-soft-foreground)}.chip--warning.chip--soft{--chip-bg:var(--warning-soft);--chip-fg:var(--warning-soft-foreground)}.chip--danger.chip--soft{--chip-bg:var(--danger-soft);--chip-fg:var(--danger-soft-foreground)}.chip--default.chip--soft{--chip-bg:var(--default-soft);--chip-fg:var(--default-soft-foreground)}.table-root{grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative;overflow:clip}.table__scroll-container{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);scrollbar-gutter:auto;overflow-x:auto}.table-root--primary{background-color:var(--surface-secondary);padding-inline:var(--spacing);padding-bottom:var(--spacing);border-radius:min(32px,calc(var(--radius) * 2.5))}.table-root--secondary .table__header{border-bottom-style:var(--tw-border-style);background-color:#0000;border-bottom-width:0}.table-root--secondary .table__column{background-color:var(--surface-secondary)}.table-root--secondary :is(th.table__column:first-child,[role=row]>[role=presentation]:first-of-type>.table__column){border-start-start-radius:min(32px,var(--radius-2xl));border-end-start-radius:min(32px,var(--radius-2xl))}.table-root--secondary :is(th.table__column:last-child,[role=row]>[role=presentation]:last-of-type>.table__column){border-start-end-radius:min(32px,var(--radius-2xl));border-end-end-radius:min(32px,var(--radius-2xl))}.table-root--secondary .table__body{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.table-root--secondary .table__body tr:first-child td:first-child,.table-root--secondary .table__body tr:first-child td:last-child,.table-root--secondary .table__body tr:last-child td:first-child,.table-root--secondary .table__body tr:last-child td:last-child{border-radius:0}.table-root--secondary .table__body:not(tbody){border-radius:0;overflow:visible}.table-root--secondary .table__row .table__cell{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--separator-tertiary)}@supports (color:color-mix(in lab,red,red)){.table-root--secondary .table__row .table__cell{border-color:color-mix(in oklab,var(--separator-tertiary) 50%,transparent)}}.table-root--secondary .table__row .table__cell{background-color:#0000}@media(hover:hover){.table-root--secondary .table__row:hover .table__cell,.table-root--secondary .table__row[data-hovered=true] .table__cell{background-color:var(--default)}@supports (color:color-mix(in lab,red,red)){.table-root--secondary .table__row:hover .table__cell,.table-root--secondary .table__row[data-hovered=true] .table__cell{background-color:color-mix(in oklab,var(--default) 50%,transparent)}}}.table__content{border-collapse:separate;--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.table-root--primary .table__content{overflow:clip}.table__header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--separator)}@supports (color:color-mix(in lab,red,red)){.table__header{border-color:color-mix(in oklab,var(--separator) 50%,transparent)}}.table__header{background-color:var(--surface-secondary)}.table__column{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);text-align:start;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted);position:relative}.table__column:after{content:"";pointer-events:none;height:calc(var(--spacing) * 4);--tw-translate-y: -50% ;width:1px;translate:var(--tw-translate-x) var(--tw-translate-y);border-radius:calc(var(--radius) * .5);background-color:var(--separator);inset-inline-end:calc(var(--spacing) * 0);position:absolute;top:50%}.table__column:last-child:not(:only-child):after{content:none}.table__column[data-allows-sorting=true]{cursor:var(--cursor-interactive)}@media(hover:hover){.table__column[data-allows-sorting=true]:hover,.table__column[data-allows-sorting=true][data-hovered=true]{color:var(--foreground)}}.table__column:focus-visible,.table__column[data-focus-visible=true]{border-radius:calc(var(--radius) * 1);--tw-outline-style:none;box-shadow:inset 0 0 0 2px var(--focus);outline-style:none}[role=row]>[role=presentation]:last-of-type:not(:only-of-type)>.table__column:after{content:none}.table__sortable-column-header{justify-content:space-between;align-items:center;display:flex}.table__sortable-column-indicator{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.1s;--tw-ease:var(--ease-out);transition-duration:.1s;transition-timing-function:var(--ease-out);place-content:center;display:inline-grid}.table__sortable-column-indicator[data-direction=descending]{rotate:180deg}.table__body tr:first-child td:first-child{border-start-start-radius:min(32px,var(--radius-2xl))}.table__body tr:first-child td:last-child{border-start-end-radius:min(32px,var(--radius-2xl))}.table__body tr:last-child td:first-child{border-end-start-radius:min(32px,var(--radius-2xl))}.table__body tr:last-child td:last-child{border-end-end-radius:min(32px,var(--radius-2xl))}.table__body:not(tbody){border-radius:min(32px,var(--radius-2xl));height:100%;position:relative;overflow:clip}.table__row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--separator);height:100%;position:relative}@supports (color:color-mix(in lab,red,red)){.table__row{border-color:color-mix(in oklab,var(--separator) 50%,transparent)}}.table__row:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media(hover:hover){.table__row:hover .table__cell,.table__row[data-hovered=true] .table__cell{background-color:var(--surface)}@supports (color:color-mix(in lab,red,red)){.table__row:hover .table__cell,.table__row[data-hovered=true] .table__cell{background-color:color-mix(in oklab,var(--surface) 40%,transparent)}}}.table__row[data-selected=true] .table__cell{background-color:var(--surface)}@supports (color:color-mix(in lab,red,red)){.table__row[data-selected=true] .table__cell{background-color:color-mix(in oklab,var(--surface) 10%,transparent)}}.table__row[aria-disabled=true],.table__row[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.table__row:focus-visible,.table__row[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.table__row[data-dragging=true]{opacity:.5}.table__row[data-drop-target=true] .table__cell{background-color:var(--accent-soft)}.table__cell{background-color:var(--surface);height:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);vertical-align:middle;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--separator-tertiary)}@supports (color:color-mix(in lab,red,red)){.table__cell{border-color:color-mix(in oklab,var(--separator-tertiary) 50%,transparent)}}.table__cell:focus-visible,.table__cell[data-focus-visible=true]{border-radius:calc(var(--radius) * 1);--tw-outline-style:none;box-shadow:inset 0 0 0 2px var(--focus);outline-style:none}.table__row:is(:focus-visible,[data-focus-visible=true]) :is(.table__cell,.table__column){--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.table__row:is(:focus-visible,[data-focus-visible=true])>:is(.table__cell,.table__column):only-child,.table__row:is(:focus-visible,[data-focus-visible=true])>:only-child :is(.table__cell,.table__column){border-radius:calc(var(--radius) * 1);--tw-shadow:inset 0 0 0 2px var(--tw-shadow-color,var(--focus));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;outline-style:none}.table__row:is(:focus-visible,[data-focus-visible=true])>:is(.table__cell,.table__column):first-child:not(:only-child),.table__row:is(:focus-visible,[data-focus-visible=true])>:first-child:not(:only-child) :is(.table__cell,.table__column){--tw-shadow:inset 2px 0 0 0 var(--tw-shadow-color,var(--focus)), inset 0 2px 0 0 var(--tw-shadow-color,var(--focus)), inset 0 -2px 0 0 var(--tw-shadow-color,var(--focus));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-start-start-radius:calc(var(--radius) * 1);border-end-start-radius:calc(var(--radius) * 1);outline-style:none}.table__row:is(:focus-visible,[data-focus-visible=true])>:is(.table__cell,.table__column):last-child:not(:only-child),.table__row:is(:focus-visible,[data-focus-visible=true])>:last-child:not(:only-child) :is(.table__cell,.table__column){--tw-shadow:inset -2px 0 0 0 var(--tw-shadow-color,var(--focus)), inset 0 2px 0 0 var(--tw-shadow-color,var(--focus)), inset 0 -2px 0 0 var(--tw-shadow-color,var(--focus));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-start-end-radius:calc(var(--radius) * 1);border-end-end-radius:calc(var(--radius) * 1);outline-style:none}.table__row:is(:focus-visible,[data-focus-visible=true])>:is(.table__cell,.table__column):not(:first-child):not(:last-child):not(:only-child),.table__row:is(:focus-visible,[data-focus-visible=true])>:not(:first-child):not(:last-child):not(:only-child) :is(.table__cell,.table__column){--tw-shadow:inset 0 2px 0 0 var(--tw-shadow-color,var(--focus)), inset 0 -2px 0 0 var(--tw-shadow-color,var(--focus));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;outline-style:none}.table__cell[data-tree-column]{padding-inline-start:calc(1rem * var(--table-row-level,1))}.table__footer{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);align-items:center;display:flex}.table__resizable-container{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);scrollbar-gutter:auto;position:relative;overflow:auto}.table__column-resizer{height:calc(var(--spacing) * 4);--tw-translate-y: -50% ;border-radius:calc(var(--radius) * .5);background-color:var(--separator);box-sizing:content-box;--tw-translate-x: 50% ;width:1px;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:col-resize;touch-action:none;padding-inline:calc(var(--spacing) * 2);--tw-border-style:none;--tw-outline-style:none;inset-inline-end:calc(var(--spacing) * 0);background-clip:content-box;border-style:none;outline-style:none;position:absolute;top:50%}.table__column-resizer[data-hovered=true],.table__column-resizer:hover,.table__column-resizer[data-resizing=true]{height:100%;width:calc(var(--spacing) * .5);background-color:var(--accent)}.table__column-resizer[data-focus-visible=true],.table__column-resizer:focus-visible{height:100%;width:calc(var(--spacing) * .5);background-color:var(--focus)}.table__column:has(.table__column-resizer):after{content:none}.table__load-more td,.table__load-more [role=rowheader]{padding-block:calc(var(--spacing) * 3);text-align:center}:is(.table__load-more td,.table__load-more [role=rowheader])>*{margin-inline:auto}.table__load-more-content{justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);display:flex}.alert{justify-content:flex-start;align-items:flex-start;gap:calc(var(--spacing) * 4);background-color:var(--surface);width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);--tw-shadow:var(--surface-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:min(32px,var(--radius-3xl));flex-direction:row;display:flex}.alert__content{flex-direction:column;flex-grow:1;align-items:flex-start;height:100%;display:flex}.alert__indicator{padding:var(--spacing);-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;display:flex}.alert__indicator [data-slot=alert-default-icon]{box-sizing:content-box;width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.alert__title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.alert__description{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.alert--default .alert__indicator,.alert--default .alert__title{color:var(--foreground)}.alert--accent .alert__indicator,.alert--accent .alert__title{color:var(--accent-soft-foreground)}.alert--success .alert__indicator,.alert--success .alert__title{color:var(--success-soft-foreground)}.alert--warning .alert__indicator,.alert--warning .alert__title{color:var(--warning-soft-foreground)}.alert--danger .alert__indicator,.alert--danger .alert__title{color:var(--danger-soft-foreground)}.empty-state{padding:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.skeleton{pointer-events:none;border-radius:calc(var(--radius) * .5);background-color:var(--surface-tertiary);position:relative;overflow:hidden}@supports (color:color-mix(in lab,red,red)){.skeleton{background-color:color-mix(in oklab,var(--surface-tertiary) 70%,transparent)}}.skeleton--shimmer:after{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-gradient-position:to right;animation:2s linear infinite skeleton;position:absolute;inset:0}@supports (background-image:linear-gradient(in lab,red,red)){.skeleton--shimmer:after{--tw-gradient-position:to right in oklab}}.skeleton--shimmer:after{background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:transparent;--tw-gradient-via:var(--surface-tertiary);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));--tw-gradient-to:transparent;--tw-content:"";content:var(--tw-content)}.skeleton--shimmer:has(.skeleton):after{content:none}.skeleton--shimmer:has(.skeleton):before{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-content:"";content:var(--tw-content);z-index:10;pointer-events:none;mix-blend-mode:overlay;background:linear-gradient(90deg,#0000,#ffffff80,#0000);animation:2s linear infinite skeleton;position:absolute;inset:0}.skeleton--shimmer:has(.skeleton) .skeleton:after{content:none}.skeleton--pulse{animation:var(--animate-pulse)}.meter{gap:var(--spacing);--meter-fill:var(--accent);grid-template-columns:1fr auto;grid-template-areas:"label output""track track";width:100%;display:grid}.meter [data-slot=label]{width:fit-content;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);grid-area:label}.meter .meter__output{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);grid-area:output}.meter .meter__track{border-radius:calc(var(--radius) * .5);background-color:var(--default);height:calc(var(--spacing) * 2);grid-area:track;position:relative;overflow:hidden}.meter .meter__fill{border-radius:calc(var(--radius) * .5);background-color:var(--meter-fill);height:100%;transition:width .3s var(--ease-out);inset-inline-start:calc(var(--spacing) * 0);position:absolute;top:0}.meter .meter__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *),.meter .meter__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.meter .meter__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.meter .meter__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.meter .meter__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.meter .meter__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.meter:disabled,.meter[data-disabled=true],.meter[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:is(.meter:disabled,.meter[data-disabled=true],.meter[aria-disabled=true]) [data-slot=label]{opacity:1}.meter--sm .meter__track{height:var(--spacing);border-radius:calc(var(--radius) * .25)}.meter--sm .meter__fill{border-radius:calc(var(--radius) * .25)}.meter--lg .meter__track{height:calc(var(--spacing) * 3);border-radius:calc(var(--radius) * .75)}.meter--lg .meter__fill{border-radius:calc(var(--radius) * .75)}.meter--default{--meter-fill:var(--default-foreground)}.meter--accent{--meter-fill:var(--accent)}.meter--success{--meter-fill:var(--success)}.meter--warning{--meter-fill:var(--warning)}.meter--danger{--meter-fill:var(--danger)}.progress-bar{gap:var(--spacing);--progress-bar-fill:var(--accent);grid-template-columns:1fr auto;grid-template-areas:"label output""track track";width:100%;display:grid}.progress-bar [data-slot=label]{width:fit-content;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);grid-area:label}.progress-bar .progress-bar__output{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);grid-area:output}.progress-bar .progress-bar__track{border-radius:calc(var(--radius) * .5);background-color:var(--default);height:calc(var(--spacing) * 2);grid-area:track;position:relative;overflow:hidden}.progress-bar .progress-bar__fill{border-radius:calc(var(--radius) * .5);background-color:var(--progress-bar-fill);height:100%;transition:width .3s var(--ease-out);inset-inline-start:calc(var(--spacing) * 0);position:absolute;top:0}.progress-bar .progress-bar__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *),.progress-bar .progress-bar__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.progress-bar .progress-bar__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.progress-bar .progress-bar__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.progress-bar .progress-bar__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.progress-bar .progress-bar__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.progress-bar:not([aria-valuenow]) .progress-bar__fill{width:40%;animation:1.5s cubic-bezier(.65,0,.35,1) infinite progress-bar-indeterminate}.progress-bar:not([aria-valuenow]) .progress-bar__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *),.progress-bar:not([aria-valuenow]) .progress-bar__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.progress-bar:not([aria-valuenow]) .progress-bar__fill:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){.progress-bar:not([aria-valuenow]) .progress-bar__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.progress-bar:not([aria-valuenow]) .progress-bar__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.progress-bar:not([aria-valuenow]) .progress-bar__fill:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.progress-bar:disabled,.progress-bar[data-disabled=true],.progress-bar[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:is(.progress-bar:disabled,.progress-bar[data-disabled=true],.progress-bar[aria-disabled=true]) [data-slot=label]{opacity:1}@keyframes progress-bar-indeterminate{0%{transform:translate(-100%)}to{transform:translate(350%)}}.progress-bar--sm .progress-bar__track{height:var(--spacing);border-radius:calc(var(--radius) * .25)}.progress-bar--sm .progress-bar__fill{border-radius:calc(var(--radius) * .25)}.progress-bar--lg .progress-bar__track{height:calc(var(--spacing) * 3);border-radius:calc(var(--radius) * .75)}.progress-bar--lg .progress-bar__fill{border-radius:calc(var(--radius) * .75)}.progress-bar--default{--progress-bar-fill:var(--default-foreground)}.progress-bar--accent{--progress-bar-fill:var(--accent)}.progress-bar--success{--progress-bar-fill:var(--success)}.progress-bar--warning{--progress-bar-fill:var(--warning)}.progress-bar--danger{--progress-bar-fill:var(--danger)}.progress-circle{--progress-circle-stroke:var(--accent);--progress-circle-track-stroke:var(--default);justify-content:center;align-items:center;display:inline-flex}.progress-circle .progress-circle__track{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.progress-circle .progress-circle__track-circle{stroke:var(--progress-circle-track-stroke)}.progress-circle .progress-circle__fill-circle{stroke:var(--progress-circle-stroke);transition:stroke-dashoffset .3s var(--ease-out)}.progress-circle .progress-circle__fill-circle:is([data-reduce-motion=true],[data-reduce-motion=true] *),.progress-circle .progress-circle__fill-circle:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.progress-circle .progress-circle__fill-circle:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.progress-circle .progress-circle__fill-circle:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.progress-circle .progress-circle__fill-circle:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.progress-circle .progress-circle__fill-circle:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.progress-circle:not([aria-valuenow]) .progress-circle__track{animation:1s linear infinite progress-circle-spin}.progress-circle:not([aria-valuenow]) .progress-circle__track:is([data-reduce-motion=true],[data-reduce-motion=true] *),.progress-circle:not([aria-valuenow]) .progress-circle__track:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.progress-circle:not([aria-valuenow]) .progress-circle__track:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){.progress-circle:not([aria-valuenow]) .progress-circle__track:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.progress-circle:not([aria-valuenow]) .progress-circle__track:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.progress-circle:not([aria-valuenow]) .progress-circle__track:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.progress-circle:disabled,.progress-circle[data-disabled=true],.progress-circle[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}@keyframes progress-circle-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.progress-circle--sm .progress-circle__track{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.progress-circle--lg .progress-circle__track{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.progress-circle--default{--progress-circle-stroke:var(--default-foreground)}.progress-circle--accent{--progress-circle-stroke:var(--accent)}.progress-circle--success{--progress-circle-stroke:var(--success)}.progress-circle--warning{--progress-circle-stroke:var(--warning)}.progress-circle--danger{--progress-circle-stroke:var(--danger)}.spinner{pointer-events:none;width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6);flex-shrink:0;animation:.75s linear infinite spin;display:inline-flex}.spinner:is([data-reduce-motion=true],[data-reduce-motion=true] *),.spinner:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.spinner:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){.spinner:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.spinner:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.spinner:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.spinner--sm{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.spinner--lg{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.spinner--xl{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10)}.spinner--current{color:inherit}.spinner--accent{color:var(--accent)}.spinner--danger{color:var(--danger)}.spinner--success{color:var(--success)}.spinner--warning{color:var(--warning)}.toast-region{pointer-events:none;z-index:var(--z-index-toast);--tw-outline-style:none;outline-style:none;width:calc(100vw - 2rem);position:fixed}@media(min-width:40rem){.toast-region{width:auto;min-width:var(--toast-width)}}.toast-region{display:block}.toast-region--bottom{bottom:calc(var(--spacing) * 4);--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);left:50%}.toast-region--bottom-start{inset-inline-start:calc(var(--spacing) * 4);bottom:calc(var(--spacing) * 4)}.toast-region--bottom-end{inset-inline-end:calc(var(--spacing) * 4);bottom:calc(var(--spacing) * 4)}.toast-region--top{top:calc(var(--spacing) * 4);--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);left:50%}.toast-region--top-start{inset-inline-start:calc(var(--spacing) * 4);top:calc(var(--spacing) * 4)}.toast-region--top-end{inset-inline-end:calc(var(--spacing) * 4);top:calc(var(--spacing) * 4)}.toast-region:focus-visible{outline-style:var(--tw-outline-style);outline-offset:2px;outline-width:2px;outline-color:var(--focus)}.toast{border-radius:min(32px,var(--radius-3xl));--toast-y:var(--offset-collapsed,0px);--toast-enter:0%;--toast-scale:var(--scale-collapsed,1);transform:translateY(calc(var(--dir,1) * (var(--toast-y) + var(--toast-enter)))) scale(var(--toast-scale));height:var(--toast-height,auto);pointer-events:auto;justify-content:flex-start;align-items:flex-start;gap:calc(var(--spacing) * 1.5);background-color:var(--surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);--tw-shadow:var(--overlay-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--toast-ease:var(--ease-out-fluid);--toast-enter-duration:.35s;--toast-exit-duration:.25s;--toast-translate-duration:var(--toast-enter-duration);--toast-height-duration:var(--toast-enter-duration);--toast-opacity-duration:.15s;transition:transform var(--toast-translate-duration) var(--toast-ease),height var(--toast-height-duration) var(--toast-ease),opacity var(--toast-opacity-duration) var(--toast-ease);flex-direction:row;display:flex;position:absolute;inset-inline:0}.toast:is([data-reduce-motion=true],[data-reduce-motion=true] *),.toast:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.toast:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){.toast:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.toast:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.toast:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.toast:is([data-reduce-motion=true],[data-reduce-motion=true] *),.toast:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.toast:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.toast:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.toast:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.toast:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.toast[data-entering=true]{--toast-enter:-100%}.toast--bottom,.toast--bottom-start,.toast--bottom-end{--dir:-1;transform-origin:bottom;bottom:0}:is(.toast--bottom,.toast--bottom-start,.toast--bottom-end)[data-expanded=true]:after{top:calc(-1 * var(--gap,12px) - 1px);bottom:0}.toast--top,.toast--top-start,.toast--top-end{--dir:1;transform-origin:top;top:0}:is(.toast--top,.toast--top-start,.toast--top-end)[data-expanded=true]:after{top:0;bottom:calc(-1 * var(--gap,12px) - 1px)}.toast>:not([data-slot=toast-close]){transition:opacity .2s var(--toast-ease)}.toast>:not([data-slot=toast-close]):is([data-reduce-motion=true],[data-reduce-motion=true] *),.toast>:not([data-slot=toast-close]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.toast>:not([data-slot=toast-close]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.toast>:not([data-slot=toast-close]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.toast>:not([data-slot=toast-close]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.toast>:not([data-slot=toast-close]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.toast:not([data-frontmost=true]):not([data-expanded=true]){--toast-y:var(--offset-collapsed,0px);height:var(--front-height,0px);overflow:hidden}.toast:not([data-frontmost=true]):not([data-expanded=true])>:not([data-slot=toast-close]){opacity:0}.toast:not([data-frontmost=true]):not([data-expanded=true]) .toast__close-button{pointer-events:none;opacity:0;outline:none}.toast[data-expanded=true]{--toast-y:var(--offset-expanded,0px);--toast-scale:1;height:var(--toast-height,auto)}.toast[data-expanded=true]:after{content:"";z-index:-1;position:absolute;inset-inline:0}.toast[data-exiting=true]{opacity:0}.toast[data-exiting=true][data-frontmost=true]{--toast-translate-duration:var(--toast-exit-duration);--toast-opacity-duration:.15s;--toast-enter:-100%;--toast-scale:1}.toast[data-exiting=true][data-expanded=true]:not([data-frontmost=true]){--toast-translate-duration:.2s;--toast-opacity-duration:.15s;--toast-scale:.96}.toast[data-exiting=true]:not([data-expanded=true]):not([data-frontmost=true]){--toast-translate-duration:.2s;--toast-opacity-duration:.15s;--toast-scale:calc(var(--scale-collapsed,1) * .96)}.toast[data-hidden=true]{pointer-events:none;opacity:0;display:flex}.toast:focus-visible{outline-style:var(--tw-outline-style);outline-offset:2px;outline-width:2px;outline-color:var(--focus)}.toast__content{flex-direction:column;flex-grow:1;align-self:center;align-items:flex-start;display:flex}.toast__indicator{padding:var(--spacing);color:var(--overlay-foreground);-webkit-user-select:none;user-select:none;flex-shrink:0;justify-content:center;align-items:center;display:flex}.toast__indicator [data-slot=toast-default-icon]{box-sizing:content-box;width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.toast__indicator [data-slot=spinner],.toast__indicator [data-slot=spinner-icon]{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.toast__indicator[data-swapped=true]>:not([data-slot=spinner]){animation:toast-indicator-fade .15s var(--toast-ease),toast-indicator-scale .2s var(--toast-ease)}.toast__indicator[data-swapped=true]>:not([data-slot=spinner]):is([data-reduce-motion=true],[data-reduce-motion=true] *),.toast__indicator[data-swapped=true]>:not([data-slot=spinner]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.toast__indicator[data-swapped=true]>:not([data-slot=spinner]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){.toast__indicator[data-swapped=true]>:not([data-slot=spinner]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.toast__indicator[data-swapped=true]>:not([data-slot=spinner]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.toast__indicator[data-swapped=true]>:not([data-slot=spinner]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.toast__title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--overlay-foreground)}.toast__description{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.toast__close-button{pointer-events:none;inset-inline-end:calc(var(--spacing) * -1);top:calc(var(--spacing) * -1);width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5);border-color:var(--border);background-color:var(--default);opacity:0;position:absolute}@media(min-width:40rem){.toast__close-button{border-style:var(--tw-border-style);background-color:var(--overlay);border-width:1px}}.toast__close-button{transition:opacity .15s var(--ease-smooth)}.toast__close-button:is([data-reduce-motion=true],[data-reduce-motion=true] *),.toast__close-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.toast__close-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.toast__close-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.toast__close-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.toast__close-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.toast__close-button [data-slot=close-button-icon]{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}@media(min-width:40rem){.toast__close-button [data-slot=close-button-icon]{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}}@media(hover:hover){.toast__close-button:hover,.toast__close-button[data-hovered=true]{background-color:var(--default)}}.toast[data-frontmost=true]:hover .toast__close-button,.toast[data-expanded=true]:hover .toast__close-button{pointer-events:auto;opacity:1}.toast[data-exiting=true] .toast__close-button{pointer-events:none;opacity:0}.toast__action{margin-top:calc(var(--spacing) * 2)}@media(min-width:40rem){.toast__action{margin-top:0}}.toast--accent .toast__title{color:var(--accent-soft-foreground)}.toast--success .toast__title,.toast--success .toast__indicator{color:var(--success-soft-foreground)}.toast--warning .toast__title,.toast--warning .toast__indicator{color:var(--warning-soft-foreground)}.toast--danger .toast__title,.toast--danger .toast__indicator{color:var(--danger-soft-foreground)}@keyframes toast-indicator-fade{0%{opacity:0}}@keyframes toast-indicator-scale{0%{scale:.92}}.checkbox-group{flex-direction:column;display:flex}.checkbox-group [data-slot=checkbox]{margin-top:calc(var(--spacing) * 4)}.checkbox{align-items:flex-start;gap:var(--spacing);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);outline-style:none;flex-direction:column;display:flex}.checkbox>[data-slot=description],.checkbox>[data-slot=field-error]{cursor:default;width:100%;min-width:0;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));text-wrap:wrap;overflow-wrap:break-word;color:var(--muted);-webkit-user-select:none;user-select:none;padding-inline-start:calc(var(--spacing) * 7)}.checkbox [data-slot=label]{-webkit-user-select:none;user-select:none}.checkbox .checkbox__content [data-slot=label]{cursor:var(--cursor-interactive)}.checkbox[data-disabled=true],.checkbox[data-disabled=true] [data-slot=description],.checkbox[data-disabled=true] [data-slot=field-error]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:is(.checkbox[data-selected=true],.checkbox[data-indeterminate=true]) .checkbox__indicator{border-color:var(--accent-foreground)}.checkbox [data-slot=checkbox-default-indicator--checkmark]{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5);stroke-width:2.5px;color:var(--accent-foreground);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s}.checkbox [data-slot=checkbox-default-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *),.checkbox [data-slot=checkbox-default-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.checkbox [data-slot=checkbox-default-indicator--checkmark]:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.checkbox [data-slot=checkbox-default-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.checkbox [data-slot=checkbox-default-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.checkbox [data-slot=checkbox-default-indicator--checkmark]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.checkbox[data-selected=true] [data-slot=checkbox-default-indicator--checkmark]{transition:stroke-dashoffset .15s linear 15ms}.checkbox[data-invalid=true][data-selected=true] [data-slot=checkbox-default-indicator--checkmark],.checkbox[data-invalid=true][aria-checked=true] [data-slot=checkbox-default-indicator--checkmark],.checkbox[aria-invalid=true][data-selected=true] [data-slot=checkbox-default-indicator--checkmark],.checkbox[aria-invalid=true][aria-checked=true] [data-slot=checkbox-default-indicator--checkmark]{color:var(--danger-foreground)}.checkbox[data-indeterminate=true] [data-slot=checkbox-default-indicator--indeterminate]{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.checkbox[data-indeterminate=true][data-invalid=true] [data-slot=checkbox-default-indicator--indeterminate],.checkbox[data-indeterminate=true][aria-invalid=true] [data-slot=checkbox-default-indicator--indeterminate]{color:var(--danger-foreground)}.checkbox__control{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * .75);border-style:var(--tw-border-style);border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border,var(--border));background-color:var(--field-background,var(--default));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;transition:background-color .2s var(--ease-out),border-color .2s var(--ease-out),transform .1s var(--ease-out);outline-style:none;flex-shrink:0;justify-content:center;align-items:center;display:inline-flex;position:relative;overflow:hidden}.checkbox__control:is([data-reduce-motion=true],[data-reduce-motion=true] *),.checkbox__control:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.checkbox__control:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.checkbox__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.checkbox__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.checkbox__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.checkbox__control{cursor:var(--cursor-interactive)}.checkbox__control:before{pointer-events:none;z-index:0;transform-origin:50%;--tw-scale-x:70%;--tw-scale-y:70%;--tw-scale-z:70%;scale:var(--tw-scale-x) var(--tw-scale-y);border-radius:calc(var(--radius) * .75);background-color:var(--accent);opacity:0;--tw-content:"";content:var(--tw-content);transition:scale .1s var(--ease-linear),opacity .2s var(--ease-linear),background-color .2s var(--ease-out);position:absolute;inset:0}@media(prefers-reduced-motion:reduce){.checkbox__control:before:not(:is()){transition-property:none}}.checkbox:focus-visible .checkbox__control,.checkbox:has([data-slot=checkbox-content][data-focus-visible=true]) .checkbox__control,.checkbox [data-slot=checkbox-content][data-focus-visible=true] .checkbox__control{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.checkbox:hover .checkbox__control,.checkbox:has([data-slot=checkbox-content][data-hovered=true]) .checkbox__control,.checkbox [data-slot=checkbox-content][data-hovered=true] .checkbox__control{border-color:var(--field-border-hover)}:is(.checkbox:hover .checkbox__control,.checkbox:has([data-slot=checkbox-content][data-hovered=true]) .checkbox__control,.checkbox [data-slot=checkbox-content][data-hovered=true] .checkbox__control):before{background-color:var(--accent-hover)}.checkbox[aria-checked=true] .checkbox__control,.checkbox[data-selected=true] .checkbox__control{color:var(--accent-foreground);border-color:#0000}:is(.checkbox[aria-checked=true] .checkbox__control,.checkbox[data-selected=true] .checkbox__control):before{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:1}.checkbox[data-indeterminate=true] .checkbox__control{background-color:var(--accent);color:var(--accent-foreground)}.checkbox:active[data-indeterminate=true] .checkbox__control,.checkbox[data-pressed=true][data-indeterminate=true] .checkbox__control,.checkbox:has([data-slot=checkbox-content][data-pressed=true])[data-indeterminate=true] .checkbox__control{background-color:var(--accent-hover)}.checkbox[data-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control,.checkbox[aria-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}:is(.checkbox[data-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control,.checkbox[aria-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control):focus,:is(.checkbox[data-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control,.checkbox[aria-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control):focus-visible,:is(.checkbox[data-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control,.checkbox[aria-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control)[data-focused=true],:is(.checkbox[data-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control,.checkbox[aria-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control)[data-focus-visible=true],:is(.checkbox[data-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control,.checkbox[aria-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control):focus-within,:is(.checkbox[data-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control,.checkbox[aria-invalid=true]:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control)[data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.checkbox[data-invalid=true][aria-checked=true] .checkbox__control,.checkbox[data-invalid=true][data-selected=true] .checkbox__control,.checkbox[aria-invalid=true][aria-checked=true] .checkbox__control,.checkbox[aria-invalid=true][data-selected=true] .checkbox__control{background-color:var(--danger);color:var(--danger-foreground);border-color:#0000}:is(.checkbox[data-invalid=true][aria-checked=true] .checkbox__control,.checkbox[data-invalid=true][data-selected=true] .checkbox__control,.checkbox[aria-invalid=true][aria-checked=true] .checkbox__control,.checkbox[aria-invalid=true][data-selected=true] .checkbox__control):before{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);background-color:var(--danger);opacity:1}.checkbox[data-indeterminate=true][aria-invalid=true] .checkbox__control,.checkbox[data-indeterminate=true][data-invalid=true] .checkbox__control{background-color:var(--danger);color:var(--danger-foreground)}.checkbox__indicator{z-index:10;width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3);justify-content:center;align-items:center;display:flex;position:relative}.checkbox__indicator svg{width:100%;height:100%;transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.checkbox--secondary .checkbox__control{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--checkbox-control-bg);--checkbox-control-bg:var(--default)}.checkbox:hover :is(.checkbox--secondary .checkbox__control),.checkbox:has([data-slot=checkbox-content][data-hovered=true]) :is(.checkbox--secondary .checkbox__control),.checkbox [data-slot=checkbox-content][data-hovered=true] :is(.checkbox--secondary .checkbox__control){border-color:var(--field-border-hover)}.checkbox__content{cursor:inherit;align-items:center;gap:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline-style:none;display:inline-flex;position:relative}.checkbox--secondary:not([aria-checked=true]):not([data-selected=true]):not([data-indeterminate=true]) .checkbox__control{background-color:var(--checkbox-control-bg)}:is(.checkbox--secondary[aria-checked=true] .checkbox__control,.checkbox--secondary[data-selected=true] .checkbox__control):before,.checkbox--secondary[data-indeterminate=true] .checkbox__control,.checkbox--secondary[data-indeterminate=true] .checkbox__control:before{background-color:var(--accent)}.fieldset{gap:calc(var(--spacing) * 6);flex-direction:column;flex:1 1 0;display:flex}.fieldset__legend{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}.fieldset__field-group{width:100%}:where(.fieldset__field-group>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.fieldset__actions{align-items:center;gap:calc(var(--spacing) * 2);padding-top:var(--spacing);display:flex}.input-otp{align-items:center;gap:calc(var(--spacing) * 2);width:100%;display:flex;position:relative}.input-otp[data-disabled=true]{cursor:not-allowed;opacity:.5}.input-otp__group{align-items:center;gap:calc(var(--spacing) * 2);min-width:0;display:flex}.input-otp__slot{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 9.5);border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));min-width:0;color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:var(--field-radius,calc(var(--radius) * 1.5));font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-outline-style:none;border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border);transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);outline-style:none;flex:1;justify-content:center;align-items:center;display:flex;position:relative}.input-otp__slot:is([data-reduce-motion=true],[data-reduce-motion=true] *),.input-otp__slot:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.input-otp__slot:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.input-otp__slot:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.input-otp__slot:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.input-otp__slot:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.input-otp__slot:hover,.input-otp__slot[data-hovered=true]{background-color:var(--field-hover);border-color:var(--field-border-hover)}}.input-otp__slot[data-active=true]{z-index:10;background-color:var(--field-focus);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.input-otp__slot[data-filled=true]{background-color:var(--field-focus)}.input-otp__slot[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.input-otp__slot[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.input-otp__slot[data-invalid=true]:focus,.input-otp__slot[data-invalid=true]:focus-visible,.input-otp__slot[data-invalid=true][data-focused=true],.input-otp__slot[data-invalid=true][data-focus-visible=true],.input-otp__slot[data-invalid=true]:focus-within,.input-otp__slot[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.input-otp__slot[data-invalid=true]{background-color:var(--field-focus)}.input-otp__slot-value{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);--tw-tracking:-.27px;letter-spacing:-.27px;animation:slot-value-in .25s var(--ease-smooth) both;transform-origin:bottom}.input-otp__slot-value:is([data-reduce-motion=true],[data-reduce-motion=true] *),.input-otp__slot-value:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.input-otp__slot-value:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){.input-otp__slot-value:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.input-otp__slot-value:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.input-otp__slot-value:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.input-otp__caret{height:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * .5);background-color:var(--field-placeholder,var(--muted));width:2px;animation:1.2s ease-out infinite caret-blink;position:absolute}.input-otp__separator{border-radius:calc(var(--radius) * .5);background-color:var(--separator);flex-shrink:0;width:6px;height:2px}.input-otp--secondary .input-otp__slot{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--input-otp-slot-bg);--input-otp-slot-bg:var(--default);--input-otp-slot-bg-hover:var(--default-hover);--input-otp-slot-bg-focus:var(--default)}@media(hover:hover){.input-otp--secondary .input-otp__slot:hover,.input-otp--secondary .input-otp__slot[data-hovered=true]{background-color:var(--input-otp-slot-bg-hover)}}.input-otp--secondary .input-otp__slot[data-active=true],.input-otp--secondary .input-otp__slot[data-filled=true]{background-color:var(--input-otp-slot-bg-focus)}@keyframes slot-value-in{0%{opacity:0;transform:translateY(8px)scale(.8)}to{opacity:1;transform:translateY(0)scale(1)}}.input{border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-width:1px;outline-style:none}.input::placeholder{color:var(--field-placeholder,var(--muted))}@media(min-width:40rem){.input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.input{border-width:var(--border-width-field);border-color:var(--field-border);transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out)}.input:is([data-reduce-motion=true],[data-reduce-motion=true] *),.input:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.input:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.input:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.input:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.input:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.input:hover:not(:focus):not(:focus-visible),.input[data-hovered=true]:not([data-focused=true]):not([data-focus-visible=true]){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.input:focus,.input[data-focused=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;border-color:var(--field-border-focus);background-color:var(--field-focus);outline-style:none}.input[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.input[data-invalid=true]:focus,.input[data-invalid=true]:focus-visible,.input[data-invalid=true][data-focused=true],.input[data-invalid=true][data-focus-visible=true],.input[data-invalid=true]:focus-within,.input[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.input[data-invalid=true]{background-color:var(--field-focus)}.input:disabled,.input[data-disabled=true],.input[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.input--secondary{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--input-bg);--input-bg:var(--default);--input-bg-hover:var(--default-hover);--input-bg-focus:var(--default)}@media(hover:hover){.input--secondary:hover:not(:focus):not(:focus-visible),.input--secondary[data-hovered=true]:not([data-focus-visible=true]):not([data-focused=true]){background-color:var(--input-bg-hover)}}.input--secondary:focus,.input--secondary[data-focused=true]{background-color:var(--input-bg-focus)}.input--secondary[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.input--secondary[data-invalid=true]:focus,.input--secondary[data-invalid=true]:focus-visible,.input--secondary[data-invalid=true][data-focused=true],.input--secondary[data-invalid=true][data-focus-visible=true],.input--secondary[data-invalid=true]:focus-within,.input--secondary[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.input--secondary[data-invalid=true]{background-color:var(--input-bg-focus)}.input--full-width{width:100%}.input-group{min-height:calc(var(--spacing) * 9);border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border);outline-style:none;align-items:center;display:inline-flex}.input-group:has([data-slot=input-group-textarea]){align-items:flex-start;height:auto}.input-group{transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out)}.input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *),.input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.input-group:hover:not(:focus-within),.input-group[data-hovered=true]:not([data-focus-within=true]){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.input-group:has([data-slot=input-group-input]:focus),.input-group:has([data-slot=input-group-textarea]:focus){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;border-color:var(--field-border-focus);background-color:var(--field-focus);outline-style:none}.input-group[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.input-group[data-invalid=true]:focus,.input-group[data-invalid=true]:focus-visible,.input-group[data-invalid=true][data-focused=true],.input-group[data-invalid=true][data-focus-visible=true],.input-group[data-invalid=true]:focus-within,.input-group[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.input-group[data-invalid=true]{background-color:var(--field-focus);border-color:var(--color-field-border-invalid)}.input-group[data-disabled=true],.input-group[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:-webkit-any(.input-group:has([data-slot=input-group-input]:-webkit-autofill),.input-group:has([data-slot=input-group-input]:autofill)){background-color:var(--field-focus);border-color:var(--field-border-focus)}:is(.input-group:has([data-slot=input-group-input]:autofill)){background-color:var(--field-focus);border-color:var(--field-border-focus)}.input-group__input{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;background-color:#0000;border-width:0;border-radius:0;outline-style:none;flex:1}.input-group__input::placeholder{color:var(--field-placeholder,var(--muted))}@media(min-width:40rem){.input-group__input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.input-group:has([data-slot=input-group-prefix]) .input-group__input{border-start-start-radius:0;border-end-start-radius:0;padding-inline-start:0}.input-group:has([data-slot=input-group-suffix]) .input-group__input{border-start-end-radius:0;border-end-end-radius:0;padding-inline-end:0}.input-group__input:focus,.input-group__input:focus-visible{--tw-outline-style:none;outline-style:none}.input-group__input:-webkit-autofill{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.input-group__input:-webkit-autofill:hover{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.input-group__input:-webkit-autofill:focus{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.input-group__input:-webkit-autofill:active{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.input-group__input:autofill{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.input-group__input[data-slot=input-group-textarea]{resize:vertical;min-height:38px}.input-group__prefix{height:100%;padding-inline:calc(var(--spacing) * 3);color:var(--field-placeholder,var(--muted));border-width:var(--border-width-field);border-color:var(--field-border);border-style:solid;border-inline-end-color:var(--field-border);border-inline-start:none;background-color:#0000;border-top:none;border-bottom:none;border-start-start-radius:var(--field-radius,calc(var(--radius) * 1.5));border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:var(--field-radius,calc(var(--radius) * 1.5));justify-content:center;align-items:center;display:flex}.input-group:has([data-slot=input-group-textarea]) .input-group__prefix{align-items:flex-start;padding-top:.5rem}.input-group__prefix{transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth)}.input-group__prefix:is([data-reduce-motion=true],[data-reduce-motion=true] *),.input-group__prefix:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.input-group__prefix:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.input-group__prefix:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.input-group__prefix:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.input-group__prefix:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.input-group__suffix{height:100%;padding-inline:calc(var(--spacing) * 3);color:var(--field-placeholder,var(--muted));border-width:var(--border-width-field);border-color:var(--field-border);border-style:solid;border-inline-start-color:var(--field-border);border-inline-end:none;background-color:#0000;border-top:none;border-bottom:none;border-start-start-radius:0;border-start-end-radius:var(--field-radius,calc(var(--radius) * 1.5));border-end-end-radius:var(--field-radius,calc(var(--radius) * 1.5));border-end-start-radius:0;justify-content:center;align-items:center;display:flex}.input-group:has([data-slot=input-group-textarea]) .input-group__suffix{align-items:flex-start;padding-top:.5rem}.input-group__suffix{transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth)}.input-group__suffix:is([data-reduce-motion=true],[data-reduce-motion=true] *),.input-group__suffix:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.input-group__suffix:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.input-group__suffix:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.input-group__suffix:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.input-group__suffix:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.input-group--secondary{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--input-group-bg);--input-group-bg:var(--default);--input-group-bg-hover:var(--default-hover);--input-group-bg-focus:var(--default)}@media(hover:hover){.input-group--secondary:hover:not(:focus-within),.input-group--secondary[data-hovered=true]:not([data-focus-within=true]){background-color:var(--input-group-bg-hover)}}.input-group--secondary:has([data-slot=input-group-input]:focus),.input-group--secondary:has([data-slot=input-group-textarea]:focus){background-color:var(--input-group-bg-focus)}.input-group--secondary[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.input-group--secondary[data-invalid=true]:focus,.input-group--secondary[data-invalid=true]:focus-visible,.input-group--secondary[data-invalid=true][data-focused=true],.input-group--secondary[data-invalid=true][data-focus-visible=true],.input-group--secondary[data-invalid=true]:focus-within,.input-group--secondary[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.input-group--secondary[data-invalid=true]{border-color:var(--color-field-border-invalid);background-color:var(--input-group-bg-focus)}.input-group--secondary [data-slot=input-group-input],.input-group--secondary [data-slot=input-group-textarea]{background-color:#0000}.input-group--full-width{width:100%}.number-field{gap:var(--spacing);flex-direction:column;display:flex}:is(.number-field[data-invalid=true],.number-field[aria-invalid=true]) [data-slot=description]{display:none}.number-field [data-slot=label]{width:fit-content}.number-field__group{height:calc(var(--spacing) * 9);border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border);outline-style:none;grid-template-columns:1fr;align-items:center;display:grid;overflow:hidden}.number-field__group:has([slot=decrement]){grid-template-columns:40px 1fr}.number-field__group:has([slot=increment]){grid-template-columns:1fr 40px}.number-field__group:has([slot=decrement]):has([slot=increment]){grid-template-columns:40px 1fr 40px}.number-field__group{transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out)}.number-field__group:is([data-reduce-motion=true],[data-reduce-motion=true] *),.number-field__group:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.number-field__group:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.number-field__group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.number-field__group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.number-field__group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.number-field__group:hover:not(:focus-within),.number-field__group[data-hovered=true]:not([data-focus-within=true]){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.number-field__group[data-focus-within=true],.number-field__group:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;border-color:var(--field-border-focus);background-color:var(--field-focus);outline-style:none}.number-field__group[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.number-field__group[data-invalid=true]:focus,.number-field__group[data-invalid=true]:focus-visible,.number-field__group[data-invalid=true][data-focused=true],.number-field__group[data-invalid=true][data-focus-visible=true],.number-field__group[data-invalid=true]:focus-within,.number-field__group[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.number-field__group[data-invalid=true]{background-color:var(--field-focus);border-color:var(--color-field-border-invalid)}.number-field__group[data-disabled=true],.number-field__group[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:-webkit-any(.number-field__group:has([data-slot=number-field-input]:-webkit-autofill),.number-field__group:has([data-slot=number-field-input]:autofill)){background-color:var(--field-focus);border-color:var(--field-border-focus)}:is(.number-field__group:has([data-slot=number-field-input]:autofill)){background-color:var(--field-focus);border-color:var(--field-border-focus)}.number-field__input{border-style:var(--tw-border-style);min-width:0;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;background-color:#0000;border-width:0;border-radius:0;outline-style:none}@media(min-width:40rem){.number-field__input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.number-field__group:has([slot=decrement]) .number-field__input{border-start-start-radius:0;border-end-start-radius:0}.number-field__group:has([slot=increment]) .number-field__input{border-start-end-radius:0;border-end-end-radius:0}.number-field__input:focus,.number-field__input:focus-visible{--tw-outline-style:none;outline-style:none}.number-field__input:-webkit-autofill{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.number-field__input:-webkit-autofill:hover{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.number-field__input:-webkit-autofill:focus{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.number-field__input:-webkit-autofill:active{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.number-field__input:autofill{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.number-field__increment-button,.number-field__decrement-button{height:100%;width:calc(var(--spacing) * 10);color:var(--field-foreground,var(--foreground));--tw-outline-style:none;border-width:var(--border-width-field);border-color:var(--field-border);transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth);background-color:#0000;border-style:solid;border-radius:0;outline-style:none;justify-content:center;align-items:center;display:flex}:is(.number-field__increment-button,.number-field__decrement-button):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.number-field__increment-button,.number-field__decrement-button):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.number-field__increment-button,.number-field__decrement-button):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){:is(.number-field__increment-button,.number-field__decrement-button):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.number-field__increment-button,.number-field__decrement-button):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.number-field__increment-button,.number-field__decrement-button):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.number-field__increment-button,.number-field__decrement-button{cursor:var(--cursor-interactive)}:is(.number-field__increment-button,.number-field__decrement-button):active,:is(.number-field__increment-button,.number-field__decrement-button)[data-pressed=true]{background-color:var(--field-foreground,var(--foreground))}@supports (color:color-mix(in lab,red,red)){:is(.number-field__increment-button,.number-field__decrement-button):active,:is(.number-field__increment-button,.number-field__decrement-button)[data-pressed=true]{background-color:color-mix(in oklab,var(--field-foreground,var(--foreground)) 10%,transparent)}}:is(.number-field__increment-button,.number-field__decrement-button):active,:is(.number-field__increment-button,.number-field__decrement-button)[data-pressed=true]{transform:scale(.97)}:is(.number-field__increment-button,.number-field__decrement-button):disabled,:is(.number-field__increment-button,.number-field__decrement-button)[data-disabled=true],:is(.number-field__increment-button,.number-field__decrement-button)[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:is(.number-field__increment-button,.number-field__decrement-button) [data-slot=number-field-increment-button-icon],:is(.number-field__increment-button,.number-field__decrement-button) [data-slot=number-field-decrement-button-icon]{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.number-field__increment-button{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px;border-color:var(--field-placeholder,var(--muted));border-start-start-radius:0;border-start-end-radius:var(--field-radius,calc(var(--radius) * 1.5));border-end-end-radius:var(--field-radius,calc(var(--radius) * 1.5));border-end-start-radius:0}@supports (color:color-mix(in lab,red,red)){.number-field__increment-button{border-color:color-mix(in oklab,var(--field-placeholder,var(--muted)) 15%,transparent)}}.number-field__decrement-button{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px;border-color:var(--field-placeholder,var(--muted));border-start-start-radius:var(--field-radius,calc(var(--radius) * 1.5));border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:var(--field-radius,calc(var(--radius) * 1.5))}@supports (color:color-mix(in lab,red,red)){.number-field__decrement-button{border-color:color-mix(in oklab,var(--field-placeholder,var(--muted)) 15%,transparent)}}.number-field--secondary .number-field__group{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--number-field-group-bg);--number-field-group-bg:var(--default);--number-field-group-bg-hover:var(--default-hover);--number-field-group-bg-focus:var(--default)}@media(hover:hover){.number-field--secondary .number-field__group:hover:not(:focus-within),.number-field--secondary .number-field__group[data-hovered=true]:not([data-focus-within=true]){background-color:var(--number-field-group-bg-hover)}}.number-field--secondary .number-field__group:focus-within,.number-field--secondary .number-field__group[data-focus-within=true]{background-color:var(--number-field-group-bg-focus)}.number-field--secondary .number-field__group[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.number-field--secondary .number-field__group[data-invalid=true]:focus,.number-field--secondary .number-field__group[data-invalid=true]:focus-visible,.number-field--secondary .number-field__group[data-invalid=true][data-focused=true],.number-field--secondary .number-field__group[data-invalid=true][data-focus-visible=true],.number-field--secondary .number-field__group[data-invalid=true]:focus-within,.number-field--secondary .number-field__group[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.number-field--secondary .number-field__group[data-invalid=true]{border-color:var(--color-field-border-invalid);background-color:var(--number-field-group-bg-focus)}.number-field--secondary .number-field__group [data-slot=number-field-input]{background-color:#0000}.number-field--full-width,.number-field__group--full-width{width:100%}.radio-group{flex-direction:column;display:flex}.radio-group[data-orientation=vertical] [data-slot=radio]{margin-top:calc(var(--spacing) * 4)}.radio-group[data-orientation=horizontal]{gap:calc(var(--spacing) * 4);flex-flow:wrap}.radio-group--secondary .radio__control{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--radio-control-bg);--radio-control-bg:var(--default);--radio-control-bg-hover:var(--default-hover)}.radio:has([data-slot=radio-content][data-hovered=true]) :is(.radio-group--secondary .radio__control),.radio [data-slot=radio-content][data-hovered=true] :is(.radio-group--secondary .radio__control){border-color:var(--field-border-hover)}.radio:not([data-selected]):not(:has(input:checked)) :is(.radio-group--secondary .radio__control) .radio__indicator:empty:before{background-color:var(--radio-control-bg)}.radio:has([data-slot=radio-content][data-hovered=true]):not([data-selected]):not(:has(input:checked)) :is(.radio-group--secondary .radio__control) .radio__indicator:empty:before{background-color:var(--radio-control-bg-hover)}.radio{align-items:flex-start;gap:var(--spacing);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);outline-style:none;flex-direction:column;display:flex}.radio [data-slot=label]{-webkit-user-select:none;user-select:none}.radio .radio__content [data-slot=label]{cursor:var(--cursor-interactive)}.radio>[data-slot=description],.radio>[data-slot=field-error]{cursor:default;width:100%;min-width:0;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));text-wrap:wrap;overflow-wrap:break-word;color:var(--muted);-webkit-user-select:none;user-select:none;padding-inline-start:calc(var(--spacing) * 7)}.radio:disabled,.radio[data-disabled=true],.radio[aria-disabled=true],:is(.radio:disabled,.radio[data-disabled=true],.radio[aria-disabled=true]) [data-slot=description],:is(.radio:disabled,.radio[data-disabled=true],.radio[aria-disabled=true]) [data-slot=field-error]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.radio__content{cursor:inherit;align-items:center;gap:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline-style:none;display:inline-flex;position:relative}.radio__control{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * 1);border-style:var(--tw-border-style);border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border,var(--border));background-color:var(--field-background,var(--default));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;transition:background-color .2s var(--ease-out),border-color .2s var(--ease-out),transform .1s var(--ease-out);outline-style:none;flex-shrink:0;justify-content:center;align-items:center;display:inline-flex;position:relative}.radio__control:is([data-reduce-motion=true],[data-reduce-motion=true] *),.radio__control:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.radio__control:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.radio__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.radio__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.radio__control:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.radio__control{cursor:var(--cursor-interactive)}.radio:has([data-slot=radio-content][data-focus-visible=true]) .radio__control,.radio [data-slot=radio-content][data-focus-visible=true] .radio__control{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.radio:has([data-slot=radio-content][data-hovered=true]) .radio__control,.radio [data-slot=radio-content][data-hovered=true] .radio__control{border-color:var(--field-border-hover)}.radio:has([data-slot=radio-content][data-hovered=true]):not([data-selected]):not(:has(input:checked)) .radio__control .radio__indicator:empty:before{background-color:var(--field-hover)}.radio:has([data-slot=radio-content][data-pressed=true]) .radio__control,.radio [data-slot=radio-content][data-pressed=true] .radio__control{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.radio[data-selected] .radio__control,.radio:has([data-slot=radio-content][aria-checked=true]) .radio__control,.radio:has(input:checked) .radio__control{background-color:var(--accent);border-color:#0000}.radio[data-selected]:has([data-slot=radio-content][data-pressed=true]) .radio__control,.radio:has([data-slot=radio-content][data-pressed=true][aria-checked=true]) .radio__control,.radio:has(input:checked):has([data-slot=radio-content][data-pressed=true]) .radio__control{background-color:var(--accent-hover)}.radio[data-invalid=true] .radio__control,.radio[aria-invalid=true] .radio__control{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}:is(.radio[data-invalid=true] .radio__control,.radio[aria-invalid=true] .radio__control):focus,:is(.radio[data-invalid=true] .radio__control,.radio[aria-invalid=true] .radio__control):focus-visible,:is(.radio[data-invalid=true] .radio__control,.radio[aria-invalid=true] .radio__control)[data-focused=true],:is(.radio[data-invalid=true] .radio__control,.radio[aria-invalid=true] .radio__control)[data-focus-visible=true],:is(.radio[data-invalid=true] .radio__control,.radio[aria-invalid=true] .radio__control):focus-within,:is(.radio[data-invalid=true] .radio__control,.radio[aria-invalid=true] .radio__control)[data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.radio[data-invalid=true][data-selected] .radio__control,.radio[aria-invalid=true][data-selected] .radio__control,.radio[data-invalid=true]:has(input:checked) .radio__control,.radio[aria-invalid=true]:has(input:checked) .radio__control{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}:is(.radio[data-invalid=true][data-selected] .radio__control,.radio[aria-invalid=true][data-selected] .radio__control,.radio[data-invalid=true]:has(input:checked) .radio__control,.radio[aria-invalid=true]:has(input:checked) .radio__control):focus,:is(.radio[data-invalid=true][data-selected] .radio__control,.radio[aria-invalid=true][data-selected] .radio__control,.radio[data-invalid=true]:has(input:checked) .radio__control,.radio[aria-invalid=true]:has(input:checked) .radio__control):focus-visible,:is(.radio[data-invalid=true][data-selected] .radio__control,.radio[aria-invalid=true][data-selected] .radio__control,.radio[data-invalid=true]:has(input:checked) .radio__control,.radio[aria-invalid=true]:has(input:checked) .radio__control)[data-focused=true],:is(.radio[data-invalid=true][data-selected] .radio__control,.radio[aria-invalid=true][data-selected] .radio__control,.radio[data-invalid=true]:has(input:checked) .radio__control,.radio[aria-invalid=true]:has(input:checked) .radio__control)[data-focus-visible=true],:is(.radio[data-invalid=true][data-selected] .radio__control,.radio[aria-invalid=true][data-selected] .radio__control,.radio[data-invalid=true]:has(input:checked) .radio__control,.radio[aria-invalid=true]:has(input:checked) .radio__control):focus-within,:is(.radio[data-invalid=true][data-selected] .radio__control,.radio[aria-invalid=true][data-selected] .radio__control,.radio[data-invalid=true]:has(input:checked) .radio__control,.radio[aria-invalid=true]:has(input:checked) .radio__control)[data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.radio__indicator{pointer-events:none;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.radio__indicator:empty:before{content:"";border-radius:calc(var(--radius) * 1);background-color:var(--field-background,var(--default));width:100%;height:100%;transition:scale .2s var(--ease-out),background-color .2s var(--ease-out);scale:1}@media(prefers-reduced-motion:reduce){.radio__indicator:empty:before:not(:is()){transition-property:none}}.radio[data-selected] .radio__indicator:empty:before,.radio:has([data-slot=radio-content][aria-checked=true]) .radio__indicator:empty:before,.radio:has(input:checked) .radio__indicator:empty:before{background-color:var(--accent-foreground);scale:.4286}.radio[data-selected]:has([data-slot=radio-content][data-pressed=true]) .radio__indicator:empty:before,.radio:has([data-slot=radio-content][data-pressed=true][aria-checked=true]) .radio__indicator:empty:before,.radio:has(input:checked):has([data-slot=radio-content][data-pressed=true]) .radio__indicator:empty:before{scale:.5714}.textfield{gap:var(--spacing);flex-direction:column;display:flex}:is(.textfield[data-invalid=true],.textfield[aria-invalid=true]) [data-slot=description]{display:none}.textfield--full-width,.textfield--full-width [data-slot=input],.textfield--full-width [data-slot=textarea]{width:100%}.search-field{gap:var(--spacing);flex-direction:column;display:flex}:is(.search-field[data-invalid=true],.search-field[aria-invalid=true]) [data-slot=description]{display:none}.search-field [data-slot=label]{width:fit-content}.search-field[data-empty=true] [data-slot=search-field-clear-button]{pointer-events:none;opacity:0}.search-field__group{height:calc(var(--spacing) * 9);border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border);transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);outline-style:none;align-items:center;display:inline-flex;position:relative;overflow:hidden}.search-field__group:is([data-reduce-motion=true],[data-reduce-motion=true] *),.search-field__group:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.search-field__group:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.search-field__group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.search-field__group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.search-field__group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.search-field__group:hover:not(:focus-within),.search-field__group[data-hovered=true]:not([data-focus-within=true]){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.search-field__group[data-focus-within=true],.search-field__group:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.search-field__group[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.search-field__group[data-invalid=true]:focus,.search-field__group[data-invalid=true]:focus-visible,.search-field__group[data-invalid=true][data-focused=true],.search-field__group[data-invalid=true][data-focus-visible=true],.search-field__group[data-invalid=true]:focus-within,.search-field__group[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.search-field__group[data-invalid=true]{background-color:var(--field-focus);border-color:var(--color-field-border-invalid)}.search-field__group[data-disabled=true],.search-field__group[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}:-webkit-any(.search-field__group:has([data-slot=search-field-input]:-webkit-autofill),.search-field__group:has([data-slot=search-field-input]:autofill)){background-color:var(--field-focus);border-color:var(--field-border-focus)}:is(.search-field__group:has([data-slot=search-field-input]:autofill)){background-color:var(--field-focus);border-color:var(--field-border-focus)}.search-field__input{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;background-color:#0000;border-width:0;border-radius:0;outline-style:none;flex:1}@media(min-width:40rem){.search-field__input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.search-field__input::-webkit-search-cancel-button{appearance:none}.search-field__input::-webkit-search-decoration{appearance:none}.search-field__group:has([data-slot=search-field-search-icon]) .search-field__input{border-start-start-radius:0;border-end-start-radius:0;padding-inline-start:calc(var(--spacing) * 2)}.search-field__group:has([slot=clear]) .search-field__input{border-start-end-radius:0;border-end-end-radius:0;padding-inline-end:calc(var(--spacing) * 2)}.search-field__input:focus,.search-field__input:focus-visible{--tw-outline-style:none;outline-style:none}.search-field__input:-webkit-autofill{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.search-field__input:-webkit-autofill:hover{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.search-field__input:-webkit-autofill:focus{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.search-field__input:-webkit-autofill:active{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.search-field__input:autofill{-webkit-text-fill-color:var(--field-foreground);caret-color:var(--field-foreground);transition:background-color 9999s ease-in-out;box-shadow:inset 0 0 0 1000px #0000}.search-field__search-icon{pointer-events:none;color:var(--field-placeholder,var(--muted));width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);flex-shrink:0;margin-inline-start:calc(var(--spacing) * 3);margin-inline-end:0}.search-field__clear-button{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5);flex-shrink:0;margin-inline-end:calc(var(--spacing) * 2)}.search-field__clear-button [data-slot=close-button-icon]{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.search-field--secondary .search-field__group{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--search-field-group-bg);--search-field-group-bg:var(--default);--search-field-group-bg-hover:var(--default-hover);--search-field-group-bg-focus:var(--default)}@media(hover:hover){.search-field--secondary .search-field__group:hover:not(:focus-within),.search-field--secondary .search-field__group[data-hovered=true]:not([data-focus-within=true]){background-color:var(--search-field-group-bg-hover)}}.search-field--secondary .search-field__group:focus-within,.search-field--secondary .search-field__group[data-focus-within=true]{background-color:var(--search-field-group-bg-focus)}.search-field--secondary .search-field__group[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.search-field--secondary .search-field__group[data-invalid=true]:focus,.search-field--secondary .search-field__group[data-invalid=true]:focus-visible,.search-field--secondary .search-field__group[data-invalid=true][data-focused=true],.search-field--secondary .search-field__group[data-invalid=true][data-focus-visible=true],.search-field--secondary .search-field__group[data-invalid=true]:focus-within,.search-field--secondary .search-field__group[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.search-field--secondary .search-field__group[data-invalid=true]{border-color:var(--color-field-border-invalid);background-color:var(--search-field-group-bg-focus)}.search-field--secondary .search-field__group [data-slot=search-field-input]{background-color:#0000}.search-field--full-width,.search-field__group--full-width{width:100%}.textarea{border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-width:1px;outline-style:none}.textarea::placeholder{color:var(--field-placeholder,var(--muted))}@media(min-width:40rem){.textarea{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.textarea{border-width:var(--border-width-field);border-color:var(--field-border);min-height:38px;transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out)}.textarea:is([data-reduce-motion=true],[data-reduce-motion=true] *),.textarea:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.textarea:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.textarea:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.textarea:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.textarea:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.textarea:hover:not(:focus):not(:focus-visible),.textarea[data-hovered=true]:not([data-focused=true]):not([data-focus-visible=true]){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.textarea:focus,.textarea[data-focused=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;border-color:var(--field-border-focus);background-color:var(--field-focus);outline-style:none}.textarea[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.textarea[data-invalid=true]:focus,.textarea[data-invalid=true]:focus-visible,.textarea[data-invalid=true][data-focused=true],.textarea[data-invalid=true][data-focus-visible=true],.textarea[data-invalid=true]:focus-within,.textarea[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.textarea[data-invalid=true]{background-color:var(--field-focus)}.textarea:disabled,.textarea[data-disabled=true],.textarea[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.textarea--secondary{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--textarea-bg);--textarea-bg:var(--default);--textarea-bg-hover:var(--default-hover);--textarea-bg-focus:var(--default)}@media(hover:hover){.textarea--secondary:hover:not(:focus):not(:focus-visible),.textarea--secondary[data-hovered=true]:not([data-focused=true]):not([data-focus-visible=true]){background-color:var(--textarea-bg-hover)}}.textarea--secondary:focus,.textarea--secondary[data-focused=true]{background-color:var(--textarea-bg-focus)}.textarea--secondary[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.textarea--secondary[data-invalid=true]:focus,.textarea--secondary[data-invalid=true]:focus-visible,.textarea--secondary[data-invalid=true][data-focused=true],.textarea--secondary[data-invalid=true][data-focus-visible=true],.textarea--secondary[data-invalid=true]:focus-within,.textarea--secondary[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.textarea--secondary[data-invalid=true]{background-color:var(--textarea-bg-focus)}.textarea--full-width{width:100%}.calendar{width:calc(var(--spacing) * 63);max-width:calc(var(--spacing) * 63);container-type:inline-size}.calendar--week-view .calendar__cell,.calendar--day-view .calendar__cell{aspect-ratio:1;place-self:center;width:100%;height:auto}.calendar--day-view .calendar__grid{flex-direction:column;display:flex}.calendar--day-view .calendar__grid-header{grid-template-columns:repeat(7,1fr);width:100%;display:grid}.calendar--day-view .calendar__grid-header>tr{display:contents}.calendar--day-view .calendar__grid-body{margin-top:var(--spacing);grid-template-columns:repeat(7,1fr);width:100%;display:grid}.calendar--day-view .calendar__grid-body>tr{display:contents}.calendar--day-view .calendar__grid-body>tr:first-child>td{margin-top:0}.calendar__header{padding-inline:calc(var(--spacing) * .5);padding-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.calendar__header:has(.calendar-year-picker__trigger[data-open=true]) .calendar__nav-button{pointer-events:none;opacity:0}.calendar__heading{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);flex:1}.calendar__nav-button{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6);border-radius:calc(var(--radius) * 2);color:var(--accent-soft-foreground);will-change:scale;transition:transform .25s var(--ease-out),background-color .1s var(--ease-out),box-shadow .1s var(--ease-out),opacity .15s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);justify-content:center;align-items:center;display:flex}.calendar__nav-button:is([data-reduce-motion=true],[data-reduce-motion=true] *),.calendar__nav-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.calendar__nav-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.calendar__nav-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.calendar__nav-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.calendar__nav-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar__nav-button{cursor:var(--cursor-interactive)}@media(hover:hover){.calendar__nav-button:hover,.calendar__nav-button[data-hovered=true]{background-color:var(--default);color:var(--accent-soft-foreground)}}.calendar__nav-button:active,.calendar__nav-button[data-pressed=true]{transform:scale(.95)}.calendar__nav-button:focus-visible,.calendar__nav-button[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.calendar__nav-button:disabled,.calendar__nav-button[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.calendar__nav-button-icon{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.calendar__nav-button-icon:where(:dir(rtl),[dir=rtl],[dir=rtl] *){rotate:180deg}.calendar__grid{grid-template-columns:repeat(7,1fr);width:100%;display:grid}.calendar__grid[aria-readonly=true] .calendar__cell{pointer-events:none}.calendar__grid-header,.calendar__grid-header>tr,.calendar__grid-body,.calendar__grid-body>tr{display:contents}.calendar__grid-body>tr:first-child>td{margin-top:var(--spacing)}.calendar__grid-row{display:contents}.calendar__header-cell{padding-bottom:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted);justify-content:center;align-items:center;display:flex}.calendar__cell{aspect-ratio:1;border-radius:calc(var(--radius) * 3);text-align:center;width:100%;height:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-outline-style:none;-webkit-tap-highlight-color:transparent;will-change:scale;transition:transform .25s var(--ease-out),box-shadow .1s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);outline-style:none;justify-content:center;align-items:center;display:flex;position:relative}.calendar__cell:is([data-reduce-motion=true],[data-reduce-motion=true] *),.calendar__cell:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.calendar__cell:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.calendar__cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.calendar__cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.calendar__cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar__cell{cursor:var(--cursor-interactive)}.calendar__cell:focus-visible:not([data-focused]),.calendar__cell[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.calendar__cell[data-today=true]{background-color:var(--accent-soft);color:var(--accent-soft-foreground)}@media(hover:hover){.calendar__cell[data-today=true]:hover:not([data-selected=true]),.calendar__cell[data-today=true][data-hovered=true]:not([data-selected=true]){background-color:var(--accent-soft-hover)}}.calendar__cell[data-selected=true]{background-color:var(--accent);color:var(--accent-foreground)}.calendar__cell:active,.calendar__cell[data-pressed=true]{background-color:var(--default);transform:scale(.95)}:is(.calendar__cell:active,.calendar__cell[data-pressed=true])[data-selected=true]{background-color:var(--accent-hover)}@media(hover:hover){.calendar__cell:hover:not([data-selected=true]),.calendar__cell[data-hovered=true]:not([data-selected=true]){background-color:var(--default)}}.calendar__cell[data-outside-month=true]{color:var(--muted);opacity:.5}.calendar__cell[data-selected=true][data-outside-month=true]{background-color:var(--default)}.calendar__cell[data-unavailable=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.calendar__cell:disabled:not([data-outside-month=true]),.calendar__cell[data-disabled=true]:not([data-outside-month=true]){opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none;text-decoration:line-through}.calendar__cell-indicator{bottom:var(--spacing);--tw-translate-x: -50% ;width:3px;height:3px;translate:var(--tw-translate-x) var(--tw-translate-y);border-radius:calc(var(--radius) * .25);background-color:var(--muted);position:absolute;left:50%}[data-selected=true]>.calendar__cell-indicator{background-color:var(--accent-foreground)}.range-calendar{width:calc(var(--spacing) * 63);max-width:calc(var(--spacing) * 63);container-type:inline-size}.range-calendar--week-view .range-calendar__cell,.range-calendar--day-view .range-calendar__cell{aspect-ratio:1;place-self:center;width:100%;height:auto}.range-calendar--day-view .range-calendar__grid{flex-direction:column;display:flex}.range-calendar--day-view .range-calendar__grid-header{grid-template-columns:repeat(7,1fr);width:100%;display:grid}.range-calendar--day-view .range-calendar__grid-header>tr{display:contents}.range-calendar--day-view .range-calendar__grid-body{margin-top:var(--spacing);grid-template-columns:repeat(7,1fr);width:100%;display:grid}.range-calendar--day-view .range-calendar__grid-body>tr{display:contents}.range-calendar--day-view .range-calendar__grid-body>tr:first-child>td{margin-top:0}.range-calendar__header{padding-inline:calc(var(--spacing) * .5);padding-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.range-calendar__header:has(.calendar-year-picker__trigger[data-open=true]) .range-calendar__nav-button{pointer-events:none;opacity:0}.range-calendar__heading{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);flex:1}.range-calendar__nav-button{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6);border-radius:calc(var(--radius) * 1.5);color:var(--accent-soft-foreground);will-change:scale;transition:transform .25s var(--ease-out),background-color .1s var(--ease-out),box-shadow .1s var(--ease-out),opacity .15s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);justify-content:center;align-items:center;display:flex}.range-calendar__nav-button:is([data-reduce-motion=true],[data-reduce-motion=true] *),.range-calendar__nav-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.range-calendar__nav-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.range-calendar__nav-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.range-calendar__nav-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.range-calendar__nav-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.range-calendar__nav-button{cursor:var(--cursor-interactive)}@media(hover:hover){.range-calendar__nav-button:hover,.range-calendar__nav-button[data-hovered=true]{background-color:var(--default);color:var(--accent-soft-foreground)}}.range-calendar__nav-button:active,.range-calendar__nav-button[data-pressed=true]{transform:scale(.95)}.range-calendar__nav-button:focus-visible,.range-calendar__nav-button[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.range-calendar__nav-button:disabled,.range-calendar__nav-button[data-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.range-calendar__nav-button-icon{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.range-calendar__nav-button-icon:where(:dir(rtl),[dir=rtl],[dir=rtl] *){rotate:180deg}.range-calendar__grid{grid-template-columns:repeat(7,1fr);width:100%;display:grid}.range-calendar__grid[aria-readonly=true] .range-calendar__cell{pointer-events:none}.range-calendar__grid-header,.range-calendar__grid-header>tr,.range-calendar__grid-body,.range-calendar__grid-body>tr{display:contents}.range-calendar__grid-body>tr:first-child>td{margin-top:var(--spacing)}.range-calendar__grid-row{display:contents}.range-calendar__header-cell{padding-bottom:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted);justify-content:center;align-items:center;display:flex}.range-calendar__cell{z-index:1;border-radius:calc(var(--radius) * 3);--tw-outline-style:none;cursor:var(--cursor-interactive);will-change:background-color,border-color;transition:box-shadow .1s var(--ease-out),border-color .1s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);outline-style:none;margin-block:2px;margin-inline:0;padding:0;position:relative}.range-calendar__cell:is([data-reduce-motion=true],[data-reduce-motion=true] *),.range-calendar__cell:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.range-calendar__cell:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.range-calendar__cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.range-calendar__cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.range-calendar__cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.range-calendar__cell .range-calendar__cell-button{aspect-ratio:1;border-radius:calc(var(--radius) * 3);width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);-webkit-tap-highlight-color:transparent;will-change:scale;transition:scale .2s var(--ease-out);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);justify-content:center;align-items:center;display:flex}.range-calendar__cell .range-calendar__cell-button:is([data-reduce-motion=true],[data-reduce-motion=true] *),.range-calendar__cell .range-calendar__cell-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.range-calendar__cell .range-calendar__cell-button:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.range-calendar__cell .range-calendar__cell-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.range-calendar__cell .range-calendar__cell-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.range-calendar__cell .range-calendar__cell-button:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.range-calendar__cell:focus-visible:not([data-focused]),.range-calendar__cell[data-focus-visible=true]{z-index:2}:is(.range-calendar__cell:focus-visible:not([data-focused]),.range-calendar__cell[data-focus-visible=true]) .range-calendar__cell-button{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.range-calendar__cell[data-today=true] .range-calendar__cell-button{background-color:var(--accent-soft);color:var(--accent-soft-foreground)}@media(hover:hover){:is(.range-calendar__cell[data-today=true]:hover:not([data-selected=true]),.range-calendar__cell[data-today=true][data-hovered=true]:not([data-selected=true])) .range-calendar__cell-button{background-color:var(--accent-soft-hover)}}.range-calendar__cell[data-selected=true]:not([data-outside-month=true]){background-color:var(--accent-soft);border-radius:0}.range-calendar__cell[data-selected=true]:is(td:first-child>*,[aria-disabled]+td>*){border-start-start-radius:calc(var(--radius) * 1);border-end-start-radius:calc(var(--radius) * 1)}.range-calendar__cell[data-selected=true]:is(td:first-child>*,[aria-disabled]+td>*)[data-selection-start=true]{border-start-start-radius:calc(var(--radius) * 3);border-end-start-radius:calc(var(--radius) * 3)}.range-calendar__cell[data-selected=true]:is(td:last-child>*,td:has(+[aria-disabled])>*){border-start-end-radius:calc(var(--radius) * 1);border-end-end-radius:calc(var(--radius) * 1)}.range-calendar__cell[data-selected=true]:is(td:last-child>*,td:has(+[aria-disabled])>*)[data-selection-end=true]{border-start-end-radius:calc(var(--radius) * 3);border-end-end-radius:calc(var(--radius) * 3)}.range-calendar__cell[data-selection-start=true]:not([data-outside-month=true]),.range-calendar__cell[data-selection-end=true]:not([data-outside-month=true]){z-index:2}:is(.range-calendar__cell[data-selection-start=true]:not([data-outside-month=true]),.range-calendar__cell[data-selection-end=true]:not([data-outside-month=true])) .range-calendar__cell-button{background-color:var(--accent);color:var(--accent-foreground)}.range-calendar__cell[data-selection-start=true]:not([data-outside-month=true]){border-start-start-radius:calc(var(--radius) * 3);border-end-start-radius:calc(var(--radius) * 3)}.range-calendar__cell[data-selection-end=true]:not([data-outside-month=true]){border-start-end-radius:calc(var(--radius) * 3);border-end-end-radius:calc(var(--radius) * 3)}:is(.range-calendar__cell:active,.range-calendar__cell[data-pressed=true]) .range-calendar__cell-button{scale:.9}:is(:is(.range-calendar__cell:active,.range-calendar__cell[data-pressed=true])[data-selection-start=true],:is(.range-calendar__cell:active,.range-calendar__cell[data-pressed=true])[data-selection-end=true]) .range-calendar__cell-button{background-color:var(--accent-hover)}@media(hover:hover){:is(.range-calendar__cell:hover:not([data-selected=true]),.range-calendar__cell[data-hovered=true]:not([data-selected=true])) .range-calendar__cell-button{background-color:var(--default)}}.range-calendar__cell[data-outside-month=true]{color:var(--muted);opacity:.5}.range-calendar__cell[data-selected=true][data-outside-month=true]:not([data-selection-start=true],[data-selection-end=true]){background-color:var(--default)}@supports (color:color-mix(in lab,red,red)){.range-calendar__cell[data-selected=true][data-outside-month=true]:not([data-selection-start=true],[data-selection-end=true]){background-color:color-mix(in oklab,var(--default) 20%,transparent)}}.range-calendar__cell[data-unavailable=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.range-calendar__cell:disabled:not([data-outside-month=true]),.range-calendar__cell[data-disabled=true]:not([data-outside-month=true]){opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none;text-decoration:line-through}.range-calendar__grid-body td:has(>.range-calendar__cell[data-outside-month=true])+td>.range-calendar__cell[data-selected=true]{border-start-start-radius:calc(var(--radius) * 1);border-end-start-radius:calc(var(--radius) * 1)}.range-calendar__grid-body td:has(>.range-calendar__cell[data-outside-month=true])+td>.range-calendar__cell[data-selected=true][data-outside-month=true],.range-calendar__grid-body td:has(>.range-calendar__cell[data-outside-month=true])+td>.range-calendar__cell[data-selected=true][data-selection-start=true]{border-start-start-radius:calc(var(--radius) * 3);border-end-start-radius:calc(var(--radius) * 3)}.range-calendar__grid-body td:has(>.range-calendar__cell[data-selected=true]):has(+td>.range-calendar__cell[data-outside-month=true])>.range-calendar__cell[data-selected=true]{border-start-end-radius:calc(var(--radius) * 1);border-end-end-radius:calc(var(--radius) * 1)}.range-calendar__grid-body td:has(>.range-calendar__cell[data-selected=true]):has(+td>.range-calendar__cell[data-outside-month=true])>.range-calendar__cell[data-selected=true][data-outside-month=true],.range-calendar__grid-body td:has(>.range-calendar__cell[data-selected=true]):has(+td>.range-calendar__cell[data-outside-month=true])>.range-calendar__cell[data-selected=true][data-selection-end=true]{border-start-end-radius:calc(var(--radius) * 3);border-end-end-radius:calc(var(--radius) * 3)}.range-calendar__cell-indicator{bottom:var(--spacing);--tw-translate-x: -50% ;width:3px;height:3px;translate:var(--tw-translate-x) var(--tw-translate-y);border-radius:calc(var(--radius) * .25);background-color:var(--muted);position:absolute;left:50%}[data-selected=true]>.range-calendar__cell-indicator{background-color:var(--accent-foreground)}.calendar:has(.calendar-year-picker__year-grid),.range-calendar:has(.calendar-year-picker__year-grid){position:relative}.calendar:has(.calendar-year-picker__year-grid)>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid)>[data-slot=range-calendar-grid]{will-change:opacity;transition:opacity .15s var(--ease-out),visibility 0s linear}:is(.calendar:has(.calendar-year-picker__year-grid)>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid)>[data-slot=range-calendar-grid]):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.calendar:has(.calendar-year-picker__year-grid)>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid)>[data-slot=range-calendar-grid]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.calendar:has(.calendar-year-picker__year-grid)>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid)>[data-slot=range-calendar-grid]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){:is(.calendar:has(.calendar-year-picker__year-grid)>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid)>[data-slot=range-calendar-grid]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.calendar:has(.calendar-year-picker__year-grid)>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid)>[data-slot=range-calendar-grid]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.calendar:has(.calendar-year-picker__year-grid)>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid)>[data-slot=range-calendar-grid]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=range-calendar-grid]{pointer-events:none;opacity:0;visibility:hidden;transition:opacity .15s var(--ease-out),visibility 0s linear .15s}:is(.calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=range-calendar-grid]):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=range-calendar-grid]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=range-calendar-grid]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){:is(.calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=range-calendar-grid]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=range-calendar-grid]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=calendar-grid],.range-calendar:has(.calendar-year-picker__year-grid[data-open=true])>[data-slot=range-calendar-grid]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar-year-picker__trigger{justify-content:flex-start;align-items:center;gap:var(--spacing);border-radius:calc(var(--radius) * 1);--tw-outline-style:none;cursor:var(--cursor-interactive);touch-action:manipulation;outline-style:none;flex:1;display:flex}.calendar-year-picker__trigger:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.calendar-year-picker__trigger-heading{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition:color .15s var(--ease-out)}.calendar-year-picker__trigger-heading:is([data-reduce-motion=true],[data-reduce-motion=true] *),.calendar-year-picker__trigger-heading:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.calendar-year-picker__trigger-heading:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.calendar-year-picker__trigger-heading:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.calendar-year-picker__trigger-heading:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.calendar-year-picker__trigger-heading:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar-year-picker__trigger-indicator{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--accent-soft-foreground)}.calendar-year-picker__trigger-indicator:where(:dir(rtl),[dir=rtl],[dir=rtl] *){transform:rotate(180deg)}.calendar-year-picker__trigger-indicator{transition:transform .15s var(--ease-out)}.calendar-year-picker__trigger-indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *),.calendar-year-picker__trigger-indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.calendar-year-picker__trigger-indicator:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.calendar-year-picker__trigger-indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.calendar-year-picker__trigger-indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.calendar-year-picker__trigger-indicator:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar-year-picker__trigger[data-open=true] .calendar-year-picker__trigger-indicator{transform:rotate(90deg)}.calendar-year-picker__trigger[data-open=true] .calendar-year-picker__trigger-heading{color:var(--accent-soft-foreground)}.calendar-year-picker__year-grid{pointer-events:none;scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);align-content:flex-start;gap:var(--spacing);padding:var(--spacing);opacity:0;will-change:opacity;grid-template-columns:repeat(3,1fr);display:grid;position:absolute;inset-inline-start:calc(var(--spacing) * 0);inset-inline-end:calc(var(--spacing) * 0);overflow-y:auto}.calendar-year-picker__year-grid[data-open=true]{pointer-events:auto;opacity:1;transition:opacity .2s var(--ease-out) 50ms}.calendar-year-picker__year-grid[data-open=true]:is([data-reduce-motion=true],[data-reduce-motion=true] *),.calendar-year-picker__year-grid[data-open=true]:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.calendar-year-picker__year-grid[data-open=true]:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.calendar-year-picker__year-grid[data-open=true]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.calendar-year-picker__year-grid[data-open=true]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.calendar-year-picker__year-grid[data-open=true]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar-year-picker__year-cell{height:calc(var(--spacing) * 8);border-radius:calc(var(--radius) * 3);padding-inline:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation;transition:color .1s var(--ease-smooth),scale .1s var(--ease-smooth),opacity .1s var(--ease-smooth),background-color .1s var(--ease-smooth),box-shadow .1s var(--ease-out);transform-origin:50%;transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);outline-style:none;justify-content:center;align-items:center;display:inline-flex;position:relative}.calendar-year-picker__year-cell:is([data-reduce-motion=true],[data-reduce-motion=true] *),.calendar-year-picker__year-cell:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.calendar-year-picker__year-cell:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.calendar-year-picker__year-cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.calendar-year-picker__year-cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.calendar-year-picker__year-cell:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.calendar-year-picker__year-cell{cursor:var(--cursor-interactive)}@media(hover:hover)and (pointer:fine){.calendar-year-picker__year-cell:is(:hover,[data-hovered=true]):not([data-selected=true]){background-color:var(--default);color:var(--default-foreground)}}.calendar-year-picker__year-cell[data-selected=true],.calendar-year-picker__year-cell[aria-selected=true]{background-color:var(--accent);color:var(--accent-foreground)}@media(hover:hover)and (pointer:fine){:is(.calendar-year-picker__year-cell[data-selected=true],.calendar-year-picker__year-cell[aria-selected=true]):is(:hover,[data-hovered=true]){background-color:var(--accent-hover)}}.calendar-year-picker__year-cell:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.date-field{gap:var(--spacing);flex-direction:column;display:flex}:is(.date-field[data-invalid=true],.date-field[aria-invalid=true]) [data-slot=description]{display:none}.date-field [data-slot=label]{width:fit-content}.date-field--full-width{width:100%}.time-field{gap:var(--spacing);flex-direction:column;display:flex}:is(.time-field[data-invalid=true],.time-field[aria-invalid=true]) [data-slot=description]{display:none}.time-field [data-slot=label]{width:fit-content}.time-field--full-width{width:100%}.date-input-group{height:calc(var(--spacing) * 9);border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-width:1px;border-width:var(--border-width-field);border-color:var(--field-border);transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);outline-style:none;align-items:center;display:inline-flex;overflow:hidden}.date-input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *),.date-input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.date-input-group:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.date-input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.date-input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.date-input-group:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}@media(hover:hover){.date-input-group:hover:not(:focus-within),.date-input-group[data-hovered=true]:not([data-focus-within=true]){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.date-input-group[data-focus-within=true]:not(:has([data-slot=date-picker-trigger]:focus,[data-slot=date-picker-trigger][data-focused=true],[data-slot=date-range-picker-trigger]:focus,[data-slot=date-range-picker-trigger][data-focused=true])),.date-input-group:focus-within:not(:has([data-slot=date-picker-trigger]:focus,[data-slot=date-picker-trigger][data-focused=true],[data-slot=date-range-picker-trigger]:focus,[data-slot=date-range-picker-trigger][data-focused=true])){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.date-input-group[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.date-input-group[data-invalid=true]:focus,.date-input-group[data-invalid=true]:focus-visible,.date-input-group[data-invalid=true][data-focused=true],.date-input-group[data-invalid=true][data-focus-visible=true],.date-input-group[data-invalid=true]:focus-within,.date-input-group[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.date-input-group[data-invalid=true]{background-color:var(--field-focus);border-color:var(--color-field-border-invalid)}.date-input-group[data-disabled=true],.date-input-group[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.date-input-group__input{cursor:text;border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;background-color:#0000;border-width:0;border-radius:0;outline-style:none;flex:1;align-items:center;gap:1px;display:flex}@media(min-width:40rem){.date-input-group__input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.date-input-group:has([data-slot=date-input-group-prefix]) .date-input-group__input{border-start-start-radius:0;border-end-start-radius:0;padding-inline-start:calc(var(--spacing) * 2)}.date-input-group:has([data-slot=date-input-group-suffix]) .date-input-group__input{border-start-end-radius:0;border-end-end-radius:0;padding-inline-end:calc(var(--spacing) * 2)}.date-input-group:has(.date-range-picker__range-separator) .date-input-group__input[slot=start]{flex:none;padding-inline-end:0}.date-input-group:has(.date-range-picker__range-separator) .date-input-group__input[slot=end]{padding-inline-start:0}.date-input-group__input:focus,.date-input-group__input:focus-visible{--tw-outline-style:none;outline-style:none}.date-input-group__input-container{scrollbar-color:auto;scrollbar-gutter:auto;-ms-overflow-style:none;scrollbar-width:none;flex:1;align-items:center;width:fit-content;display:flex;overflow:auto clip}.date-input-group__segment{border-radius:calc(var(--radius) * .75);padding-inline:calc(var(--spacing) * .5);text-align:end;text-wrap:nowrap;--tw-outline-style:none;outline-style:none;display:inline-block}.date-input-group__segment[data-type=literal]{color:var(--muted);padding:0}.date-input-group__segment[data-placeholder=true]{color:var(--field-placeholder,var(--muted))}.date-input-group__segment:focus,.date-input-group__segment[data-focused=true]{background-color:var(--accent-soft);color:var(--accent-soft-foreground)}.date-input-group__segment[data-disabled=true]{opacity:.5}.date-input-group__segment[data-invalid=true]{color:var(--danger)}.date-input-group__segment[data-invalid=true]:focus,.date-input-group__segment[data-invalid=true][data-focused=true]{background-color:var(--danger-soft);color:var(--danger-soft-foreground)}.date-input-group__prefix{pointer-events:none;color:var(--field-placeholder,var(--muted));flex-shrink:0;align-items:center;margin-inline-start:calc(var(--spacing) * 3);margin-inline-end:0;display:flex}.date-input-group__suffix{pointer-events:none;color:var(--field-placeholder,var(--muted));flex-shrink:0;align-items:center;margin-inline-end:calc(var(--spacing) * 3);display:flex}.date-input-group--secondary{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--date-input-group-bg);--date-input-group-bg:var(--default);--date-input-group-bg-hover:var(--default-hover);--date-input-group-bg-focus:var(--default)}@media(hover:hover){.date-input-group--secondary:hover:not(:focus-within),.date-input-group--secondary[data-hovered=true]:not([data-focus-within=true]){background-color:var(--date-input-group-bg-hover)}}.date-input-group--secondary:focus-within,.date-input-group--secondary[data-focus-within=true]{background-color:var(--date-input-group-bg-focus)}.date-input-group--secondary[data-invalid=true]{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}.date-input-group--secondary[data-invalid=true]:focus,.date-input-group--secondary[data-invalid=true]:focus-visible,.date-input-group--secondary[data-invalid=true][data-focused=true],.date-input-group--secondary[data-invalid=true][data-focus-visible=true],.date-input-group--secondary[data-invalid=true]:focus-within,.date-input-group--secondary[data-invalid=true][data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.date-input-group--secondary[data-invalid=true]{border-color:var(--color-field-border-invalid);background-color:var(--date-input-group-bg-focus)}.date-input-group--secondary [data-slot=date-input-group-input]{background-color:#0000}.date-input-group--full-width{width:100%}.date-picker{gap:var(--spacing);flex-direction:column;display:inline-flex}.date-picker .date-input-group__suffix,.date-picker .date-input-group__prefix{pointer-events:auto}.date-picker__trigger{border-radius:var(--field-radius,calc(var(--radius) * 1.5));width:100%;padding:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);transition:box-shadow .15s var(--ease-out);align-items:center;display:inline-flex}.date-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.date-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.date-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.date-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.date-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.date-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.date-picker__trigger:focus-visible:not([data-focused]),.date-picker__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.date-picker__trigger:disabled,.date-picker__trigger[data-disabled=true],.date-picker__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.date-picker__trigger-indicator{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);color:var(--field-placeholder,var(--muted));justify-content:center;align-items:center;display:inline-flex}.date-picker__popover{width:fit-content;transform-origin:var(--trigger-anchor-point);scrollbar-color:auto;scrollbar-gutter:auto;-ms-overflow-style:none;scrollbar-width:none;overscroll-behavior:contain;background-color:var(--overlay);padding:calc(var(--spacing) * 3);overflow-y:auto}.date-picker__popover:is([data-reduce-motion=true],[data-reduce-motion=true] *),.date-picker__popover:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.date-picker__popover:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.date-picker__popover:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.date-picker__popover:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.date-picker__popover:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.date-picker__popover{box-shadow:var(--overlay-shadow);border-radius:min(32px,calc(var(--radius) * 2.5))}.date-picker__popover:focus-visible:not([data-focused]),.date-picker__popover[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.date-picker__popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.95;transition-duration:.15s;transition-timing-function:ease}.date-picker__popover[data-entering=true][data-placement^=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.date-picker__popover[data-entering=true][data-placement^=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.date-picker__popover[data-entering=true][data-placement^=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.date-picker__popover[data-entering=true][data-placement^=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.date-picker__popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.date-picker__popover[data-exiting=true],.date-picker__popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.date-picker__popover .calendar__nav-button-icon,.date-picker__popover .range-calendar__nav-button-icon{rotate:0deg}.date-range-picker{gap:var(--spacing);flex-direction:column;display:inline-flex}.date-range-picker .date-input-group__suffix,.date-range-picker .date-input-group__prefix{pointer-events:auto}.date-range-picker__trigger{border-radius:var(--field-radius,calc(var(--radius) * 1.5));width:100%;padding:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);transition:box-shadow .15s var(--ease-out);align-items:center;display:inline-flex}.date-range-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.date-range-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.date-range-picker__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.date-range-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.date-range-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.date-range-picker__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.date-range-picker__trigger:focus-visible:not([data-focused]),.date-range-picker__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.date-range-picker__trigger:disabled,.date-range-picker__trigger[data-disabled=true],.date-range-picker__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.date-range-picker__trigger-indicator{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);color:var(--field-placeholder,var(--muted));justify-content:center;align-items:center;display:inline-flex}.date-range-picker__range-separator{padding-inline:var(--spacing);color:var(--field-placeholder,var(--muted));-webkit-user-select:none;user-select:none}.date-range-picker__popover{width:fit-content;transform-origin:var(--trigger-anchor-point);scrollbar-color:auto;scrollbar-gutter:auto;-ms-overflow-style:none;scrollbar-width:none;overscroll-behavior:contain;background-color:var(--overlay);padding:calc(var(--spacing) * 3);overflow-y:auto}.date-range-picker__popover:is([data-reduce-motion=true],[data-reduce-motion=true] *),.date-range-picker__popover:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.date-range-picker__popover:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.date-range-picker__popover:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.date-range-picker__popover:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.date-range-picker__popover:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.date-range-picker__popover{box-shadow:var(--overlay-shadow);border-radius:min(32px,calc(var(--radius) * 2.5))}.date-range-picker__popover:focus-visible:not([data-focused]),.date-range-picker__popover[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.date-range-picker__popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.95;transition-duration:.15s;transition-timing-function:ease}.date-range-picker__popover[data-entering=true][data-placement^=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.date-range-picker__popover[data-entering=true][data-placement^=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.date-range-picker__popover[data-entering=true][data-placement^=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.date-range-picker__popover[data-entering=true][data-placement^=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.date-range-picker__popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.date-range-picker__popover[data-exiting=true],.date-range-picker__popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.date-range-picker__popover .range-calendar__nav-button-icon,.date-range-picker__popover .calendar__nav-button-icon{rotate:0deg}.card{gap:calc(var(--spacing) * 3);padding:calc(var(--spacing) * 4);--tw-shadow:var(--surface-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:min(32px,var(--radius-3xl));flex-direction:column;display:flex;position:relative;overflow:visible}.card__header{flex-direction:column;display:flex}.card__title{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}.card__description{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);color:var(--muted)}.card__content{gap:var(--spacing);flex-direction:column;flex:1;display:flex}.card__footer{flex-direction:row;align-items:center;display:flex}.card--transparent{--tw-border-style:none;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#0000;border-style:none}.card--default{background-color:var(--surface)}.card--secondary{background-color:var(--surface-secondary)}.card--tertiary{background-color:var(--surface-tertiary)}.header{width:100%;padding-inline:calc(var(--spacing) * 2);padding-top:calc(var(--spacing) * 1.5);padding-bottom:var(--spacing);text-align:start;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted)}.separator{border-radius:calc(var(--radius) * .5);border-top-style:var(--tw-border-style);border-top-width:0;border-bottom-style:var(--tw-border-style);background-color:var(--separator);border-bottom-width:0;flex-shrink:0;width:100%;height:1px}.separator--horizontal{width:100%;height:1px}.separator--vertical{height:auto;min-height:calc(var(--spacing) * 2);align-self:stretch;width:1px}.separator--default{background-color:var(--separator)}.separator--secondary{background-color:var(--separator-secondary)}.separator--tertiary{background-color:var(--separator-tertiary)}.surface{color:var(--foreground);position:relative}.surface--transparent{background-color:#0000}.surface--default{background-color:var(--surface);color:var(--surface-foreground)}.surface--secondary{background-color:var(--surface-secondary);color:var(--surface-secondary-foreground)}.surface--tertiary{background-color:var(--surface-tertiary);color:var(--surface-tertiary-foreground)}.avatar{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10);border-radius:calc(var(--radius) * 3);background-color:var(--default);flex-shrink:0;justify-content:center;align-items:center;display:flex;position:relative;overflow:hidden}.avatar__fallback{background-color:var(--default);width:100%;height:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);justify-content:center;align-items:center;display:flex}.avatar__image{aspect-ratio:1;width:100%;height:100%;transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.25s;transition-duration:.25s;position:absolute;inset:0}.avatar__image:is([data-reduce-motion=true],[data-reduce-motion=true] *),.avatar__image:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.avatar__image:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.avatar__image:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.avatar__image:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.avatar__image:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.avatar--sm{--avatar-size:2rem;width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8);border-radius:calc(var(--radius) * 2)}.avatar--sm .avatar__fallback{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.avatar--md{--avatar-size:2.5rem}.avatar--lg{--avatar-size:3rem;width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12);border-radius:calc(var(--radius) * 3)}.avatar--lg .avatar__fallback{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.avatar__fallback--accent{color:var(--accent-soft-foreground)}.avatar__fallback--default{color:var(--default-soft-foreground)}.avatar__fallback--success{color:var(--success-soft-foreground)}.avatar__fallback--warning{color:var(--warning-soft-foreground)}.avatar__fallback--danger{color:var(--danger-soft-foreground)}.avatar--soft{background-color:#0000}.avatar--soft .avatar__fallback--accent{background-color:var(--accent-soft);color:var(--accent-soft-foreground)}.avatar--soft .avatar__fallback--success{background-color:var(--success-soft);color:var(--success-soft-foreground)}.avatar--soft .avatar__fallback--warning{background-color:var(--warning-soft);color:var(--warning-soft-foreground)}.avatar--soft .avatar__fallback--default{background-color:var(--default-soft);color:var(--default-soft-foreground)}.avatar--soft .avatar__fallback--danger{background-color:var(--danger-soft);color:var(--danger-soft-foreground)}.avatar-group{--avatar-group-overlap:.5rem;--avatar-group-seam:2px;align-items:center;display:inline-flex}.avatar-group--grid{gap:calc(var(--spacing) * 3);flex-wrap:wrap}.avatar-group:not(.avatar-group--grid)>.avatar+.avatar,.avatar-group:not(.avatar-group--grid)>.avatar+.avatar-group__count{margin-inline-start:calc(-1 * var(--avatar-group-overlap,.5rem))}.avatar-group__count{flex-shrink:0}.avatar-group--ring:not(.avatar-group--grid)>.avatar,.avatar-group--ring:not(.avatar-group--grid)>.avatar-group__count{box-shadow:0 0 0 var(--avatar-group-seam,2px) var(--background);position:relative}.avatar-group--clip:not(.avatar-group--grid)>.avatar:not(:last-child){--avatar-group-cut-center:calc(100% + (var(--avatar-size,2.5rem) / 2) - var(--avatar-group-overlap,.5rem));--avatar-group-cut-radius:calc((var(--avatar-size,2.5rem) / 2) + var(--avatar-group-seam,2px));-webkit-mask-image:radial-gradient(circle at var(--avatar-group-cut-center) 50%,transparent var(--avatar-group-cut-radius),#000 var(--avatar-group-cut-radius));mask-image:radial-gradient(circle at var(--avatar-group-cut-center) 50%,transparent var(--avatar-group-cut-radius),#000 var(--avatar-group-cut-radius))}:is(.avatar-group--clip:not(.avatar-group--grid):dir(rtl)>.avatar:not(:last-child),:dir(rtl) .avatar-group--clip:not(.avatar-group--grid)>.avatar:not(:last-child)){--avatar-group-cut-center:calc(0% - (var(--avatar-size,2.5rem) / 2) + var(--avatar-group-overlap,.5rem))}.avatar-group--clip:not(.avatar-group--grid)>.avatar:not(:last-child) .avatar__fallback{box-sizing:border-box;padding-inline-end:calc(var(--avatar-group-overlap,.5rem) * .35)}.alert-dialog__trigger{cursor:var(--cursor-interactive);transition:transform .25s var(--ease-out-quart),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);display:inline-block}.alert-dialog__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.alert-dialog__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.alert-dialog__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.alert-dialog__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.alert-dialog__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.alert-dialog__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.alert-dialog__trigger:focus-visible:not([data-focused]),.alert-dialog__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.alert-dialog__trigger:disabled,.alert-dialog__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.alert-dialog__trigger:active,.alert-dialog__trigger[data-pressed=true]{transform:scale(.97)}.alert-dialog__backdrop{z-index:var(--z-index-overlay);height:var(--visual-viewport-height);flex-direction:row;justify-content:center;align-items:center;width:100%;display:flex;position:fixed;inset:0}.alert-dialog__backdrop[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:var(--ease-out);transition-duration:.15s;transition-timing-function:var(--ease-out);--tw-enter-opacity:0}.alert-dialog__backdrop[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:var(--ease-out);transition-duration:.1s;transition-timing-function:var(--ease-out);--tw-exit-opacity:0}.alert-dialog__backdrop[data-exiting=true],.alert-dialog__backdrop[data-entering=true]{will-change:opacity}:is(.alert-dialog__backdrop[data-exiting=true],.alert-dialog__backdrop[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.alert-dialog__backdrop[data-exiting=true],.alert-dialog__backdrop[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.alert-dialog__backdrop[data-exiting=true],.alert-dialog__backdrop[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){:is(.alert-dialog__backdrop[data-exiting=true],.alert-dialog__backdrop[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.alert-dialog__backdrop[data-exiting=true],.alert-dialog__backdrop[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.alert-dialog__backdrop[data-exiting=true],.alert-dialog__backdrop[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.alert-dialog__backdrop--transparent{background-color:#0000}.alert-dialog__backdrop--opaque{background-color:var(--backdrop)}.alert-dialog__backdrop--blur{background-color:var(--backdrop);--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.alert-dialog__container{height:var(--visual-viewport-height);width:100%;min-width:0;padding:calc(var(--spacing) * 4);flex-direction:column;flex:1;align-items:center;display:flex}@media(min-width:40rem){.alert-dialog__container{width:fit-content;padding:calc(var(--spacing) * 10)}}.alert-dialog__container{pointer-events:none}.alert-dialog__container[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.25s;--tw-ease:cubic-bezier(.25, .46, .45, .94);--tw-enter-opacity:0;--tw-enter-scale: 105% ;transition-duration:.25s;transition-timing-function:cubic-bezier(.25,.46,.45,.94)}.alert-dialog__container[data-entering=true][data-placement=auto]{--tw-enter-translate-y:calc(1*var(--spacing))}@media(min-width:40rem){.alert-dialog__container[data-entering=true][data-placement=auto]{--tw-enter-translate-y: 0% }}.alert-dialog__container[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.alert-dialog__container[data-entering=true][data-placement=center]{--tw-enter-translate-y: -0% }.alert-dialog__container[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing))}.alert-dialog__container[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:cubic-bezier(.25, .46, .45, .94);--tw-exit-opacity:0;--tw-exit-scale:.95;transition-duration:.1s;transition-timing-function:cubic-bezier(.25,.46,.45,.94)}.alert-dialog__container[data-exiting=true],.alert-dialog__container[data-entering=true]{will-change:opacity,transform}:is(.alert-dialog__container[data-exiting=true],.alert-dialog__container[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.alert-dialog__container[data-exiting=true],.alert-dialog__container[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.alert-dialog__container[data-exiting=true],.alert-dialog__container[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){:is(.alert-dialog__container[data-exiting=true],.alert-dialog__container[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.alert-dialog__container[data-exiting=true],.alert-dialog__container[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.alert-dialog__container[data-exiting=true],.alert-dialog__container[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.alert-dialog__dialog{background-color:var(--overlay);--tw-shadow:var(--overlay-shadow);width:100%;min-height:0;max-height:100%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-radius:min(32px,var(--radius-3xl));padding:calc(var(--spacing) * 6);pointer-events:auto;outline-style:none;flex-direction:column;display:flex;position:relative;overflow:clip}.alert-dialog__dialog[data-placement=auto]{margin-top:auto}@media(min-width:40rem){.alert-dialog__dialog[data-placement=auto]{margin-block:auto}}.alert-dialog__dialog[data-placement=center]{margin-block:auto}.alert-dialog__dialog[data-placement=bottom]{margin-top:auto}.alert-dialog__dialog[data-placement=top]{margin-top:0}.alert-dialog__dialog--xs{max-width:var(--container-xs)}.alert-dialog__dialog--sm{max-width:var(--container-sm)}.alert-dialog__dialog--md{max-width:var(--container-md)}.alert-dialog__dialog--lg{max-width:var(--container-lg)}.alert-dialog__dialog--cover{width:100%;height:100%;min-height:100%}.alert-dialog__header{gap:calc(var(--spacing) * 3);flex-direction:column;margin-bottom:0;display:flex}.alert-dialog__header>.modal__icon{margin-bottom:0}.alert-dialog__heading{vertical-align:middle;font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}.alert-dialog__icon{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10);border-radius:calc(var(--radius) * 3);-webkit-user-select:none;user-select:none;flex-shrink:0;justify-content:center;align-items:center;display:flex}.alert-dialog__icon [data-slot=alert-dialog-default-icon]{box-sizing:content-box;width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.alert-dialog__icon--default{background-color:var(--default);color:var(--foreground)}.alert-dialog__icon--accent{background-color:var(--accent-soft);color:var(--accent-soft-foreground)}.alert-dialog__icon--success{background-color:var(--success-soft);color:var(--success-soft-foreground)}.alert-dialog__icon--warning{background-color:var(--warning-soft);color:var(--warning-soft-foreground)}.alert-dialog__icon--danger{background-color:var(--danger-soft);color:var(--danger-soft-foreground)}.alert-dialog__body{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);min-height:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.43;color:var(--muted);overscroll-behavior:contain;-webkit-overflow-scrolling:touch;margin:-3px;flex:1;margin-block:0;padding:3px;line-height:1.43;overflow-y:auto}.alert-dialog__footer{justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 2);flex-direction:row;margin-top:0;display:flex}.alert-dialog__close-trigger{inset-inline-end:calc(var(--spacing) * 4);top:calc(var(--spacing) * 4);position:absolute}.alert-dialog__header+.alert-dialog__body{margin-top:calc(var(--spacing) * 2)}.alert-dialog__header+.alert-dialog__footer,.alert-dialog__body+.alert-dialog__footer{margin-top:calc(var(--spacing) * 5)}.drawer__trigger{cursor:var(--cursor-interactive);transition:transform .25s var(--ease-out-quart),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);display:inline-block}.drawer__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.drawer__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.drawer__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.drawer__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.drawer__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.drawer__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.drawer__trigger:focus-visible:not([data-focused]),.drawer__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.drawer__trigger:disabled,.drawer__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.drawer__trigger:active,.drawer__trigger[data-pressed=true]{transform:scale(.97)}.drawer__backdrop{z-index:var(--z-index-overlay);height:var(--visual-viewport-height);opacity:1;width:100%;transition:opacity .25s cubic-bezier(.32,.72,0,1);position:fixed;inset:0}.drawer__backdrop[data-entering=true]{opacity:0}.drawer__backdrop[data-exiting=true]{opacity:0;transition-duration:.2s;transition-timing-function:cubic-bezier(.32,.72,0,1)}.drawer__backdrop[data-exiting=true],.drawer__backdrop[data-entering=true]{will-change:opacity}@media(prefers-reduced-motion:reduce){.drawer__backdrop{transition:none}}.drawer__backdrop--transparent{background-color:#0000}.drawer__backdrop--opaque{background-color:var(--backdrop)}.drawer__backdrop--blur{background-color:var(--backdrop);--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.drawer__content{pointer-events:none;z-index:var(--z-index-overlay);height:var(--visual-viewport-height);width:100%;min-width:0;display:flex;position:fixed;inset:0}.drawer__content--bottom{align-items:flex-end}.drawer__content--top{align-items:flex-start}.drawer__content--left{justify-content:flex-start}.drawer__content--right{justify-content:flex-end}.drawer__dialog{background-color:var(--overlay);--tw-shadow:var(--overlay-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;padding:calc(var(--spacing) * 6);pointer-events:auto;--drawer-enter-duration:.25s;--drawer-exit-duration:.2s;--drawer-enter-ease:cubic-bezier(.32, .72, 0, 1);--drawer-exit-ease:cubic-bezier(.32, .72, 0, 1);will-change:translate;transition:translate var(--drawer-enter-duration) var(--drawer-enter-ease);outline-style:none;flex-direction:column;display:flex;position:relative}@media(prefers-reduced-motion:reduce){.drawer__dialog{transition:none}}.drawer__dialog[data-placement=bottom]{border-start-start-radius:min(32px,var(--radius-2xl));border-start-end-radius:min(32px,var(--radius-2xl));width:100%;max-height:85vh}.drawer__dialog[data-placement=top]{border-end-end-radius:min(32px,var(--radius-2xl));border-end-start-radius:min(32px,var(--radius-2xl));width:100%;max-height:85vh}.drawer__dialog[data-placement=left]{height:100%;width:calc(var(--spacing) * 80);border-radius:0;max-width:85vw}@media(min-width:40rem){.drawer__dialog[data-placement=left]{width:calc(var(--spacing) * 96)}}.drawer__dialog[data-placement=right]{height:100%;width:calc(var(--spacing) * 80);border-radius:0;max-width:85vw}@media(min-width:40rem){.drawer__dialog[data-placement=right]{width:calc(var(--spacing) * 96)}}[data-exiting=true] .drawer__dialog{transition-duration:var(--drawer-exit-duration);transition-timing-function:var(--drawer-exit-ease)}.drawer__content--left .drawer__dialog,.drawer__content--right .drawer__dialog,.drawer__content--top .drawer__dialog,.drawer__content--bottom .drawer__dialog{translate:0}.drawer__content--left[data-entering=true] .drawer__dialog,.drawer__content--left[data-exiting=true] .drawer__dialog{translate:-100%}.drawer__content--right[data-entering=true] .drawer__dialog,.drawer__content--right[data-exiting=true] .drawer__dialog{translate:100%}.drawer__content--top[data-entering=true] .drawer__dialog,.drawer__content--top[data-exiting=true] .drawer__dialog{translate:0 -100%}.drawer__content--bottom[data-entering=true] .drawer__dialog,.drawer__content--bottom[data-exiting=true] .drawer__dialog{translate:0 100%}.drawer__dialog--top{padding-bottom:calc(var(--spacing) * 2)}.drawer__dialog--top .drawer__handle{padding-bottom:0}.drawer__header{gap:calc(var(--spacing) * 3);flex-direction:column;margin-bottom:0;display:flex}.drawer__heading{vertical-align:middle;font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}.drawer__body{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);min-height:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.43;color:var(--muted);overscroll-behavior:contain;-webkit-overflow-scrolling:touch;margin:-3px;flex:1;margin-block:0;padding:3px;line-height:1.43;overflow-y:auto}.drawer__footer{justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 2);flex-direction:row;margin-top:0;display:flex}.drawer__handle{padding-bottom:calc(var(--spacing) * 2);justify-content:center;align-items:center;display:flex}.drawer__handle>[data-slot=drawer-handle-bar]{height:var(--spacing);width:calc(var(--spacing) * 9);border-radius:calc(var(--radius) * .25);background-color:var(--separator)}.drawer__close-trigger{inset-inline-end:calc(var(--spacing) * 4);top:calc(var(--spacing) * 4);position:absolute}.drawer__header+.drawer__body{margin-top:calc(var(--spacing) * 2)}.drawer__header+.drawer__footer,.drawer__body+.drawer__footer{margin-top:calc(var(--spacing) * 5)}.drawer__handle+.drawer__header,.drawer__handle+.drawer__body{margin-top:0}.modal__trigger{cursor:var(--cursor-interactive);transition:transform .25s var(--ease-out-quart),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);display:inline-block}.modal__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.modal__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.modal__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.modal__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.modal__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.modal__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.modal__trigger:focus-visible:not([data-focused]),.modal__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.modal__trigger:disabled,.modal__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.modal__trigger:active,.modal__trigger[data-pressed=true]{transform:scale(.97)}.modal__backdrop{z-index:var(--z-index-overlay);height:var(--visual-viewport-height);flex-direction:row;justify-content:center;align-items:center;width:100%;display:flex;position:fixed;inset:0}.modal__backdrop[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:var(--ease-out);transition-duration:.15s;transition-timing-function:var(--ease-out);--tw-enter-opacity:0}.modal__backdrop[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:var(--ease-out);transition-duration:.1s;transition-timing-function:var(--ease-out);--tw-exit-opacity:0}.modal__backdrop[data-exiting=true],.modal__backdrop[data-entering=true]{will-change:opacity}:is(.modal__backdrop[data-exiting=true],.modal__backdrop[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.modal__backdrop[data-exiting=true],.modal__backdrop[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.modal__backdrop[data-exiting=true],.modal__backdrop[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){:is(.modal__backdrop[data-exiting=true],.modal__backdrop[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.modal__backdrop[data-exiting=true],.modal__backdrop[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.modal__backdrop[data-exiting=true],.modal__backdrop[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.modal__backdrop--transparent{background-color:#0000}.modal__backdrop--opaque{background-color:var(--backdrop)}.modal__backdrop--blur{background-color:var(--backdrop);--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.modal__container{height:var(--visual-viewport-height);width:100%;min-width:0;padding:calc(var(--spacing) * 4);flex-direction:column;flex:1;align-items:center;display:flex}@media(min-width:40rem){.modal__container{width:fit-content;padding:calc(var(--spacing) * 10)}}.modal__container{pointer-events:none}.modal__container[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.25s;--tw-ease:cubic-bezier(.25, .46, .45, .94);--tw-enter-opacity:0;--tw-enter-scale: 105% ;transition-duration:.25s;transition-timing-function:cubic-bezier(.25,.46,.45,.94)}.modal__container[data-entering=true][data-placement=auto]{--tw-enter-translate-y:calc(1*var(--spacing))}@media(min-width:40rem){.modal__container[data-entering=true][data-placement=auto]{--tw-enter-translate-y: 0% }}.modal__container[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.modal__container[data-entering=true][data-placement=center]{--tw-enter-translate-y: -0% }.modal__container[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing))}.modal__container[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:cubic-bezier(.25, .46, .45, .94);--tw-exit-opacity:0;--tw-exit-scale:.95;transition-duration:.1s;transition-timing-function:cubic-bezier(.25,.46,.45,.94)}.modal__container[data-exiting=true],.modal__container[data-entering=true]{will-change:opacity,transform}:is(.modal__container[data-exiting=true],.modal__container[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *),:is(.modal__container[data-exiting=true],.modal__container[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,:is(.modal__container[data-exiting=true],.modal__container[data-entering=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{animation:none}@media(prefers-reduced-motion:reduce){:is(.modal__container[data-exiting=true],.modal__container[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),:is(.modal__container[data-exiting=true],.modal__container[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,:is(.modal__container[data-exiting=true],.modal__container[data-entering=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{animation:none}}.modal__container--scroll-outside{height:auto;min-height:var(--visual-viewport-height);overflow-y:visible}.modal__backdrop:has(.modal__container--scroll-outside){scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);-webkit-overflow-scrolling:touch;align-items:flex-start;overflow-y:auto}.modal__container--full{padding:0}@media(min-width:40rem){.modal__container--full{padding:0}}.modal__container--full[data-entering=true]{--tw-enter-translate-y: 0% ;--tw-enter-scale:1}@media(min-width:40rem){.modal__container--full[data-entering=true]{--tw-enter-translate-y: 0% }}.modal__container--full[data-exiting=true]{--tw-exit-scale:1}.modal__dialog{background-color:var(--overlay);--tw-shadow:var(--overlay-shadow);width:100%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;border-radius:min(32px,var(--radius-3xl));padding:calc(var(--spacing) * 6);pointer-events:auto;outline-style:none;flex-direction:column;display:flex;position:relative}.modal__dialog[data-placement=auto]{margin-top:auto}@media(min-width:40rem){.modal__dialog[data-placement=auto]{margin-block:auto}}.modal__dialog[data-placement=center]{margin-block:auto}.modal__dialog[data-placement=bottom]{margin-top:auto}.modal__dialog[data-placement=top]{margin-top:0}.modal__dialog--scroll-inside{min-height:0;max-height:100%;overflow:clip}.modal__dialog--scroll-outside{flex-shrink:0;height:auto;min-height:0}.modal__dialog--xs{max-width:var(--container-xs)}.modal__dialog--sm{max-width:var(--container-sm)}.modal__dialog--md{max-width:var(--container-md)}.modal__dialog--lg{max-width:var(--container-lg)}.modal__dialog--cover{width:100%;height:100%;min-height:100%}.modal__dialog--full{--tw-shadow:0 0 #0000;width:100%;height:100%;min-height:100%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:0}.modal__header{gap:calc(var(--spacing) * 3);flex-direction:column;margin-bottom:0;display:flex}.modal__header>.modal__icon{margin-bottom:0}.modal__heading{vertical-align:middle;font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}.modal__icon{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10);border-radius:calc(var(--radius) * 3);-webkit-user-select:none;user-select:none;flex-shrink:0;justify-content:center;align-items:center;display:flex}.modal__body{min-height:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.43;color:var(--muted);margin:-3px;flex:1;margin-block:0;padding:3px;line-height:1.43;overflow:visible}.modal__body--scroll-inside{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);overscroll-behavior:contain;-webkit-overflow-scrolling:touch;overflow-y:auto}.modal__body--scroll-outside{overflow-y:visible}.modal__footer{justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 2);flex-direction:row;margin-top:0;display:flex}.modal__close-trigger{inset-inline-end:calc(var(--spacing) * 4);top:calc(var(--spacing) * 4);position:absolute}.modal__header+.modal__body{margin-top:calc(var(--spacing) * 2)}.modal__header+.modal__footer,.modal__body+.modal__footer{margin-top:calc(var(--spacing) * 5)}.popover{transform-origin:var(--trigger-anchor-point);background-color:var(--overlay);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-radius:min(32px,var(--radius-3xl));box-shadow:var(--overlay-shadow);padding:0}.popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.9;transition-duration:.15s;transition-timing-function:ease}.popover[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.popover[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.popover[data-entering=true][data-placement=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.popover[data-entering=true][data-placement=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.popover[data-exiting=true],.popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.popover [data-slot=popover-overlay-arrow]{fill:var(--overlay)}.popover[data-placement=bottom] [data-slot=popover-overlay-arrow]{rotate:180deg}.popover[data-placement=left] [data-slot=popover-overlay-arrow]{rotate:-90deg}.popover[data-placement=right] [data-slot=popover-overlay-arrow]{rotate:90deg}.popover__dialog{padding:calc(var(--spacing) * 4);--tw-outline-style:none;outline-style:none}.popover__heading{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.popover__trigger{transition:color .15s var(--ease-smooth),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);display:inline-block}.popover__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.popover__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.popover__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.popover__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.popover__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.popover__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.popover__trigger{cursor:var(--cursor-interactive)}.popover__trigger:focus-visible:not([data-focused]),.popover__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.popover__trigger:disabled,.popover__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.tooltip{max-width:var(--container-xs);transform-origin:var(--trigger-anchor-point);background-color:var(--overlay);padding:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));word-break:break-all;border-radius:min(32px,var(--radius-xl));box-shadow:var(--overlay-shadow)}.tooltip[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.9;transition-duration:.15s;transition-timing-function:ease}.tooltip[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.tooltip[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.tooltip[data-entering=true][data-placement=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.tooltip[data-entering=true][data-placement=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.tooltip[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.tooltip[data-exiting=true],.tooltip[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.tooltip [data-slot=overlay-arrow]{stroke:var(--border)}@supports (color:color-mix(in lab,red,red)){.tooltip [data-slot=overlay-arrow]{stroke:color-mix(in oklab,var(--border) 40%,transparent)}}.tooltip [data-slot=overlay-arrow]{fill:var(--overlay)}.tooltip[data-placement=bottom] [data-slot=overlay-arrow]{rotate:180deg}.tooltip[data-placement=left] [data-slot=overlay-arrow]{rotate:-90deg}.tooltip[data-placement=right] [data-slot=overlay-arrow]{rotate:90deg}.tooltip__trigger{transition:color .15s var(--ease-smooth),background-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);display:inline-block}.tooltip__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.tooltip__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.tooltip__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.tooltip__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.tooltip__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.tooltip__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.tooltip__trigger:focus-visible:not([data-focused]),.tooltip__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;outline-style:none}.combo-box{gap:var(--spacing);flex-direction:column;display:flex}:is(.combo-box[data-invalid=true],.combo-box[aria-invalid=true]) [data-slot=description]{display:none}.combo-box [data-slot=label]{width:fit-content}.combo-box [data-slot=input]{flex:1;min-width:0}.combo-box [data-slot=input]:has(+.combo-box__trigger){padding-inline-end:calc(var(--spacing) * 7)}.combo-box [data-slot=input]:focus,.combo-box [data-slot=input][data-focus]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;border-color:var(--field-border-focus);background-color:var(--field-focus);outline-style:none}.combo-box [data-slot=input]:disabled,.combo-box [data-slot=input][data-disabled],.combo-box [data-slot=input][aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.combo-box__input-group{isolation:isolate;align-items:center;display:inline-flex;position:relative}.combo-box__value{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));overflow-wrap:break-word;color:var(--field-foreground,var(--foreground))}.combo-box__value:empty{display:none}.combo-box__value[data-placeholder=true]{color:var(--field-placeholder,var(--muted))}.combo-box__value [data-slot=list-box-item-indicator]{display:none}.combo-box__trigger{--tw-translate-y: -50% ;height:100%;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;color:var(--field-placeholder,var(--muted));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;-webkit-tap-highlight-color:transparent;--tw-border-style:none;--tw-outline-style:none;inset-inline-end:calc(var(--spacing) * 0);background-color:#0000;border-style:none;outline-style:none;flex-shrink:0;justify-content:center;align-items:center;padding-inline-end:calc(var(--spacing) * 2);transition-duration:.15s;display:flex;position:absolute;top:50%}@media(hover:hover){.combo-box__trigger:hover,.combo-box__trigger[data-hovered=true]{color:var(--field-foreground,var(--foreground))}}.combo-box__trigger:focus-visible:not([data-focused]),.combo-box__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;border-radius:.25rem;outline-style:none}.combo-box__trigger[data-pressed=true]{opacity:.7}.combo-box__trigger:disabled,.combo-box__trigger[data-disabled],.combo-box__trigger[aria-disabled=true]{cursor:not-allowed;opacity:.5}.combo-box__trigger [data-slot=combo-box-trigger-default-icon]{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}.combo-box__trigger [data-slot=combo-box-trigger-default-icon]:is([data-reduce-motion=true],[data-reduce-motion=true] *),.combo-box__trigger [data-slot=combo-box-trigger-default-icon]:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.combo-box__trigger [data-slot=combo-box-trigger-default-icon]:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.combo-box__trigger [data-slot=combo-box-trigger-default-icon]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.combo-box__trigger [data-slot=combo-box-trigger-default-icon]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.combo-box__trigger [data-slot=combo-box-trigger-default-icon]:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.combo-box__trigger[data-open=true] [data-slot=combo-box-trigger-default-icon]{rotate:180deg}.combo-box__popover{min-width:var(--trigger-width);transform-origin:var(--trigger-anchor-point);scroll-padding-block:var(--spacing);scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);overscroll-behavior:contain;background-color:var(--overlay);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-radius:min(32px,var(--radius-3xl));box-shadow:var(--overlay-shadow);padding:0;overflow-y:auto}.combo-box__popover:focus-visible:not([data-focused]),.combo-box__popover[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.combo-box__popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.95;transition-duration:.15s;transition-timing-function:ease}.combo-box__popover[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.combo-box__popover[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.combo-box__popover[data-entering=true][data-placement=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.combo-box__popover[data-entering=true][data-placement=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.combo-box__popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.combo-box__popover[data-exiting=true],.combo-box__popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.combo-box__popover [data-slot=popover-overlay-arrow]{fill:var(--overlay)}.combo-box__popover[data-placement=bottom] [data-slot=popover-overlay-arrow]{rotate:180deg}.combo-box__popover[data-placement=left] [data-slot=popover-overlay-arrow]{rotate:-90deg}.combo-box__popover[data-placement=right] [data-slot=popover-overlay-arrow]{rotate:90deg}.combo-box__popover [data-slot=list-box]{padding:calc(var(--spacing) * 1.5);--tw-outline-style:none;outline-style:none}.combo-box__popover [data-slot=list-box-item]{padding-inline:calc(var(--spacing) * 2.5)}.combo-box__popover [data-slot=list-box-item] [data-slot=list-box-item-indicator--checkmark]{transition-property:none}.combo-box--full-width,.combo-box__input-group--full-width{width:100%}.select{gap:var(--spacing);flex-direction:column;display:flex}:is(.select[data-invalid=true],.select[aria-invalid=true]) [data-slot=description]{display:none}.select [data-slot=label]{width:fit-content}.select__trigger{isolation:isolate;min-height:calc(var(--spacing) * 9);border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);border-width:1px;outline-style:none;display:inline-flex;position:relative}.select__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.select__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.select__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.select__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.select__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.select__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.select__trigger{cursor:var(--cursor-interactive);border-width:var(--border-width-field);border-color:var(--field-border)}.select__trigger:has(.select__indicator){padding-inline-end:calc(var(--spacing) * 7)}@media(hover:hover){.select__trigger:hover:not(:has(.select__clear-button:hover)),.select__trigger[data-hovered=true]:not(:has(.select__clear-button:hover)){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.select__trigger:focus-visible:not([data-focused]),.select__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;border-color:var(--field-border-focus);background-color:var(--field-focus);outline-style:none}.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}:is(.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger):focus,:is(.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger):focus-visible,:is(.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger)[data-focused=true],:is(.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger)[data-focus-visible=true],:is(.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger):focus-within,:is(.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger)[data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.select[data-invalid=true] .select__trigger,.select[aria-invalid=true] .select__trigger{background-color:var(--field-focus)}.select__trigger:disabled,.select__trigger[data-disabled=true],.select__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.select--secondary .select__trigger{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--select-trigger-bg);--select-trigger-bg:var(--default);--select-trigger-bg-hover:var(--default-hover);--select-trigger-bg-focus:var(--default)}@media(hover:hover){.select--secondary .select__trigger:hover:not(:has(.select__clear-button:hover)),.select--secondary .select__trigger[data-hovered=true]:not(:has(.select__clear-button:hover)){background-color:var(--select-trigger-bg-hover)}}.select--secondary .select__trigger:focus-visible:not([data-focused]),.select--secondary .select__trigger[data-focus-visible=true],.select[data-invalid=true] :is(.select--secondary .select__trigger),.select[aria-invalid=true] :is(.select--secondary .select__trigger){background-color:var(--select-trigger-bg-focus)}.select__value{text-align:start;font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));overflow-wrap:break-word;color:currentColor;flex:1}@media(min-width:40rem){.select__value{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.select__value[data-placeholder=true]{color:var(--field-placeholder,var(--muted))}.select__value [data-slot=list-box-item-indicator]{display:none}.select__clear-button{isolation:isolate;width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5);transform-origin:50%;transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);border-radius:calc(var(--radius) * 1.5);padding:var(--spacing);color:var(--muted);-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;cursor:var(--cursor-interactive);background-color:#0000;flex-shrink:0;justify-content:center;align-self:center;align-items:center;display:inline-flex;position:relative}.select__clear-button:after{inset:calc(var(--spacing) * -.5);content:"";position:absolute}.select__clear-button:not([data-empty=true]){transition:opacity .15s var(--ease-smooth)}.select__clear-button:not([data-empty=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *),.select__clear-button:not([data-empty=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.select__clear-button:not([data-empty=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.select__clear-button:not([data-empty=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.select__clear-button:not([data-empty=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.select__clear-button:not([data-empty=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.select__clear-button[data-empty=true]{pointer-events:none;opacity:0}.select__clear-button [data-slot=select-clear-button-icon]{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}@media(hover:hover){.select__clear-button:hover,.select__clear-button[data-hovered=true]{background-color:var(--default-hover)}}.select__clear-button:active,.select__clear-button[data-pressed=true]{transform:scale(.93)}.select__indicator{color:var(--field-placeholder,var(--muted));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;flex-shrink:0;justify-content:center;align-items:center;margin-block:auto;transition-duration:.15s;display:flex;position:absolute;inset-block:0;inset-inline-end:calc(var(--spacing) * 2)}.select__indicator[data-open=true]{rotate:180deg}.select__indicator[data-slot=select-default-indicator]{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.select__popover{min-width:var(--trigger-width);transform-origin:var(--trigger-anchor-point);scroll-padding-block:var(--spacing);scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);overscroll-behavior:contain;background-color:var(--overlay);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-radius:min(32px,var(--radius-3xl));box-shadow:var(--overlay-shadow);padding:0;overflow-y:auto}.select__popover:focus-visible:not([data-focused]),.select__popover[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.select__popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.15s;--tw-ease:ease;--tw-enter-opacity:0;--tw-enter-scale:.95;transition-duration:.15s;transition-timing-function:ease}.select__popover[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.select__popover[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.select__popover[data-entering=true][data-placement=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.select__popover[data-entering=true][data-placement=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.select__popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:ease;--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:ease}.select__popover[data-exiting=true],.select__popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.select__popover [data-slot=popover-overlay-arrow]{fill:var(--overlay)}.select__popover[data-placement=bottom] [data-slot=popover-overlay-arrow]{rotate:180deg}.select__popover[data-placement=left] [data-slot=popover-overlay-arrow]{rotate:-90deg}.select__popover[data-placement=right] [data-slot=popover-overlay-arrow]{rotate:90deg}.select__popover [data-slot=list-box]{padding:calc(var(--spacing) * 1.5);--tw-outline-style:none;outline-style:none}.select__popover [data-slot=list-box-item]{padding-inline:calc(var(--spacing) * 2.5)}.select__popover [data-slot=list-box]:not([aria-multiselectable=true]) [data-slot=list-box-item-indicator],.select__popover [data-slot=list-box]:not([aria-multiselectable=true]) [data-slot=list-box-item-indicator] [data-slot=list-box-item-indicator--checkmark]{transition-property:none}.select--full-width,.select__trigger--full-width{width:100%}.autocomplete{gap:var(--spacing);flex-direction:column;display:flex}.autocomplete__trigger{isolation:isolate;min-height:calc(var(--spacing) * 9);border-radius:var(--field-radius,calc(var(--radius) * 1.5));border-style:var(--tw-border-style);background-color:var(--field-background,var(--default));padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--field-foreground,var(--foreground));--tw-shadow:var(--field-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-outline-style:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;transition:background-color .15s var(--ease-smooth),border-color .15s var(--ease-smooth),box-shadow .15s var(--ease-out);border-width:1px;outline-style:none;display:inline-flex;position:relative}.autocomplete__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *),.autocomplete__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.autocomplete__trigger:is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.autocomplete__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.autocomplete__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.autocomplete__trigger:not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.autocomplete__trigger{cursor:var(--cursor-interactive);border-width:var(--border-width-field);border-color:var(--field-border)}.autocomplete__trigger:has(.autocomplete__indicator){padding-inline-end:calc(var(--spacing) * 7)}@media(hover:hover){.autocomplete__trigger:hover:not(:has(.autocomplete__clear-button:hover)),.autocomplete__trigger[data-hovered=true]:not(:has(.autocomplete__clear-button:hover)){background-color:var(--field-hover);border-color:var(--field-border-hover)}}.autocomplete__trigger:focus-visible:not([data-focused]),.autocomplete__trigger[data-focus-visible=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--focus);--tw-ring-offset-width:var(--ring-offset-width);--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--background);--tw-outline-style:none;border-color:var(--field-border-focus);background-color:var(--field-focus);outline-style:none}.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger{outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--danger);--tw-outline-style:solid;--tw-ring-offset-width:3px;outline-style:solid}:is(.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger):focus,:is(.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger):focus-visible,:is(.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger)[data-focused=true],:is(.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger)[data-focus-visible=true],:is(.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger):focus-within,:is(.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger)[data-focus-within=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--danger);--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.autocomplete[data-invalid=true] .autocomplete__trigger,.autocomplete[aria-invalid=true] .autocomplete__trigger{background-color:var(--field-focus)}.autocomplete__trigger:disabled,.autocomplete__trigger[data-disabled=true],.autocomplete__trigger[aria-disabled=true]{opacity:var(--disabled-opacity);cursor:var(--cursor-disabled);pointer-events:none}.autocomplete--secondary .autocomplete__trigger{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:var(--autocomplete-trigger-bg);--autocomplete-trigger-bg:var(--default);--autocomplete-trigger-bg-hover:var(--default-hover);--autocomplete-trigger-bg-focus:var(--default)}@media(hover:hover){.autocomplete--secondary .autocomplete__trigger:hover:not(:has(.autocomplete__clear-button:hover)),.autocomplete--secondary .autocomplete__trigger[data-hovered=true]:not(:has(.autocomplete__clear-button:hover)){background-color:var(--autocomplete-trigger-bg-hover)}}.autocomplete--secondary .autocomplete__trigger:focus-visible:not([data-focused]),.autocomplete--secondary .autocomplete__trigger[data-focus-visible=true],.autocomplete[data-invalid=true] :is(.autocomplete--secondary .autocomplete__trigger),.autocomplete[aria-invalid=true] :is(.autocomplete--secondary .autocomplete__trigger){background-color:var(--autocomplete-trigger-bg-focus)}.autocomplete__value{text-align:start;font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));overflow-wrap:break-word;color:currentColor;flex:1}@media(min-width:40rem){.autocomplete__value{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.autocomplete__value[data-placeholder=true]{color:var(--field-placeholder,var(--muted))}.autocomplete__value [data-slot=list-box-item-indicator]{display:none}.autocomplete__indicator{color:var(--field-placeholder,var(--muted));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;cursor:var(--cursor-interactive);flex-shrink:0;justify-content:center;align-items:center;margin-block:auto;transition-duration:.15s;display:flex;position:absolute;inset-block:0;inset-inline-end:calc(var(--spacing) * 2)}.autocomplete__indicator[data-open=true]{rotate:180deg}.autocomplete__indicator[data-slot=autocomplete-default-indicator]{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.autocomplete__popover{width:var(--trigger-width);max-width:var(--trigger-width);transform-origin:var(--trigger-anchor-point);scroll-padding-block:var(--spacing);overscroll-behavior:contain;background-color:var(--overlay);padding:0;padding-top:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-outline-style:none;border-radius:min(32px,var(--radius-3xl));box-shadow:var(--overlay-shadow);outline-style:none;flex-direction:column;display:flex;overflow:hidden}.autocomplete__popover:focus,.autocomplete__popover:focus-visible:not([data-focused]),.autocomplete__popover[data-focus-visible=true]{--tw-outline-style:none;outline-style:none}.autocomplete__popover[data-entering=true]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.25s;--tw-ease:cubic-bezier(.32, .72, 0, 1);--tw-enter-opacity:0;--tw-enter-scale:.95;transition-duration:.25s;transition-timing-function:cubic-bezier(.32,.72,0,1)}.autocomplete__popover[data-entering=true][data-placement=top]{--tw-enter-translate-y:calc(1*var(--spacing))}.autocomplete__popover[data-entering=true][data-placement=bottom]{--tw-enter-translate-y:calc(1*var(--spacing)*-1)}.autocomplete__popover[data-entering=true][data-placement=left]{--tw-enter-translate-x:calc(1*var(--spacing))}.autocomplete__popover[data-entering=true][data-placement=right]{--tw-enter-translate-x:calc(1*var(--spacing)*-1)}.autocomplete__popover[data-exiting=true]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none);--tw-duration:.1s;--tw-ease:cubic-bezier(.25, .46, .45, .94);--tw-exit-scale:.95;--tw-exit-opacity:0;pointer-events:none;transition-duration:.1s;transition-timing-function:cubic-bezier(.25,.46,.45,.94)}.autocomplete__popover[data-exiting=true],.autocomplete__popover[data-entering=true]{transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));will-change:opacity,transform}.autocomplete__popover [data-slot=popover-overlay-arrow]{fill:var(--overlay)}.autocomplete__popover[data-placement=bottom] [data-slot=popover-overlay-arrow]{rotate:180deg}.autocomplete__popover[data-placement=left] [data-slot=popover-overlay-arrow]{rotate:-90deg}.autocomplete__popover[data-placement=right] [data-slot=popover-overlay-arrow]{rotate:90deg}.autocomplete__popover [data-slot=list-box]{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);min-height:0;max-height:320px;padding:calc(var(--spacing) * 1.5);--tw-outline-style:none;outline-style:none;overflow-y:auto}.autocomplete__popover [data-slot=list-box-item]{padding-inline:calc(var(--spacing) * 2.5)}.autocomplete__popover [role=presentation]>[data-slot=list-box-item]{width:calc(100% - var(--spacing) * 3)}.autocomplete__popover [data-slot=list-box]:not([aria-multiselectable=true]) [data-slot=list-box-item-indicator],.autocomplete__popover [data-slot=list-box]:not([aria-multiselectable=true]) [data-slot=list-box-item-indicator] [data-slot=list-box-item-indicator--checkmark]{transition-property:none}.autocomplete__popover [data-slot=search-field]{padding-inline:calc(var(--spacing) * 3);padding-block:var(--spacing);--tw-outline-style:none;outline-style:none;flex-shrink:0}.autocomplete__popover [data-slot=empty-state]{text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--overlay-foreground)}@supports (color:color-mix(in lab,red,red)){.autocomplete__popover [data-slot=empty-state]{color:color-mix(in oklab,var(--overlay-foreground) 60%,transparent)}}.autocomplete--full-width,.autocomplete__trigger--full-width{width:100%}.autocomplete__clear-button{isolation:isolate;height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);transform-origin:50%;border-radius:calc(var(--radius) * 1.5);padding:var(--spacing);color:var(--muted);-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5);cursor:var(--cursor-interactive);transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);background-color:#0000;flex-shrink:0;justify-content:center;align-self:center;align-items:center;margin-inline-end:0;display:inline-flex;position:relative}.autocomplete__clear-button:not([data-empty=true]){transition:opacity .15s var(--ease-smooth)}.autocomplete__clear-button:not([data-empty=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *),.autocomplete__clear-button:not([data-empty=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):before,.autocomplete__clear-button:not([data-empty=true]):is([data-reduce-motion=true],[data-reduce-motion=true] *):after{transition-property:none}@media(prefers-reduced-motion:reduce){.autocomplete__clear-button:not([data-empty=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)),.autocomplete__clear-button:not([data-empty=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):before,.autocomplete__clear-button:not([data-empty=true]):not(:is([data-reduce-motion=true],[data-reduce-motion=true] *)):after{transition-property:none}}.autocomplete__clear-button[data-empty=true]{pointer-events:none;opacity:0}.autocomplete__clear-button [data-slot=autocomplete-clear-button-icon]{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}@media(hover:hover){.autocomplete__clear-button:hover,.autocomplete__clear-button[data-hovered=true]{background-color:var(--default-hover)}}.autocomplete__clear-button:active,.autocomplete__clear-button[data-pressed=true]{transform:scale(.93)}.kbd{height:calc(var(--spacing) * 6);align-items:center;display:inline-flex}:where(.kbd>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * .5) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-x-reverse)))}.kbd{border-radius:calc(var(--radius) * 1);background-color:var(--default);padding-inline:calc(var(--spacing) * 2);text-align:center;font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;color:var(--muted)}:where(.kbd:where(:dir(rtl),[dir=rtl],[dir=rtl] *)>:not(:last-child)){--tw-space-x-reverse:1}.kbd{word-spacing:-.25rem}.kbd__abbr{justify-content:center;align-items:center;width:100%;height:100%;text-decoration:none;display:flex}.kbd__content{justify-content:center;align-items:center;display:flex}.kbd--light{background-color:#0000}.typography,.typography-prose{color:var(--foreground)}.typography-prose h1{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography-prose h2{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography-prose h3{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography-prose h4{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography-prose h5{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography-prose h6{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography-prose p{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.typography-prose code{border-radius:calc(var(--radius) * .75);background-color:var(--default);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground)}.typography-prose a{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--link);text-underline-offset:4px;text-decoration-line:underline}.typography-prose blockquote{margin-top:calc(var(--spacing) * 4);border-inline-start-style:var(--tw-border-style);border-inline-start-width:4px;border-color:var(--border);color:var(--muted);padding-inline-start:calc(var(--spacing) * 4);font-style:italic}.typography-prose ul{margin-block:calc(var(--spacing) * 4);list-style-type:disc}:where(.typography-prose ul>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.typography-prose ul{padding-inline-start:calc(var(--spacing) * 6)}.typography-prose ol{margin-block:calc(var(--spacing) * 4);list-style-type:decimal}:where(.typography-prose ol>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.typography-prose ol{padding-inline-start:calc(var(--spacing) * 6)}.typography-prose li{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.typography-prose hr{margin-block:calc(var(--spacing) * 8);border-color:var(--separator)}.typography-prose pre{margin-block:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * 1.5);background-color:var(--default);padding:calc(var(--spacing) * 4);font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);overflow-x:auto}.typography-prose strong{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}.typography-prose em{font-style:italic}.typography-prose img{margin-block:calc(var(--spacing) * 4);border-radius:calc(var(--radius) * 1.5)}.typography--h1{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography--h2{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography--h3{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography--h4{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography--h5{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography--h6{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.typography--body{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.typography--body-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.typography--body-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.typography--code{border-radius:calc(var(--radius) * .75);background-color:var(--default);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground)}.typography--align-start{text-align:start}.typography--align-center{text-align:center}.typography--align-end{text-align:end}.typography--align-justify{text-align:justify}.typography--color-default{color:var(--foreground)}.typography--color-muted{color:var(--muted)}.typography--truncate{text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.typography--weight-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.typography--weight-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.typography--weight-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.typography--weight-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.scroll-shadow{--scroll-shadow-size:40px;--scroll-shadow-scrollbar-size:10px;--scroll-shadow-offset:0px;position:relative}@property --scroll-shadow-start-fade{syntax:"";inherits:false;initial-value:0}@property --scroll-shadow-end-fade{syntax:"";inherits:false;initial-value:0}@keyframes scroll-shadow-start-fade{0%{--scroll-shadow-start-fade:0px}to{--scroll-shadow-start-fade:var(--scroll-shadow-size)}}@keyframes scroll-shadow-end-fade{0%{--scroll-shadow-end-fade:var(--scroll-shadow-size)}to{--scroll-shadow-end-fade:0px}}.scroll-shadow--vertical{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);overflow-y:auto}.scroll-shadow--horizontal{scrollbar-width:var(--scrollbar-width);scrollbar-color:var(--scrollbar-color);scrollbar-gutter:var(--scrollbar-gutter);overflow-x:auto}.scroll-shadow--fade.scroll-shadow--vertical:where([data-top-scroll=true],[data-bottom-scroll=true],[data-top-bottom-scroll=true],[data-scroll-shadow-mode=auto]){mask-image:linear-gradient(var(--scroll-linear-gradient)),linear-gradient(#000,#000);mask-position:0 0,100% 0;mask-repeat:no-repeat;mask-size:calc(100% - var(--scroll-shadow-scrollbar-size)) 100%,var(--scroll-shadow-scrollbar-size) 100%;-webkit-mask-image:linear-gradient(var(--scroll-linear-gradient)),linear-gradient(#000,#000);-webkit-mask-position:0 0,100% 0;-webkit-mask-repeat:no-repeat;-webkit-mask-size:calc(100% - var(--scroll-shadow-scrollbar-size)) 100%,var(--scroll-shadow-scrollbar-size) 100%}.scroll-shadow--fade.scroll-shadow--vertical[data-top-scroll=true]{--scroll-linear-gradient:0deg, #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--fade.scroll-shadow--vertical[data-bottom-scroll=true]{--scroll-linear-gradient:180deg, #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--fade.scroll-shadow--vertical[data-top-bottom-scroll=true]{--scroll-linear-gradient:#000, #000, transparent 0, #000 var(--scroll-shadow-size), #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--fade.scroll-shadow--horizontal:where([data-left-scroll=true],[data-right-scroll=true],[data-left-right-scroll=true],[data-scroll-shadow-mode=auto]){mask-image:linear-gradient(var(--scroll-linear-gradient)),linear-gradient(#000,#000);mask-position:0 0,0 100%;mask-repeat:no-repeat;mask-size:100% calc(100% - var(--scroll-shadow-scrollbar-size)),100% var(--scroll-shadow-scrollbar-size);-webkit-mask-image:linear-gradient(var(--scroll-linear-gradient)),linear-gradient(#000,#000);-webkit-mask-position:0 0,0 100%;-webkit-mask-repeat:no-repeat;-webkit-mask-size:100% calc(100% - var(--scroll-shadow-scrollbar-size)),100% var(--scroll-shadow-scrollbar-size)}.scroll-shadow--fade.scroll-shadow--horizontal[data-left-scroll=true]{--scroll-linear-gradient:270deg, #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--fade.scroll-shadow--horizontal[data-left-scroll=true]:dir(rtl){--scroll-linear-gradient:90deg, #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--fade.scroll-shadow--horizontal[data-right-scroll=true]{--scroll-linear-gradient:90deg, #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--fade.scroll-shadow--horizontal[data-right-scroll=true]:dir(rtl){--scroll-linear-gradient:270deg, #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--fade.scroll-shadow--horizontal[data-left-right-scroll=true]{--scroll-linear-gradient:to right, #000, #000, transparent 0, #000 var(--scroll-shadow-size), #000 calc(100% - var(--scroll-shadow-size)), transparent}.scroll-shadow--hide-scrollbar{scrollbar-color:auto;scrollbar-gutter:auto;-ms-overflow-style:none;scrollbar-width:none;--scroll-shadow-scrollbar-size:0px}@supports (animation-timeline:scroll(self)){.scroll-shadow.scroll-shadow--fade[data-scroll-shadow-mode=auto]{animation:linear both scroll-shadow-start-fade,linear both scroll-shadow-end-fade;animation-range:var(--scroll-shadow-offset) calc(var(--scroll-shadow-offset) + var(--scroll-shadow-size)),calc(100% - var(--scroll-shadow-size) - var(--scroll-shadow-offset)) calc(100% - var(--scroll-shadow-offset))}.scroll-shadow.scroll-shadow--fade.scroll-shadow--vertical[data-scroll-shadow-mode=auto]{--scroll-linear-gradient:180deg, transparent 0, #000 var(--scroll-shadow-start-fade), #000 calc(100% - var(--scroll-shadow-end-fade)), transparent 100%;animation-timeline:scroll(self),scroll(self)}.scroll-shadow.scroll-shadow--fade.scroll-shadow--horizontal[data-scroll-shadow-mode=auto]{--scroll-linear-gradient:90deg, transparent 0, #000 var(--scroll-shadow-start-fade), #000 calc(100% - var(--scroll-shadow-end-fade)), transparent 100%;animation-timeline:scroll(self inline),scroll(self inline)}.scroll-shadow.scroll-shadow--fade.scroll-shadow--horizontal[data-scroll-shadow-mode=auto]:dir(rtl){--scroll-linear-gradient:270deg, transparent 0, #000 var(--scroll-shadow-start-fade), #000 calc(100% - var(--scroll-shadow-end-fade)), transparent 100%}}}@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.static{position:static}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing) * 2)}.top-5{top:calc(var(--spacing) * 5)}.right-0{right:0}.right-2{right:calc(var(--spacing) * 2)}.left-3\.5{left:calc(var(--spacing) * 3.5)}.z-10{z-index:10}.mx-2{margin-inline:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.mt-1{margin-top:var(--spacing)}.mt-9{margin-top:calc(var(--spacing) * 9)}.mr-3{margin-right:calc(var(--spacing) * 3)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.aspect-\[4\/3\]{aspect-ratio:4/3}.\!size-\[48px\]{width:48px!important;height:48px!important}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-\[18px\]{width:18px;height:18px}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-24{height:calc(var(--spacing) * 24)}.h-64{height:calc(var(--spacing) * 64)}.h-auto{height:auto}.h-full{height:100%}.min-h-0{min-height:0}.w-48{width:calc(var(--spacing) * 48)}.w-full{width:100%}.max-w-24{max-width:calc(var(--spacing) * 24)}.min-w-0{min-width:0}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-7{min-width:calc(var(--spacing) * 7)}.min-w-9{min-width:calc(var(--spacing) * 9)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.grid-cols-\[repeat\(auto-fill\,minmax\(min\(100\%\,280px\)\,1fr\)\)\]{grid-template-columns:repeat(auto-fill,minmax(min(100%,280px),1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-start{justify-content:flex-start}.gap-0{gap:0}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-7{column-gap:calc(var(--spacing) * 7)}.gap-y-10{row-gap:calc(var(--spacing) * 10)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.\!rounded-full{border-radius:3.40282e38px!important}.rounded-2xl{border-radius:calc(var(--radius) * 2)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:calc(var(--radius) * 1)}.rounded-md{border-radius:calc(var(--radius) * .75)}.rounded-none{border-radius:0}.rounded-xl{border-radius:calc(var(--radius) * 1.5)}.border-0{border-style:var(--tw-border-style);border-width:0}.bg-accent,.bg-accent\/15{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/15{background-color:color-mix(in oklab,var(--accent) 15%,transparent)}}.bg-background,.bg-background\/90{background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){.bg-background\/90{background-color:color-mix(in oklab,var(--background) 90%,transparent)}}.bg-default\/30{background-color:var(--default)}@supports (color:color-mix(in lab,red,red)){.bg-default\/30{background-color:color-mix(in oklab,var(--default) 30%,transparent)}}.bg-default\/50{background-color:var(--default)}@supports (color:color-mix(in lab,red,red)){.bg-default\/50{background-color:color-mix(in oklab,var(--default) 50%,transparent)}}.bg-default\/60{background-color:var(--default)}@supports (color:color-mix(in lab,red,red)){.bg-default\/60{background-color:color-mix(in oklab,var(--default) 60%,transparent)}}.bg-default\/70{background-color:var(--default)}@supports (color:color-mix(in lab,red,red)){.bg-default\/70{background-color:color-mix(in oklab,var(--default) 70%,transparent)}}.bg-transparent{background-color:#0000}.object-cover{object-fit:cover}.p-0{padding:0}.px-0{padding-inline:0}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-7{padding-inline:calc(var(--spacing) * 7)}.py-1{padding-block:var(--spacing)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-7{padding-bottom:calc(var(--spacing) * 7)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.text-accent{color:var(--accent)}.text-danger{color:var(--danger)}.text-foreground,.text-foreground\/80{color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.text-foreground\/80{color:color-mix(in oklab,var(--foreground) 80%,transparent)}}.text-muted,.text-muted-foreground,.text-muted-foreground\/20{color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/20{color:color-mix(in oklab,var(--muted) 20%,transparent)}}.text-muted-foreground\/60{color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/60{color:color-mix(in oklab,var(--muted) 60%,transparent)}}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.opacity-40{opacity:.4}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-focus-within\:opacity-100:is(:where(.group):focus-within *){opacity:1}@media(hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:bg-default\/40:hover{background-color:var(--default)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-default\/40:hover{background-color:color-mix(in oklab,var(--default) 40%,transparent)}}}.focus\:opacity-100:focus{opacity:1}@media(min-width:64rem){.lg\:w-56{width:calc(var(--spacing) * 56)}.lg\:gap-x-9{column-gap:calc(var(--spacing) * 9)}.lg\:px-10{padding-inline:calc(var(--spacing) * 10)}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}@font-face{font-family:DM Sans;src:url(/fonts/DMSans-Variable.ttf)format("truetype");font-weight:100 1000;font-style:normal;font-display:swap}.share-header{border-bottom:1px solid var(--separator);background:var(--surface);justify-content:space-between;align-items:center;gap:20px;min-height:64px;padding:12px 28px;display:flex}.header-start,.brand{align-items:center;gap:10px;min-width:0;display:flex}.brand{color:var(--foreground);flex-shrink:0;font-weight:600;text-decoration:none}.brand img{border-radius:8px}.header-divider{background:var(--separator);width:1px;height:20px;margin:0 8px}.header-context{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.share-workspace{flex:1;grid-template-columns:minmax(0,1fr) 350px;align-items:stretch;gap:16px;width:100%;min-height:0;padding:16px;display:grid}.recording-column{grid-template-rows:auto minmax(0,1fr) auto;gap:12px;min-width:0;min-height:0;display:grid}.recording-heading{justify-content:space-between;align-items:center;gap:16px;min-height:58px;display:flex}.recording-title{letter-spacing:-.025em;overflow-wrap:anywhere;font-size:22px;font-weight:600}.recording-meta{color:var(--muted);flex-wrap:wrap;align-items:center;gap:8px;margin-top:6px;font-size:12px;display:flex}.recording-meta span+span:before{content:"·";margin-right:8px}.reaction-rail{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;display:flex}.reaction-buttons{flex-wrap:wrap;gap:4px;display:flex}.reaction-buttons .button{min-width:40px}.chapter-list{flex-direction:column;gap:4px;display:flex}.chapter-list .button{white-space:normal;text-align:left;justify-content:flex-start;height:auto;min-height:40px}.timestamp{font-variant-numeric:tabular-nums;color:var(--accent)}.feedback-card{gap:0;min-width:0;height:100%;min-height:0;padding:16px;overflow:hidden}.feedback-tabs{flex-direction:column;flex:1;gap:12px;width:100%;min-height:0;display:flex}.feedback-tabs .tabs__list{width:100%}.feedback-tabs .tabs__tab{flex:1;gap:5px;min-width:0;padding-inline:8px;font-size:12px}.empty-feedback{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:8px;height:100%;padding:12px;display:flex}.empty-feedback>svg{color:var(--muted)}.empty-feedback h2{font-size:14px;font-weight:500}.empty-feedback p{color:var(--muted);max-width:230px;font-size:13px;line-height:1.6}.composer{border-top:1px solid var(--separator);flex-direction:column;flex-shrink:0;gap:8px;margin-top:12px;padding-top:12px;display:flex}.composer textarea{resize:none;height:64px;min-height:56px}.composer-footer{justify-content:space-between;align-items:center;gap:8px;display:flex}.comment-item{border-bottom:1px solid var(--separator);border-radius:var(--radius);padding:14px 8px}.comment-item[data-active=true]{background:var(--accent-soft)}.comment-heading{align-items:center;gap:8px;font-size:13px;display:flex}.comment-heading .button{margin-left:auto}.comment-item p{white-space:pre-wrap;overflow-wrap:anywhere;margin:8px 0;font-size:13px;line-height:1.65}.comment-date{color:var(--muted);font-size:11px}.inline-timestamp{vertical-align:baseline;min-width:0;height:auto;padding:0 2px;display:inline}.transcript-item{border-bottom:1px solid var(--separator);padding:12px 4px}.transcript-item[data-active=true]{background:var(--accent-soft);border-radius:var(--radius)}.transcript-heading{color:var(--muted);align-items:center;gap:8px;font-size:12px;display:flex}.transcript-heading .copy-timestamp{margin-left:auto}.transcript-seek{text-align:left;white-space:normal;justify-content:flex-start;width:100%;height:auto;padding:8px;line-height:1.65}.gate-page{place-items:center;min-height:100svh;padding:24px;display:grid}.gate-card{gap:24px;width:100%;max-width:400px;padding:24px}.gate-content,.gate-form{flex-direction:column;gap:16px;display:flex}.gate-content h1{letter-spacing:-.025em;font-size:22px;font-weight:600}.gate-content p{color:var(--muted);font-size:14px;line-height:1.6}.loading-title{width:48%;height:26px}.loading-meta{width:35%;height:14px;margin-top:12px}.loading-video{aspect-ratio:16/9;width:100%}.loading-sidebar{height:440px}.video-shell{flex-direction:column;gap:0;min-height:0;padding:0;display:flex;overflow:hidden}.video-stage{background:#000;flex:1;width:100%;min-height:0;position:relative}.video-stage video{object-fit:contain;width:100%;height:100%;display:block;position:absolute;inset:0}.video-play-overlay{pointer-events:none;place-items:center;display:grid;position:absolute;inset:0}.video-play-overlay .button{pointer-events:auto}.video-caption{text-align:center;pointer-events:none;position:absolute;bottom:20px;left:8%;right:8%}.video-caption span{color:#fff;-webkit-box-decoration-break:clone;box-decoration-break:clone;background:#000c;border-radius:6px;padding:5px 10px;line-height:1.8}.video-toolbar{background:var(--surface);flex-direction:column;flex-shrink:0;gap:4px;padding:8px 12px;display:flex}.video-controls{align-items:center;gap:4px;display:flex}.video-controls .volume-control{width:72px;margin:0 8px}.video-time{font-variant-numeric:tabular-nums;color:var(--muted);white-space:nowrap;margin:0 8px;font-size:12px}.control-spacer{flex:1}.video-shell:fullscreen{background:#000;border-radius:0;flex-direction:column;display:flex}.video-shell:fullscreen .video-stage{aspect-ratio:auto;flex:1;min-height:0}.video-shell:fullscreen .video-toolbar{flex-shrink:0}.video-end-card{background:#000000b3;flex-direction:column;justify-content:center;align-items:center;gap:16px;display:flex;position:absolute;inset:0}.timeline-markers{height:20px;margin:0 8px;position:relative}.timeline-marker{min-width:18px;height:18px;padding:0 3px;font-size:10px;position:absolute;transform:translate(-50%)}@media(prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important}}.recording-app{flex-direction:column;height:100dvh;display:flex;overflow:hidden}.recording-app .share-header{flex-shrink:0;min-height:56px;padding:10px 20px}.recording-app .recording-heading{min-height:48px}.recording-heading>div{min-width:0}.recording-app .recording-title{-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:20px;display:-webkit-box;overflow:hidden}.share-workspace>aside{min-width:0;min-height:0}.feedback-tabs .tabs__list-container{flex-shrink:0}.feedback-tabs .tabs__panel{flex:1;min-height:0;padding:0;overflow:hidden}.feedback-tabs .tabs__panel:not([hidden]){flex-direction:column;display:flex}.composer[hidden]{display:none}.paged-panel{flex-direction:column;flex:1;gap:8px;height:100%;min-height:0;display:flex}.page-viewport{flex:1;min-height:0;overflow:hidden}.page-flow{column-count:1;column-fill:auto;overflow-wrap:anywhere;column-gap:24px;height:100%}.page-flow .comment-item,.page-flow .transcript-item{break-inside:avoid-column}.page-navigation{color:var(--muted);flex-shrink:0;justify-content:space-between;align-items:center;font-size:12px;display:flex}.page-navigation[hidden]{display:none}.recording-details{margin-bottom:20px}.recording-details h2{margin-bottom:10px;font-size:14px;font-weight:600}.recording-details p{white-space:pre-wrap;color:var(--muted);font-size:14px;line-height:1.7}.video-progress{flex-shrink:0;padding:8px 0;position:relative}.video-seek-input{opacity:0;cursor:pointer;width:100%;height:100%;margin:0;position:absolute;inset:0}.video-progress:focus-within{outline:2px solid var(--focus);outline-offset:2px;border-radius:var(--radius)}.mobile-view-switch{display:none}@media(max-width:1100px){.share-workspace{grid-template-columns:minmax(0,1fr) 310px;gap:12px;padding:12px}.volume-control{display:none}}@media(max-width:760px){.mobile-view-switch{flex-shrink:0;justify-content:center;gap:8px;padding:8px 12px 0;display:flex}.share-workspace{grid-template-columns:minmax(0,1fr)}.recording-app[data-mobile-view=watch] .share-workspace>aside,.recording-app[data-mobile-view=feedback] .recording-column,.recording-app .header-context,.recording-app .header-divider{display:none}.recording-app .share-header{padding:8px 12px}.recording-app .recording-title{font-size:18px}.feedback-card{padding:12px}.reaction-buttons{gap:0}.reaction-buttons .button{min-width:32px;padding-inline:6px}.reaction-rail{flex-wrap:nowrap;gap:4px}.recording-app .recording-meta{gap:4px;font-size:11px}.video-time{margin:0 4px;font-size:11px}.skip-control,.pip-control{display:none}}@media(max-height:620px){.recording-app .share-header{min-height:44px;padding-block:6px}.recording-app .share-workspace{gap:8px;padding:8px}.feedback-card{padding:10px}.composer{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:6px;margin-top:8px;padding-top:8px;display:grid}.composer textarea{height:40px;min-height:40px}.composer-footer,.composer .alert{grid-column:1/-1}.empty-feedback{gap:4px;padding:4px}.empty-feedback>svg{display:none}.empty-feedback p{font-size:12px}}.mobile-comment-button,.mobile-feedback-back{display:none}.timestamp-reactions{pointer-events:none;position:absolute;inset:0;overflow:hidden}.timestamp-reaction{font-size:30px;animation:3s ease-out forwards reaction-float;position:absolute;bottom:12%}@keyframes reaction-float{0%{opacity:0;transform:translateY(12px)scale(.7)}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:translateY(-100px)scale(1.1)}}@media(prefers-reduced-motion:reduce){.timestamp-reaction{animation:none}}@media(max-width:760px){.mobile-comment-button{flex-shrink:0;display:inline-flex}.mobile-feedback-back{padding:8px 12px 0;display:flex}}.creator-info{align-items:center;gap:8px;min-width:0;margin-top:6px;display:flex}.creator-info>div{min-width:0}.creator-name{text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600;overflow:hidden}.creator-bio{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.timeline-markers{flex-shrink:0;height:24px}.timeline-markers .timeline-marker{min-width:24px;height:24px;font-size:12px}.library-dashboard{height:100dvh;font-family:DM Sans,sans-serif}.dark .library-dashboard>.card{background:var(--background)}.library-dashboard .custom-scrollbar{scrollbar-width:thin}@media(max-width:640px){.library-dashboard{flex-direction:column;height:auto;min-height:100dvh}.library-dashboard>aside{width:100%;padding:16px}.library-dashboard>aside>nav{display:flex}.library-dashboard>aside .library-folders{max-height:160px}.library-dashboard>.card{min-height:65dvh;margin:0 8px 8px}}.library-dashboard{background:var(--background)}@supports (color:color-mix(in lab,red,red)){.library-dashboard{background:color-mix(in srgb,var(--background) 92%,var(--foreground) 8%)}}.dark .library-dashboard,.library-dashboard>.card,.dark .library-dashboard>.card{background:var(--background)}@supports (color:color-mix(in lab,red,red)){.dark .library-dashboard>.card{background:color-mix(in srgb,var(--background) 92%,var(--foreground) 8%)}}.recording-app .viewer-heading{gap:16px;min-height:48px}.viewer-identity{flex:1;align-items:flex-start;gap:16px;min-width:0;display:flex}.viewer-caption{flex:1;min-width:0;height:48px}.recording-app .viewer-heading .recording-title{letter-spacing:normal;white-space:nowrap;text-overflow:ellipsis;font-size:12px;font-weight:500;line-height:20px;display:block;overflow:hidden}.recording-app .viewer-heading .recording-meta{white-space:nowrap;gap:8px;height:24px;margin-top:4px;font-size:11px}.viewer-heading-actions{flex-shrink:0;align-items:center;gap:8px;display:flex}@media(max-width:480px){.recording-app .viewer-heading{flex-wrap:wrap}.viewer-identity{flex-basis:100%}.viewer-heading-actions{margin-left:auto}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-border-spacing-x{syntax:"";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}@keyframes caret-blink{0%,70%,to{opacity:1}20%,50%{opacity:0}}@keyframes skeleton{to{transform:translate(200%)}} diff --git a/services/recordly-share/worker/web/dist/_astro/client.9mxnYheX.js b/services/recordly-share/worker/web/dist/_astro/client.9mxnYheX.js new file mode 100644 index 000000000..bf3b6639f --- /dev/null +++ b/services/recordly-share/worker/web/dist/_astro/client.9mxnYheX.js @@ -0,0 +1,9 @@ +import{b as Wo,a as Fo,r as Wt}from"./index.DeQQz02V.js";var d0={exports:{}},We={},h0={exports:{}},o0={};var jy;function $o(){return jy||(jy=1,(function(T){function tl(E,C){var M=E.length;E.push(C);l:for(;0>>1,F=E[ll];if(0>>1;llBl(Ia,M))itBl(na,Ia)?(E[ll]=na,E[it]=M,ll=it):(E[ll]=Ia,E[ea]=M,ll=ea);else if(itBl(na,M))E[ll]=na,E[it]=M,ll=it;else break l}}return C}function Bl(E,C){var M=E.sortIndex-C.sortIndex;return M!==0?M:E.id-C.id}if(T.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var il=performance;T.unstable_now=function(){return il.now()}}else{var gt=Date,Mu=gt.now();T.unstable_now=function(){return gt.now()-Mu}}var Ml=[],Il=[],Pu=1,V=null,W=3,ct=!1,_l=!1,Ft=!1,al=!1,Rt=typeof setTimeout=="function"?setTimeout:null,la=typeof clearTimeout=="function"?clearTimeout:null,Fe=typeof setImmediate<"u"?setImmediate:null;function ta(E){for(var C=p(Il);C!==null;){if(C.callback===null)S(Il);else if(C.startTime<=E)S(Il),C.sortIndex=C.expirationTime,tl(Ml,C);else break;C=p(Il)}}function ua(E){if(Ft=!1,ta(E),!_l)if(p(Ml)!==null)_l=!0,St||(St=!0,Bt());else{var C=p(Il);C!==null&&ql(ua,C.startTime-E)}}var St=!1,X=-1,$e=5,Uu=-1;function $t(){return al?!0:!(T.unstable_now()-Uu<$e)}function st(){if(al=!1,St){var E=T.unstable_now();Uu=E;var C=!0;try{l:{_l=!1,Ft&&(Ft=!1,la(X),X=-1),ct=!0;var M=W;try{t:{for(ta(E),V=p(Ml);V!==null&&!(V.expirationTime>E&&$t());){var ll=V.callback;if(typeof ll=="function"){V.callback=null,W=V.priorityLevel;var F=ll(V.expirationTime<=E);if(E=T.unstable_now(),typeof F=="function"){V.callback=F,ta(E),C=!0;break t}V===p(Ml)&&S(Ml),ta(E)}else S(Ml);V=p(Ml)}if(V!==null)C=!0;else{var It=p(Il);It!==null&&ql(ua,It.startTime-E),C=!1}}break l}finally{V=null,W=M,ct=!1}C=void 0}}finally{C?Bt():St=!1}}}var Bt;if(typeof Fe=="function")Bt=function(){Fe(st)};else if(typeof MessageChannel<"u"){var aa=new MessageChannel,Ie=aa.port2;aa.port1.onmessage=st,Bt=function(){Ie.postMessage(null)}}else Bt=function(){Rt(st,0)};function ql(E,C){X=Rt(function(){E(T.unstable_now())},C)}T.unstable_IdlePriority=5,T.unstable_ImmediatePriority=1,T.unstable_LowPriority=4,T.unstable_NormalPriority=3,T.unstable_Profiling=null,T.unstable_UserBlockingPriority=2,T.unstable_cancelCallback=function(E){E.callback=null},T.unstable_forceFrameRate=function(E){0>E||125ll?(E.sortIndex=M,tl(Il,E),p(Ml)===null&&E===p(Il)&&(Ft?(la(X),X=-1):Ft=!0,ql(ua,M-ll))):(E.sortIndex=F,tl(Ml,E),_l||ct||(_l=!0,St||(St=!0,Bt()))),E},T.unstable_shouldYield=$t,T.unstable_wrapCallback=function(E){var C=W;return function(){var M=W;W=C;try{return E.apply(this,arguments)}finally{W=M}}}})(o0)),o0}var ry;function Io(){return ry||(ry=1,h0.exports=$o()),h0.exports}var xy;function ko(){if(xy)return We;xy=1;var T=Io(),tl=Wo(),p=Fo();function S(l){var t="https://react.dev/errors/"+l;if(1fa||(l.current=Rf[fa],Rf[fa]=null,fa--)}function I(l,t){fa++,Rf[fa]=l.current,l.current=t}var Tt=zt(null),le=zt(null),kt=zt(null),ke=zt(null);function Pe(l,t){switch(I(kt,t),I(le,l),I(Tt,null),t.nodeType){case 9:case 11:l=(l=t.documentElement)&&(l=l.namespaceURI)?F1(l):0;break;default:if(l=t.tagName,t=t.namespaceURI)t=F1(t),l=$1(t,l);else switch(l){case"svg":l=1;break;case"math":l=2;break;default:l=0}}zl(Tt),I(Tt,l)}function ca(){zl(Tt),zl(le),zl(kt)}function Bf(l){var t=l.memoizedState;t!==null&&(Wa._currentValue=t.memoizedState,I(ke,l)),t=Tt.current;var u=$1(t,l.type);t!==u&&(I(le,l),I(Tt,u))}function ln(l){le.current===l&&(zl(Tt),zl(le)),ke.current===l&&(zl(ke),Wa._currentValue=Hu)}var qf,S0;function Pt(l){if(qf===void 0)try{throw Error()}catch(u){var t=u.stack.trim().match(/\n( *(at )?)/);qf=t&&t[1]||"",S0=-1)":-1e||i[a]!==d[e]){var g=` +`+i[a].replace(" at new "," at ");return l.displayName&&g.includes("")&&(g=g.replace("",l.displayName)),g}while(1<=a&&0<=e);break}}}finally{Yf=!1,Error.prepareStackTrace=u}return(u=l?l.displayName||l.name:"")?Pt(u):""}function Iy(l,t){switch(l.tag){case 26:case 27:case 5:return Pt(l.type);case 16:return Pt("Lazy");case 13:return l.child!==t&&t!==null?Pt("Suspense Fallback"):Pt("Suspense");case 19:return Pt("SuspenseList");case 0:case 15:return Gf(l.type,!1);case 11:return Gf(l.type.render,!1);case 1:return Gf(l.type,!0);case 31:return Pt("Activity");case 30:return Pt("ViewTransition");default:return""}}function s0(l){try{var t="",u=null;do t+=Iy(l,u),u=l,l=l.return;while(l);return t}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var Qf=Object.prototype.hasOwnProperty,Xf=T.unstable_scheduleCallback,Zf=T.unstable_cancelCallback,ky=T.unstable_shouldYield,Py=T.unstable_requestPaint,jl=T.unstable_now,ld=T.unstable_getCurrentPriorityLevel,z0=T.unstable_ImmediatePriority,T0=T.unstable_UserBlockingPriority,tn=T.unstable_NormalPriority,td=T.unstable_LowPriority,b0=T.unstable_IdlePriority,ud=T.log,ad=T.unstable_setDisableYieldValue,te=null,rl=null;function lu(l){if(typeof ud=="function"&&ad(l),rl&&typeof rl.setStrictMode=="function")try{rl.setStrictMode(te,l)}catch{}}var xl=Math.clz32?Math.clz32:fd,ed=Math.log,nd=Math.LN2;function fd(l){return l>>>=0,l===0?32:31-(ed(l)/nd|0)|0}var un=256,an=262144,en=4194304;function Cu(l){var t=l&42;if(t!==0)return t;switch(l&-l){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return l&-l;case 262144:case 524288:case 1048576:case 2097152:return l&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return l&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return l}}function nn(l,t,u){var a=l.pendingLanes;if(a===0)return 0;var e=0,n=l.suspendedLanes,f=l.pingedLanes;l=l.warmLanes;var c=a&134217727;return c!==0?(a=c&~n,a!==0?e=Cu(a):(f&=c,f!==0?e=Cu(f):u||(u=c&~l,u!==0&&(e=Cu(u))))):(c=a&~n,c!==0?e=Cu(c):f!==0?e=Cu(f):u||(u=a&~l,u!==0&&(e=Cu(u)))),e===0?0:t!==0&&t!==e&&(t&n)===0&&(n=e&-e,u=t&-t,n>=u||n===32&&(u&4194048)!==0)?t:e}function ue(l,t){return(l.pendingLanes&~(l.suspendedLanes&~l.pingedLanes)&t)===0}function E0(l,t){(t&8)!==0&&(t|=t&32);var u=l.entangledLanes;if(u!==0)for(l=l.entanglements,u&=t;0u;u++)t.push(l);return t}function ae(l,t){l.pendingLanes|=t,t!==268435456&&(l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0)}function id(l,t,u,a,e,n){var f=l.pendingLanes;l.pendingLanes=u,l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0,l.expiredLanes&=u,l.entangledLanes&=u,l.errorRecoveryDisabledLanes&=u,l.shellSuspendCounter=0;var c=l.entanglements,i=l.expirationTimes,d=l.hiddenUpdates;for(u=f&~u;0"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ff=!1;if(Gt)try{var ce={};Object.defineProperty(ce,"passive",{get:function(){Ff=!0}}),window.addEventListener("test",ce,ce),window.removeEventListener("test",ce,ce)}catch{Ff=!1}var tu=null,$f=null,dn=null;function J0(){if(dn)return dn;var l,t=$f,u=t.length,a,e="value"in tu?tu.value:tu.textContent,n=e.length;for(l=0;l=me),I0=" ",k0=!1;function P0(l,t){switch(l){case"keyup":return Ld.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function lv(l){return l=l.detail,typeof l=="object"&&"data"in l?l.data:null}var Sa=!1;function pd(l,t){switch(l){case"compositionend":return lv(t);case"keypress":return t.which!==32?null:(k0=!0,I0);case"textInput":return l=t.data,l===I0&&k0?null:l;default:return null}}function wd(l,t){if(Sa)return l==="compositionend"||!tc&&P0(l,t)?(l=J0(),dn=$f=tu=null,Sa=!1,l):null;switch(l){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1"u")return null;try{return l.activeElement||l.body}catch{return l.body}}function iv(l){for(;l&&l.firstChild;)l=l.firstChild;return l}function vv(l,t){var u=iv(l);l=0;for(var a;u;){if(u.nodeType===3){if(a=l+u.textContent.length,l<=t&&a>=t)return{node:u,offset:t-l};l=a}l:{for(;u;){if(u.nextSibling){u=u.nextSibling;break l}u=u.parentNode}u=void 0}u=iv(u)}}function mv(l,t){return l&&t?l===t?!0:l&&l.nodeType===3?!1:t&&t.nodeType===3?mv(l,t.parentNode):"contains"in l?l.contains(t):l.compareDocumentPosition?!!(l.compareDocumentPosition(t)&16):!1:!1}function yv(l){l=l!=null&&l.ownerDocument!=null&&l.ownerDocument.defaultView!=null?l.ownerDocument.defaultView:window;for(var t=ec(l.document);t instanceof l.HTMLIFrameElement;){try{var u=typeof t.contentWindow.location.href=="string"}catch{u=!1}if(u)l=t.contentWindow;else break;t=ec(l.document)}return t}function nc(l){var t=l&&l.nodeName&&l.nodeName.toLowerCase();return t&&(t==="input"&&(l.type==="text"||l.type==="search"||l.type==="tel"||l.type==="url"||l.type==="password")||t==="textarea"||l.contentEditable==="true")}var th=Gt&&"documentMode"in document&&11>=document.documentMode,sa=null,fc=null,oe=null,cc=!1;function dv(l,t,u){var a=u.window===u?u.document:u.nodeType===9?u:u.ownerDocument;cc||sa==null||sa!==ec(a)||(a=sa,"selectionStart"in a&&nc(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),oe&&he(oe,a)||(oe=a,a=zf(fc,"onSelect"),0>=f,e-=f,Et=1<<32-xl(t)+e|u<_?(dl=A,A=null):dl=A.sibling;var Q=m(y,A,h[_],s);if(Q===null){A===null&&(A=dl);break}l&&A&&Q.alternate===null&&t(y,A),v=n(Q,v,_),G===null?O=Q:G.sibling=Q,G=Q,A=dl}if(_===h.length)return u(y,A),R&&Vt(y,_),O;if(A===null){for(;__?(dl=A,A=null):dl=A.sibling;var Du=m(y,A,Q.value,s);if(Du===null){A===null&&(A=dl);break}l&&A&&Du.alternate===null&&t(y,A),v=n(Du,v,_),G===null?O=Du:G.sibling=Du,G=Du,A=dl}if(Q.done)return u(y,A),R&&Vt(y,_),O;if(A===null){for(;!Q.done;_++,Q=h.next())Q=z(y,Q.value,s),Q!==null&&(v=n(Q,v,_),G===null?O=Q:G.sibling=Q,G=Q);return R&&Vt(y,_),O}for(A=a(A);!Q.done;_++,Q=h.next())Q=o(A,y,_,Q.value,s),Q!==null&&(l&&(dl=Q.alternate,dl!==null&&A.delete(dl.key===null?_:dl.key)),v=n(Q,v,_),G===null?O=Q:G.sibling=Q,G=Q);return l&&A.forEach(function(wo){return t(y,wo)}),R&&Vt(y,_),O}function H(y,v,h,s){if(typeof h=="object"&&h!==null&&h.type===st&&h.key===null&&h.props.ref===void 0&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Uu:l:{for(var O=h.key;v!==null;){if(v.key===O){if(O=h.type,O===st){if(v.tag===7){u(y,v.sibling),s=e(v,h.props.children),cu(s,h),s.return=y,y=s;break l}}else if(v.elementType===O||typeof O=="object"&&O!==null&&O.$$typeof===F&&Ku(O)===v.type){u(y,v.sibling),s=e(v,h.props),cu(s,h),s.return=y,y=s;break l}u(y,v);break}else t(y,v);v=v.sibling}h.type===st?(s=Qu(h.props.children,y.mode,s,h.key),cu(s,h),s.return=y,y=s):(s=On(h.type,h.key,h.props,null,y.mode,s),cu(s,h),s.return=y,y=s)}return f(y);case $t:l:{for(O=h.key;v!==null;){if(v.key===O)if(v.tag===4&&v.stateNode.containerInfo===h.containerInfo&&v.stateNode.implementation===h.implementation){u(y,v.sibling),s=e(v,h.children||[]),s.return=y,y=s;break l}else{u(y,v);break}else t(y,v);v=v.sibling}s=oc(h,y.mode,s),s.return=y,y=s}return f(y);case F:return h=Ku(h),H(y,v,h,s)}if(Pa(h))return b(y,v,h,s);if(ka(h)){if(O=ka(h),typeof O!="function")throw Error(S(150));return h=O.call(h),N(y,v,h,s)}if(typeof h.then=="function")return H(y,v,Bn(h),s);if(h.$$typeof===ql)return H(y,v,Un(y,h),s);qn(y,h)}return typeof h=="string"&&h!==""||typeof h=="number"||typeof h=="bigint"?(h=""+h,v!==null&&v.tag===6?(u(y,v.sibling),s=e(v,h),s.return=y,y=s):(u(y,v),s=hc(h,y.mode,s),s.return=y,y=s),f(y)):u(y,v)}return function(y,v,h,s){try{be=0;var O=H(y,v,h,s);return Da=null,O}catch(A){if(A===_a||A===Cn)throw A;var G=Gl(29,A,null,y.mode);return G.lanes=s,G.return=y,G}}}var Ju=Qv(!0),Xv=Qv(!1),iu=!1;function Ac(l){l.updateQueue={baseState:l.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function _c(l,t){l=l.updateQueue,t.updateQueue===l&&(t.updateQueue={baseState:l.baseState,firstBaseUpdate:l.firstBaseUpdate,lastBaseUpdate:l.lastBaseUpdate,shared:l.shared,callbacks:null})}function vu(l){return{lane:l,tag:0,payload:null,callback:null,next:null}}function mu(l,t,u){var a=l.updateQueue;if(a===null)return null;if(a=a.shared,(r&2)!==0){var e=a.pending;return e===null?t.next=t:(t.next=e.next,e.next=t),a.pending=t,t=En(l),bv(l,null,u),t}return bn(l,a,t,u),En(l)}function Ee(l,t,u){if(t=t.updateQueue,t!==null&&(t=t.shared,(u&4194048)!==0)){var a=t.lanes;a&=l.pendingLanes,u|=a,t.lanes=u,A0(l,u)}}function Dc(l,t){var u=l.updateQueue,a=l.alternate;if(a!==null&&(a=a.updateQueue,u===a)){var e=null,n=null;if(u=u.firstBaseUpdate,u!==null){do{var f={lane:u.lane,tag:u.tag,payload:u.payload,callback:null,next:null};n===null?e=n=f:n=n.next=f,u=u.next}while(u!==null);n===null?e=n=t:n=n.next=t}else e=n=t;u={baseState:a.baseState,firstBaseUpdate:e,lastBaseUpdate:n,shared:a.shared,callbacks:a.callbacks},l.updateQueue=u;return}l=u.lastBaseUpdate,l===null?u.firstBaseUpdate=t:l.next=t,u.lastBaseUpdate=t}var Mc=!1;function Oe(){if(Mc){var l=Aa;if(l!==null)throw l}}function Ne(l,t,u,a){Mc=!1;var e=l.updateQueue;iu=!1;var n=e.firstBaseUpdate,f=e.lastBaseUpdate,c=e.shared.pending;if(c!==null){e.shared.pending=null;var i=c,d=i.next;i.next=null,f===null?n=d:f.next=d,f=i;var g=l.alternate;g!==null&&(g=g.updateQueue,c=g.lastBaseUpdate,c!==f&&(c===null?g.firstBaseUpdate=d:c.next=d,g.lastBaseUpdate=i))}if(n!==null){var z=e.baseState;f=0,g=d=i=null,c=n;do{var m=c.lane&-536870913,o=m!==c.lane;if(o?(Y&m)===m:(a&m)===m){m!==0&&m===ru&&(Mc=!0),g!==null&&(g=g.next={lane:0,tag:c.tag,payload:c.payload,callback:null,next:null});l:{var b=l,N=c;m=t;var H=u;switch(N.tag){case 1:if(b=N.payload,typeof b=="function"){z=b.call(H,z,m);break l}z=b;break l;case 3:b.flags=b.flags&-65537|128;case 0:if(b=N.payload,m=typeof b=="function"?b.call(H,z,m):b,m==null)break l;z=X({},z,m);break l;case 2:iu=!0}}m=c.callback,m!==null&&(l.flags|=64,o&&(l.flags|=8192),o=e.callbacks,o===null?e.callbacks=[m]:o.push(m))}else o={lane:m,tag:c.tag,payload:c.payload,callback:c.callback,next:null},g===null?(d=g=o,i=z):g=g.next=o,f|=m;if(c=c.next,c===null){if(c=e.shared.pending,c===null)break;o=c,c=o.next,o.next=null,e.lastBaseUpdate=o,e.shared.pending=null}}while(!0);g===null&&(i=z),e.baseState=i,e.firstBaseUpdate=d,e.lastBaseUpdate=g,n===null&&(e.shared.lanes=0),su|=f,l.lanes=f,l.memoizedState=z}}function Zv(l,t){if(typeof l!="function")throw Error(S(191,l));l.call(t)}function Vv(l,t){var u=l.callbacks;if(u!==null)for(l.callbacks=null,l=0;ln?n:8;var f=D.T,c={};c.types=f!==null?f.types:null,D.T=c,wc(l,!1,t,u);try{var i=e(),d=D.S;if(d!==null&&d(c,i),i!==null&&typeof i=="object"&&typeof i.then=="function"){var g=dh(i,a);Me(l,t,g,Fl(l))}else Me(l,t,a,Fl(l))}catch(z){Me(l,t,{then:function(){},status:"rejected",reason:z},Fl())}finally{j.p=n,f!==null&&c.types!==null&&(f.types=c.types),D.T=f}}function zh(){}function Jc(l,t,u,a){if(l.tag!==5)throw Error(S(476));var e=sm(l).queue;Sm(l,e,t,Hu,u===null?zh:function(){return zm(l),u(a)})}function sm(l){var t=l.memoizedState;if(t!==null)return t;t={memoizedState:Hu,baseState:Hu,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Kt,lastRenderedState:Hu},next:null};var u={};return t.next={memoizedState:u,baseState:u,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Kt,lastRenderedState:u},next:null},l.memoizedState=t,l=l.alternate,l!==null&&(l.memoizedState=t),t}function zm(l){var t=sm(l);t.next===null&&(t=l.alternate.memoizedState),Me(l,t.next.queue,{},Fl())}function pc(){return bl(Wa)}function Tm(){return cl().memoizedState}function bm(){return cl().memoizedState}function Th(l){for(var t=l.return;t!==null;){switch(t.tag){case 24:case 3:var u=Fl();l=vu(u);var a=mu(t,l,u);a!==null&&(Vl(a,t,u),Ee(a,t,u)),t={cache:bc()},l.payload=t;return}t=t.return}}function bh(l,t,u){var a=Fl();u={lane:a,revertLane:0,gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null},Kn(l)?Om(t,u):(u=yc(l,t,u,a),u!==null&&(Vl(u,l,a),Nm(u,t,a)))}function Em(l,t,u){var a=Fl();Me(l,t,u,a)}function Me(l,t,u,a){var e={lane:a,revertLane:0,gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null};if(Kn(l))Om(t,e);else{var n=l.alternate;if(l.lanes===0&&(n===null||n.lanes===0)&&(n=t.lastRenderedReducer,n!==null))try{var f=t.lastRenderedState,c=n(f,u);if(e.hasEagerState=!0,e.eagerState=c,Ll(c,f))return bn(l,t,e,0),$===null&&Tn(),!1}catch{}if(u=yc(l,t,e,a),u!==null)return Vl(u,l,a),Nm(u,t,a),!0}return!1}function wc(l,t,u,a){if(a={lane:2,revertLane:Xi(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},Kn(l)){if(t)throw Error(S(479))}else t=yc(l,u,a,2),t!==null&&Vl(t,l,2)}function Kn(l){var t=l.alternate;return l===U||t!==null&&t===U}function Om(l,t){Ma=Qn=!0;var u=l.pending;u===null?t.next=t:(t.next=u.next,u.next=t),l.pending=t}function Nm(l,t,u){if((u&4194048)!==0){var a=t.lanes;a&=l.pendingLanes,u|=a,t.lanes=u,A0(l,u)}}var Ln={readContext:bl,use:Vn,useCallback:nl,useContext:nl,useEffect:nl,useImperativeHandle:nl,useLayoutEffect:nl,useInsertionEffect:nl,useMemo:nl,useReducer:nl,useRef:nl,useState:nl,useDebugValue:nl,useDeferredValue:nl,useTransition:nl,useSyncExternalStore:nl,useId:nl,useHostTransitionStatus:nl,useFormState:nl,useActionState:nl,useOptimistic:nl,useMemoCache:nl,useCacheRefresh:nl,useEffectEvent:nl},Am={readContext:bl,use:Vn,useCallback:function(l,t){return Hl().memoizedState=[l,t===void 0?null:t],l},useContext:bl,useEffect:cm,useImperativeHandle:function(l,t,u){u=u!=null?u.concat([l]):null,rn(4194308,4,ym.bind(null,t,l),u)},useLayoutEffect:function(l,t){return rn(4194308,4,l,t)},useInsertionEffect:function(l,t){rn(4,2,l,t)},useMemo:function(l,t){var u=Hl();t=t===void 0?null:t;var a=l();if(pu){lu(!0);try{l()}finally{lu(!1)}}return u.memoizedState=[a,t],a},useReducer:function(l,t,u){var a=Hl();if(u!==void 0){var e=u(t);if(pu){lu(!0);try{u(t)}finally{lu(!1)}}}else e=t;return a.memoizedState=a.baseState=e,l={pending:null,lanes:0,dispatch:null,lastRenderedReducer:l,lastRenderedState:e},a.queue=l,l=l.dispatch=bh.bind(null,U,l),[a.memoizedState,l]},useRef:function(l){var t=Hl();return l={current:l},t.memoizedState=l},useState:function(l){l=jc(l);var t=l.queue,u=Em.bind(null,U,t);return t.dispatch=u,[l.memoizedState,u]},useDebugValue:Kc,useDeferredValue:function(l,t){var u=Hl();return Lc(u,l,t)},useTransition:function(){var l=jc(!1);return l=Sm.bind(null,U,l.queue,!0,!1),Hl().memoizedState=l,[!1,l]},useSyncExternalStore:function(l,t,u){var a=U,e=Hl();if(R){if(u===void 0)throw Error(S(407));u=u()}else{if(u=t(),$===null)throw Error(S(349));(Y&127)!==0||Jv(a,t,u)}e.memoizedState=u;var n={value:u,getSnapshot:t};return e.queue=n,cm(wv.bind(null,a,n,l),[l]),a.flags|=2048,Ha(9,{destroy:void 0},pv.bind(null,a,n,u,t),null),u},useId:function(){var l=Hl(),t=$.identifierPrefix;if(R){var u=Ot,a=Et;u=(a&~(1<<32-xl(a)-1)).toString(32)+u,t="_"+t+"R_"+u,u=Xn++,0<\/script>",n=n.removeChild(n.firstChild);break;case"select":n=typeof a.is=="string"?f.createElement("select",{is:a.is}):f.createElement("select"),a.multiple?n.multiple=!0:a.size&&(n.size=a.size);break;default:n=typeof a.is=="string"?f.createElement(e,{is:a.is}):f.createElement(e)}}n[Tl]=t,n[Yl]=a;l:for(f=t.child;f!==null;){if(f.tag===5||f.tag===6)n.appendChild(f.stateNode);else if(f.tag!==4&&f.tag!==27&&f.child!==null){f.child.return=f,f=f.child;continue}if(f===t)break l;for(;f.sibling===null;){if(f.return===null||f.return===t)break l;f=f.return}f.sibling.return=f.return,f=f.sibling}t.stateNode=n;l:switch(Al(n,e,a),e){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break l;case"img":a=!0;break l;default:a=!1}a&&Jt(t)}}return P(t),t.subtreeFlags&=-33554433,fi(t,t.type,l===null?null:l.memoizedProps,t.pendingProps,u),null;case 6:if(l&&t.stateNode!=null)l.memoizedProps!==a&&Jt(t);else{if(typeof a!="string"&&t.stateNode===null)throw Error(S(166));if(l=kt.current,Na(t)){if(l=t.stateNode,u=t.memoizedProps,a=null,e=gl,e!==null)switch(e.tag){case 27:case 5:a=e.memoizedProps}l[Tl]=t,l=!!(l.nodeValue===u||a!==null&&a.suppressHydrationWarning===!0||w1(l.nodeValue,u)),l||nu(t,!0)}else l=je(l).createTextNode(a),l[Tl]=t,t.stateNode=l}return P(t),null;case 31:if(u=t.memoizedState,l===null||l.memoizedState!==null){if(a=Na(t),u!==null){if(l===null){if(!a)throw Error(S(318));if(l=t.memoizedState,l=l!==null?l.dehydrated:null,!l)throw Error(S(557));l[Tl]=t}else Xu(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;P(t),l=!1}else u=sc(),l!==null&&l.memoizedState!==null&&(l.memoizedState.hydrationErrors=u),l=!0;if(!l)return t.flags&256?(Jl(t),t):(Jl(t),null);if((t.flags&128)!==0)throw Error(S(558))}return P(t),null;case 13:if(a=t.memoizedState,l===null||l.memoizedState!==null&&l.memoizedState.dehydrated!==null){if(e=Na(t),a!==null&&a.dehydrated!==null){if(l===null){if(!e)throw Error(S(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(S(317));e[Tl]=t}else Xu(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;P(t),e=!1}else e=sc(),l!==null&&l.memoizedState!==null&&(l.memoizedState.hydrationErrors=e),e=!0;if(!e)return t.flags&256?(Jl(t),t):(Jl(t),null)}return Jl(t),(t.flags&128)!==0?(t.lanes=u,t):(u=a!==null,l=l!==null&&l.memoizedState!==null,u&&(a=t.child,e=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(e=a.alternate.memoizedState.cachePool.pool),n=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(n=a.memoizedState.cachePool.pool),n!==e&&(a.flags|=2048)),u!==l&&u&&(t.child.flags|=8192),$n(t,t.updateQueue),P(t),null);case 4:return ca(),l===null&&ri(t.stateNode.containerInfo),t.flags|=67108864,P(t),null;case 10:return rt(t.type),P(t),null;case 19:if(Rc(t),a=t.memoizedState,a===null)return P(t),null;if(e=(t.flags&128)!==0,n=a.rendering,n===null)if(e)He(a,!1);else{if(fl!==0||l!==null&&(l.flags&128)!==0)for(l=t.child;l!==null;){if(n=Gn(l),n!==null){for(t.flags|=128,He(a,!1),l=n.updateQueue,t.updateQueue=l,$n(t,l),t.subtreeFlags=0,l=u,u=t.child;u!==null;)Ev(u,l),u=u.sibling;return Ae(t,Ol.current&1|2),R&&Vt(t,a.treeForkCount),t.child}l=l.sibling}a.tail!==null&&jl()>vf&&(t.flags|=128,e=!0,He(a,!1),t.lanes=4194304)}else{if(!e)if(l=Gn(n),l!==null){if(t.flags|=128,e=!0,l=l.updateQueue,t.updateQueue=l,$n(t,l),He(a,!0),a.tail===null&&a.tailMode!=="collapsed"&&a.tailMode!=="visible"&&!n.alternate&&!R)return P(t),null}else 2*jl()-a.renderingStartTime>vf&&u!==536870912&&(t.flags|=128,e=!0,He(a,!1),t.lanes=4194304);a.isBackwards?(n.sibling=t.child,t.child=n):(l=a.last,l!==null?l.sibling=n:t.child=n,a.last=n)}if(a.tail!==null){l=a.tail;l:{for(u=l;u!==null;){if(u.alternate!==null){u=!1;break l}u=u.sibling}u=!0}return a.rendering=l,a.tail=l.sibling,a.renderingStartTime=jl(),l.sibling=null,n=Ol.current,n=e?n&1|2:n&1,a.tailMode==="visible"||a.tailMode==="collapsed"||!u||R?Ae(t,n):(u=n,I(El,t),I(Ol,u),Dl===null&&(Dl=t)),R&&Vt(t,a.treeForkCount),l}return P(t),null;case 22:case 23:return Jl(t),Hc(),a=t.memoizedState!==null,l!==null?l.memoizedState!==null!==a&&(t.flags|=8192):a&&(t.flags|=8192),a?(u&536870912)!==0&&(t.flags&128)===0&&(P(t),t.subtreeFlags&6&&(t.flags|=8192)):P(t),u=t.updateQueue,u!==null&&$n(t,u.retryQueue),u=null,l!==null&&l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(u=l.memoizedState.cachePool.pool),a=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(a=t.memoizedState.cachePool.pool),a!==u&&(t.flags|=2048),l!==null&&zl(xu),null;case 24:return u=null,l!==null&&(u=l.memoizedState.cache),t.memoizedState.cache!==u&&(t.flags|=2048),rt(vl),P(t),null;case 25:return null;case 30:return t.flags|=33554432,P(t),null}throw Error(S(156,t.tag))}function Mh(l,t){switch(gc(t),t.tag){case 1:return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 3:return rt(vl),ca(),l=t.flags,(l&65536)!==0&&(l&128)===0?(t.flags=l&-65537|128,t):null;case 26:case 27:case 5:return ln(t),null;case 31:if(t.memoizedState!==null){if(Jl(t),t.alternate===null)throw Error(S(340));Xu()}return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 13:if(Jl(t),l=t.memoizedState,l!==null&&l.dehydrated!==null){if(t.alternate===null)throw Error(S(340));Xu()}return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 19:return Rc(t),l=t.flags,l&65536?(t.flags=l&-65537|128,l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null),t.flags|=4,t):null;case 4:return ca(),null;case 10:return rt(t.type),null;case 22:case 23:return Jl(t),Hc(),l!==null&&zl(xu),l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 24:return rt(vl),null;case 25:return null;default:return null}}function Fm(l,t){switch(gc(t),t.tag){case 3:rt(vl),ca();break;case 26:case 27:case 5:ln(t);break;case 4:ca();break;case 31:t.memoizedState!==null&&Jl(t);break;case 13:Jl(t);break;case 19:Rc(t);break;case 10:rt(t.type);break;case 22:case 23:Jl(t),Hc(),l!==null&&zl(xu);break;case 24:rt(vl)}}function Ce(l,t){try{var u=t.updateQueue,a=u!==null?u.lastEffect:null;if(a!==null){var e=a.next;u=e;do{if((u.tag&l)===l){a=void 0;var n=u.create,f=u.inst;a=n(),f.destroy=a}u=u.next}while(u!==e)}}catch(c){L(t,t.return,c)}}function ou(l,t,u){try{var a=t.updateQueue,e=a!==null?a.lastEffect:null;if(e!==null){var n=e.next;a=n;do{if((a.tag&l)===l){var f=a.inst,c=f.destroy;if(c!==void 0){f.destroy=void 0,e=t;var i=u,d=c;try{d()}catch(g){L(e,i,g)}}}a=a.next}while(a!==n)}}catch(g){L(t,t.return,g)}}function $m(l){var t=l.updateQueue;if(t!==null){var u=l.stateNode;try{Vv(t,u)}catch(a){L(l,l.return,a)}}}function Im(l,t,u){u.props=wu(l.type,l.memoizedProps),u.state=l.memoizedState;try{u.componentWillUnmount()}catch(a){L(l,t,a)}}function Nt(l,t){try{var u=l.ref;if(u!==null){switch(l.tag){case 26:case 27:case 5:var a=l.stateNode;break;case 30:var e=l.stateNode,n=Qt(l.memoizedProps,e);(e.ref===null||e.ref.name!==n)&&(e.ref=ey(n)),a=e.ref;break;case 7:if(l.stateNode===null){var f=new $l(l);V(l.child,!1,zo,f,void 0,void 0),l.stateNode=f}a=l.stateNode;break;default:a=l.stateNode}typeof u=="function"?l.refCleanup=u(a):u.current=a}}catch(c){L(l,t,c)}}function Nl(l,t){var u=l.ref,a=l.refCleanup;if(u!==null)if(typeof a=="function")try{a()}catch(e){L(l,t,e)}finally{l.refCleanup=null,l=l.alternate,l!=null&&(l.refCleanup=null)}else if(typeof u=="function")try{u(null)}catch(e){L(l,t,e)}else u.current=null}function In(l,t){if((l.tag===5||l.tag===27||l.tag===6)&&l.alternate===null&&t!==null)for(var u=0;u title"))),Al(a,t,u),a[Tl]=l,ol(a),t=a;break l;case"link":if(n=Ey("link","href",e).get(t+(u.href||""))){for(f=0;fH&&(f=H,H=N,N=f);var y=vv(c,N),v=vv(c,H);if(y&&v&&(o.rangeCount!==1||o.anchorNode!==y.node||o.anchorOffset!==y.offset||o.focusNode!==v.node||o.focusOffset!==v.offset)){var h=z.createRange();h.setStart(y.node,y.offset),o.removeAllRanges(),N>H?(o.addRange(h),o.extend(v.node,v.offset)):(h.setEnd(v.node,v.offset),o.addRange(h))}}}}for(z=[],o=c;o=o.parentNode;)o.nodeType===1&&z.push({element:o,left:o.scrollLeft,top:o.scrollTop});for(typeof c.focus=="function"&&c.focus(),c=0;cu?32:u,D.T=null,u=Mi,Mi=null;var n=ht,f=Ht;if(el=0,Iu=ht=null,Ht=0,(r&6)!==0)throw Error(S(331));var c=r;if(r|=4,b1(n.current),s1(n,n.current,f,u),r=c,Xe(0,!1),rl&&typeof rl.onPostCommitFiberRoot=="function")try{rl.onPostCommitFiberRoot(te,n)}catch{}return!0}finally{j.p=e,D.T=a,G1(l,t)}}function Q1(l,t,u){t=lt(u,t),t=$c(l.stateNode,t,2),l=mu(l,t,2),l!==null&&(ae(l,2),Ct(l))}function L(l,t,u){if(l.tag===3)Q1(l,l,u);else for(;t!==null;){if(t.tag===3){Q1(t,l,u);break}else if(t.tag===1){var a=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(zu===null||!zu.has(a))){l=lt(u,l),u=Bm(2),a=mu(t,u,2),a!==null&&(qm(u,a,t,l),ae(a,2),Ct(a));break}}t=t.return}}function Yi(l,t,u){var a=l.pingCache;if(a===null){a=l.pingCache=new Rh;var e=new Set;a.set(t,e)}else e=a.get(t),e===void 0&&(e=new Set,a.set(t,e));e.has(u)||(Ai=!0,e.add(u),l=Vh.bind(null,l,t,u),t.then(l,l))}function Vh(l,t,u){var a=l.pingCache;a!==null&&a.delete(t),l.pingedLanes|=l.suspendedLanes&u,l.warmLanes&=~u,$===l&&(Y&u)===u&&((fl===4||fl===3&&(Y&62914560)===Y&&300>jl()-cf)&&(r&2)===0?ja(l,0):ff|=u,Ga===Y&&(Ga=0)),Ct(l)}function X1(l,t){t===0&&(t=O0()),l=Gu(l,t),l!==null&&(ae(l,t),Ct(l))}function jh(l){var t=l.memoizedState,u=0;t!==null&&(u=t.retryLane),X1(l,u)}function rh(l,t){var u=0;switch(l.tag){case 31:case 13:var a=l.stateNode,e=l.memoizedState;e!==null&&(u=e.retryLane);break;case 19:a=l.stateNode;break;case 22:a=l.stateNode._retryCache;break;default:throw Error(S(314))}a!==null&&a.delete(t),X1(l,u)}function xh(l,t){return Xf(l,t)}var xa=null,Ka=null,Gi=!1,Sf=!1,Qi=!1,bu=0;function Ct(l){l!==Ka&&l.next===null&&(Ka===null?xa=Ka=l:Ka=Ka.next=l),Sf=!0,Gi||(Gi=!0,Lh())}function Xe(l,t){if(!Qi&&Sf){Qi=!0;do for(var u=!1,a=xa;a!==null;){if(l!==0){var e=a.pendingLanes;if(e===0)var n=0;else{var f=a.suspendedLanes,c=a.pingedLanes;n=(1<<31-xl(42|l)+1)-1,n&=e&~(f&~c),n=n&201326741?n&201326741|1:n?n|2:0}n!==0&&(u=!0,r1(a,n))}else n=Y,n=nn(a,a===$?n:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(n&3)===0||ue(a,n)||(u=!0,r1(a,n));a=a.next}while(u);Qi=!1}}function Kh(){Z1()}function Z1(){Sf=Gi=!1;var l=0;bu!==0&&to()&&(l=bu);for(var t=jl(),u=null,a=xa;a!==null;){var e=a.next,n=V1(a,t);n===0?(a.next=null,u===null?xa=e:u.next=e,e===null&&(Ka=u)):(u=a,(l!==0||(n&3)!==0)&&(Sf=!0)),a=e}el!==0&&el!==5||Xe(l),bu!==0&&(bu=0)}function V1(l,t){for(var u=l.suspendedLanes,a=l.pingedLanes,e=l.expirationTimes,n=l.pendingLanes&-62914561;0c)break;var g=i.transferSize,z=i.initiatorType;g&&W1(z)&&(i=i.responseEnd,f+=g*(iEf){N.length=b;break}h=new Promise(co.bind(h)),N.push(h)}}}if(0"u"?null:document;function sy(l,t,u){var a=Ja;if(a&&typeof t=="string"&&t){var e=kl(t);e='link[rel="'+l+'"][href="'+e+'"]',typeof u=="string"&&(e+='[crossorigin="'+u+'"]'),Sy.has(e)||(Sy.add(e),l={rel:l,crossOrigin:u,href:t},a.querySelector(e)===null&&(t=a.createElement("link"),Al(t,"link",l),ol(t),a.head.appendChild(t)))}}function Mo(l){wt.D(l),sy("dns-prefetch",l,null)}function Uo(l,t){wt.C(l,t),sy("preconnect",l,t)}function Ho(l,t,u){wt.L(l,t,u);var a=Ja;if(a&&l&&t){var e='link[rel="preload"][as="'+kl(t)+'"]';t==="image"&&u&&u.imageSrcSet?(e+='[imagesrcset="'+kl(u.imageSrcSet)+'"]',typeof u.imageSizes=="string"&&(e+='[imagesizes="'+kl(u.imageSizes)+'"]')):e+='[href="'+kl(l)+'"]';var n=e;switch(t){case"style":n=pa(l);break;case"script":n=wa(l)}if(!(ft.has(n)||(l=X({rel:"preload",href:t==="image"&&u&&u.imageSrcSet?void 0:l,as:t},u),ft.set(n,l),a.querySelector(e)!==null||t==="style"&&a.querySelector(xe(n))||t==="script"&&a.querySelector(Ke(n))))){var f=a.createElement("link");Al(f,"link",l),t==="style"&&(f[fn]=!0,f.onload=f.onerror=function(){C0(f)}),ol(f),a.head.appendChild(f)}}}function Co(l,t){wt.m(l,t);var u=Ja;if(u&&l){var a=t&&typeof t.as=="string"?t.as:"script",e='link[rel="modulepreload"][as="'+kl(a)+'"][href="'+kl(l)+'"]',n=e;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":n=wa(l)}if(!ft.has(n)&&(l=X({rel:"modulepreload",href:l},t),ft.set(n,l),u.querySelector(e)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(u.querySelector(Ke(n)))return}a=u.createElement("link"),Al(a,"link",l),ol(a),u.head.appendChild(a)}}}function Ro(l,t,u){wt.S(l,t,u);var a=Ja;if(a&&l){var e=ma(a).hoistableStyles,n=pa(l);t=t||"default";var f=e.get(n);if(!f){var c={loading:0,preload:null};if(f=a.querySelector(xe(n)))c.loading=5;else{l=X({rel:"stylesheet",href:l,"data-precedence":t},u),(u=ft.get(n))&&a0(l,u);var i=f=a.createElement("link");ol(i),Al(i,"link",l),i._p=new Promise(function(d,g){i.onload=d,i.onerror=g}),i.addEventListener("load",function(){c.loading|=1}),i.addEventListener("error",function(){c.loading|=2}),c.loading|=4,Tf(f,t,a)}f={type:"stylesheet",instance:f,count:1,state:c},e.set(n,f)}}}function Bo(l,t){wt.X(l,t);var u=Ja;if(u&&l){var a=ma(u).hoistableScripts,e=wa(l),n=a.get(e);n||(n=u.querySelector(Ke(e)),n||(l=X({src:l,async:!0},t),(t=ft.get(e))&&e0(l,t),n=u.createElement("script"),ol(n),Al(n,"link",l),u.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(e,n))}}function qo(l,t){wt.M(l,t);var u=Ja;if(u&&l){var a=ma(u).hoistableScripts,e=wa(l),n=a.get(e);n||(n=u.querySelector(Ke(e)),n||(l=X({src:l,async:!0,type:"module"},t),(t=ft.get(e))&&e0(l,t),n=u.createElement("script"),ol(n),Al(n,"link",l),u.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(e,n))}}function zy(l,t,u,a){var e=(e=kt.current)?re(e):null;if(!e)throw Error(S(446));switch(l){case"meta":case"title":return null;case"style":return typeof u.precedence=="string"&&typeof u.href=="string"?(u=pa(u.href),t=ma(e).hoistableStyles,a=t.get(u),a||(a={type:"style",instance:null,count:0,state:null},t.set(u,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(u.rel==="stylesheet"&&typeof u.href=="string"&&typeof u.precedence=="string"){l=pa(u.href);var n=ma(e).hoistableStyles,f=n.get(l);if(f||(e=e.ownerDocument||e,f={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},n.set(l,f),(n=e.querySelector(xe(l)))?n._p||(f.instance=n,f.state.loading=5):(n=ft.get(l),n||(n={rel:"preload",as:"style",href:u.href,crossOrigin:u.crossOrigin,integrity:u.integrity,media:u.media,hrefLang:u.hrefLang,referrerPolicy:u.referrerPolicy},ft.set(l,n)),Yo(e,l,n,f.state))),t&&a===null)throw Error(S(528,""));return f}if(t&&a!==null)throw Error(S(529,""));return null;case"script":return t=u.async,u=u.src,typeof u=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(u=wa(u),t=ma(e).hoistableScripts,a=t.get(u),a||(a={type:"script",instance:null,count:0,state:null},t.set(u,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(S(444,l))}}function pa(l){return'href="'+kl(l)+'"'}function xe(l){return'link[rel="stylesheet"]['+l+"]"}function Ty(l){return X({},l,{"data-precedence":l.precedence,precedence:null})}function Yo(l,t,u,a){if(t=l.querySelector('link[rel="preload"][as="style"]['+t+"]")){if(t[fn]!==!0){a.loading=1;return}}else t=l.createElement("link"),t[fn]=!0,t.onload=t.onerror=C0.bind(null,t),Al(t,"link",u),ol(t),l.head.appendChild(t);a.preload=t,t.addEventListener("load",function(){return a.loading|=1}),t.addEventListener("error",function(){return a.loading|=2})}function wa(l){return'[src="'+kl(l)+'"]'}function Ke(l){return"script[async]"+l}function by(l,t,u){if(t.count++,t.instance===null)switch(t.type){case"style":var a=l.querySelector('style[data-href~="'+kl(u.href)+'"]');if(a)return t.instance=a,ol(a),a;var e=X({},u,{"data-href":u.href,"data-precedence":u.precedence,href:null,precedence:null});return a=(l.ownerDocument||l).createElement("style"),ol(a),Al(a,"style",e),Tf(a,u.precedence,l),t.instance=a;case"stylesheet":e=pa(u.href);var n=l.querySelector(xe(e));if(n)return t.state.loading|=4,t.instance=n,ol(n),n;a=Ty(u),(e=ft.get(e))&&a0(a,e),n=(l.ownerDocument||l).createElement("link"),ol(n);var f=n;return f._p=new Promise(function(c,i){f.onload=c,f.onerror=i}),Al(n,"link",a),t.state.loading|=4,Tf(n,u.precedence,l),t.instance=n;case"script":return n=wa(u.src),(e=l.querySelector(Ke(n)))?(t.instance=e,ol(e),e):(a=u,(e=ft.get(n))&&(a=X({},u),e0(a,e)),l=l.ownerDocument||l,e=l.createElement("script"),ol(e),Al(e,"link",a),l.head.appendChild(e),t.instance=e);case"void":return null;default:throw Error(S(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(a=t.instance,t.state.loading|=4,Tf(a,u.precedence,l));return t.instance}function Tf(l,t,u){for(var a=u.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),e=a.length?a[a.length-1]:null,n=e,f=0;f title"):null)}function Go(l,t,u){if(u===1||t.itemProp!=null)return!1;switch(l){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;return t.rel==="stylesheet"?(l=t.disabled,typeof t.precedence=="string"&&l==null):!0;case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function Oy(l,t){return l==="img"&&t.src!=null&&t.src!==""&&t.onLoad==null&&t.loading!=="lazy"}function Ny(l){return!(l.type==="stylesheet"&&(l.state.loading&3)===0)}function Ay(l){return(l.width||100)*(l.height||100)*(typeof devicePixelRatio=="number"?devicePixelRatio:1)*.25}function _y(l,t){typeof t.decode=="function"&&(l.imgCount++,t.complete||(l.imgBytes+=Ay(t),l.suspenseyImages.push(t)),l=Zo.bind(l),t.decode().then(l,l))}function Qo(l,t,u,a){if(u.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(u.state.loading&4)===0){if(u.instance===null){var e=pa(a.href),n=t.querySelector(xe(e));if(n){t=n._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(l.count++,l=Le.bind(l),t.then(l,l)),u.state.loading|=4,u.instance=n,ol(n);return}n=t.ownerDocument||t,a=Ty(a),(e=ft.get(e))&&a0(a,e),n=n.createElement("link"),ol(n);var f=n;f._p=new Promise(function(c,i){f.onload=c,f.onerror=i}),Al(n,"link",a),u.instance=n}l.stylesheets===null&&(l.stylesheets=new Map),l.stylesheets.set(u,t),(t=u.state.preload)&&(u.state.loading&3)===0&&(l.count++,u=Le.bind(l),t.addEventListener("load",u),t.addEventListener("error",u))}}var Ef=0;function Xo(l,t){return l.stylesheets&&l.count===0&&Nf(l,l.stylesheets),0Ef?50:800)+t);return l.unsuspend=u,function(){l.unsuspend=null,clearTimeout(a),clearTimeout(e)}}:null}function Dy(l){if(l.count===0&&(l.imgCount===0||!l.waitingForImages)){if(l.stylesheets)Nf(l,l.stylesheets);else if(l.unsuspend){var t=l.unsuspend;l.unsuspend=null,t()}}}function Le(){this.count--,Dy(this)}function Zo(){this.imgCount--,Dy(this)}var Of=null;function Nf(l,t){l.stylesheets=null,l.unsuspend!==null&&(l.count++,Of=new Map,t.forEach(Vo,l),Of=null,Le.call(l))}function Vo(l,t){if(!(t.state.loading&4)){var u=Of.get(l);if(u)var a=u.get(null);else{u=new Map,Of.set(l,u);for(var e=l.querySelectorAll("link[data-precedence],style[data-precedence]"),n=0;n"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(T)}catch(tl){console.error(tl)}}return T(),d0.exports=ko(),d0.exports}var Ly=Po();const lg=({value:T,name:tl,hydrate:p=!0})=>{if(T==null||T.trim()==="")return null;const S=p?"astro-slot":"astro-static-slot";return Wt.createElement(S,{name:tl,suppressHydrationWarning:!0,dangerouslySetInnerHTML:{__html:T}})};var Wy=Wt.memo(lg,()=>!0);function tg(T){for(const tl in T)if(tl.startsWith("__reactContainer"))return tl}const ug={class:"className",for:"htmlFor"};let Jy=0;function Fy(T,tl,p){tl===void 0&&(Jy+=1,tl=Jy,p=0);let S={};for(const il of T.attributes){const gt=ug[il.name]||il.name;S[gt]=il.value}if(S.key=`${tl}-${p}`,T.firstChild===null)return Wt.createElement(T.localName,S);let Bl=0;return Wt.createElement(T.localName,S,Array.from(T.childNodes).map(il=>il.nodeType===Node.TEXT_NODE?il.data:il.nodeType===Node.ELEMENT_NODE?(Bl+=1,Fy(il,tl,Bl)):void 0).filter(il=>!!il))}function ag(T,tl){if(tl&&T){let p=[],S=document.createElement("template");S.innerHTML=T;for(let Bl of S.content.children)p.push(Fy(Bl));return p}else return T?Wt.createElement(Wy,{value:T}):void 0}let py=new WeakMap;const wy=(T,tl)=>{let p=py.get(T);return p||(p=tl(),py.set(T,p)),p};var ng=T=>(tl,p,{default:S,...Bl},{client:il})=>{if(!T.hasAttribute("ssr"))return;const gt=T.getAttribute("data-action-key"),Mu=T.getAttribute("data-action-name"),Ml=T.getAttribute("data-action-result"),Il=gt&&Mu&&Ml?[JSON.parse(Ml),gt,Mu]:void 0,Pu={identifierPrefix:T.getAttribute("prefix"),formState:Il};for(const[ct,_l]of Object.entries(Bl))p[ct]=Wt.createElement(Wy,{value:_l,name:ct});const V=Wt.createElement(tl,p,ag(S,T.hasAttribute("data-react-children"))),W=tg(T);if(W&&delete T[W],il==="only")return Wt.startTransition(()=>{wy(T,()=>{const _l=Ly.createRoot(T);return T.addEventListener("astro:unmount",()=>_l.unmount(),{once:!0}),_l}).render(V)});Wt.startTransition(()=>{wy(T,()=>{const _l=Ly.hydrateRoot(T,V,Pu);return T.addEventListener("astro:unmount",()=>_l.unmount(),{once:!0}),_l}).render(V)})};export{ng as default}; diff --git a/services/recordly-share/worker/web/dist/_astro/index.DeQQz02V.js b/services/recordly-share/worker/web/dist/_astro/index.DeQQz02V.js new file mode 100644 index 000000000..cd9c2f70b --- /dev/null +++ b/services/recordly-share/worker/web/dist/_astro/index.DeQQz02V.js @@ -0,0 +1 @@ +function ie(_,m){for(var d=0;dy[T]})}}}return Object.freeze(Object.defineProperty(_,Symbol.toStringTag,{value:"Module"}))}function se(_){return _&&_.__esModule&&Object.prototype.hasOwnProperty.call(_,"default")?_.default:_}var Y={exports:{}},o={};var V;function fe(){if(V)return o;V=1;var _=Symbol.for("react.transitional.element"),m=Symbol.for("react.portal"),d=Symbol.for("react.fragment"),y=Symbol.for("react.strict_mode"),T=Symbol.for("react.profiler"),R=Symbol.for("react.consumer"),h=Symbol.for("react.context"),H=Symbol.for("react.forward_ref"),O=Symbol.for("react.suspense"),S=Symbol.for("react.memo"),i=Symbol.for("react.lazy"),t=Symbol.for("react.activity"),l=Symbol.for("react.view_transition"),g=Symbol.iterator;function w(e){return e===null||typeof e!="object"?null:(e=g&&e[g]||e["@@iterator"],typeof e=="function"?e:null)}var P={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},k=Object.assign,b={};function C(e,r,n){this.props=e,this.context=r,this.refs=b,this.updater=n||P}C.prototype.isReactComponent={},C.prototype.setState=function(e,r){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,r,"setState")},C.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function x(){}x.prototype=C.prototype;function D(e,r,n){this.props=e,this.context=r,this.refs=b,this.updater=n||P}var j=D.prototype=new x;j.constructor=D,k(j,C.prototype),j.isPureReactComponent=!0;var q=Array.isArray;function I(){}var c={H:null,A:null,T:null,S:null},z=Object.prototype.hasOwnProperty;function L(e,r,n){var u=n.ref;return{$$typeof:_,type:e,key:r,ref:u!==void 0?u:null,props:n}}function ee(e,r){return L(e.type,r,e.props)}function M(e){return typeof e=="object"&&e!==null&&e.$$typeof===_}function te(e){var r={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(n){return r[n]})}var G=/\/+/g;function $(e,r){return typeof e=="object"&&e!==null&&e.key!=null?te(""+e.key):r.toString(36)}function re(e){switch(e.status){case"fulfilled":return e.value;case"rejected":throw e.reason;default:switch(typeof e.status=="string"?e.then(I,I):(e.status="pending",e.then(function(r){e.status==="pending"&&(e.status="fulfilled",e.value=r)},function(r){e.status==="pending"&&(e.status="rejected",e.reason=r)})),e.status){case"fulfilled":return e.value;case"rejected":throw e.reason}}throw e}function A(e,r,n,u,s){var f=typeof e;(f==="undefined"||f==="boolean")&&(e=null);var a=!1;if(e===null)a=!0;else switch(f){case"bigint":case"string":case"number":a=!0;break;case"object":switch(e.$$typeof){case _:case m:a=!0;break;case i:return a=e._init,A(a(e._payload),r,n,u,s)}}if(a)return s=s(e),a=u===""?"."+$(e,0):u,q(s)?(n="",a!=null&&(n=a.replace(G,"$&/")+"/"),A(s,r,n,"",function(oe){return oe})):s!=null&&(M(s)&&(s=ee(s,n+(s.key==null||e&&e.key===s.key?"":(""+s.key).replace(G,"$&/")+"/")+a)),r.push(s)),1;a=0;var E=u===""?".":u+":";if(q(e))for(var v=0;v"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(_)}catch(m){console.error(m)}}return _(),U.exports=ae(),U.exports}export{ce as $,le as R,ye as a,J as b,se as g,F as r}; diff --git a/services/recordly-share/worker/web/dist/embed.html b/services/recordly-share/worker/web/dist/embed.html new file mode 100644 index 000000000..694e27f03 --- /dev/null +++ b/services/recordly-share/worker/web/dist/embed.html @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/services/recordly-share/worker/web/dist/fonts/DMSans-Variable.ttf b/services/recordly-share/worker/web/dist/fonts/DMSans-Variable.ttf new file mode 100644 index 000000000..c672f9806 Binary files /dev/null and b/services/recordly-share/worker/web/dist/fonts/DMSans-Variable.ttf differ diff --git a/services/recordly-share/worker/web/dist/fonts/OFL.txt b/services/recordly-share/worker/web/dist/fonts/OFL.txt new file mode 100644 index 000000000..441171d68 --- /dev/null +++ b/services/recordly-share/worker/web/dist/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/services/recordly-share/worker/web/dist/icon-64.png b/services/recordly-share/worker/web/dist/icon-64.png new file mode 100644 index 000000000..fc3c7a673 Binary files /dev/null and b/services/recordly-share/worker/web/dist/icon-64.png differ diff --git a/services/recordly-share/worker/web/dist/lib-login.html b/services/recordly-share/worker/web/dist/lib-login.html new file mode 100644 index 000000000..bb08c1a16 --- /dev/null +++ b/services/recordly-share/worker/web/dist/lib-login.html @@ -0,0 +1,21 @@ + Library — Recordly

Your library

Enter your dashboard password to manage shared recordings.

\ No newline at end of file diff --git a/services/recordly-share/worker/web/dist/lib.html b/services/recordly-share/worker/web/dist/lib.html new file mode 100644 index 000000000..731472a23 --- /dev/null +++ b/services/recordly-share/worker/web/dist/lib.html @@ -0,0 +1,21 @@ + Library — Recordly
\ No newline at end of file diff --git a/services/recordly-share/worker/web/dist/share.html b/services/recordly-share/worker/web/dist/share.html new file mode 100644 index 000000000..2bc942807 --- /dev/null +++ b/services/recordly-share/worker/web/dist/share.html @@ -0,0 +1,21 @@ + Recordly
\ No newline at end of file diff --git a/services/recordly-share/worker/web/package-lock.json b/services/recordly-share/worker/web/package-lock.json new file mode 100644 index 000000000..488c5fedb --- /dev/null +++ b/services/recordly-share/worker/web/package-lock.json @@ -0,0 +1,8312 @@ +{ + "name": "recordly-share-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "recordly-share-web", + "version": "1.0.0", + "dependencies": { + "@astrojs/react": "^6.0.6", + "@heroui/react": "^3.2.6", + "@heroui/styles": "^3.2.6", + "@phosphor-icons/react": "^2.1.10", + "@tailwindcss/vite": "^4.3.3", + "astro": "^6.4.5", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "tailwindcss": "^4.3.3" + }, + "devDependencies": { + "@astrojs/check": "^0.9.10", + "@types/node": "^24.13.6", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "typescript": "^6.0.3" + } + }, + "node_modules/@adobe/react-spectrum": { + "version": "3.47.5", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.47.5.tgz", + "integrity": "sha512-iTbAwwMijVnQjwjwbvkWn82AyRAbFNSJzt/VawWp1vXRIxVIO9COZ1pPG5Fj88xXgqlG8maVcv58p74vqqy5HA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@react-types/shared": "^3.36.1", + "@spectrum-icons/ui": "^3.7.2", + "@spectrum-icons/workflow": "^4.3.2", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "clsx": "^2.0.0", + "react-aria": "3.52.1", + "react-aria-components": "1.21.1", + "react-stately": "3.50.0", + "react-transition-group": "^4.4.5", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@adobe/react-spectrum-ui": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-ui/-/react-spectrum-ui-1.2.1.tgz", + "integrity": "sha512-wcrbEE2O/9WnEn6avBnaVRRx88S5PLFsPLr4wffzlbMfXeQsy+RMQwaJd3cbzrn18/j04Isit7f7Emfn0dhrJA==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@adobe/react-spectrum-workflow": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-workflow/-/react-spectrum-workflow-2.3.5.tgz", + "integrity": "sha512-b53VIPwPWKb/T5gzE3qs+QlGP5gVrw/LnWV3xMksDU+CRl3rzOKUwxIGiZO8ICyYh1WiyqY4myGlPU/nAynBUg==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@astrojs/astro2tsx": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/astro2tsx/-/astro2tsx-0.1.0.tgz", + "integrity": "sha512-tgprkax8mcF+BiHJQBdoQ+REqtvvnLlYzV0yCeLMdcv3pJIutdlinWNHUzN1jMOT5VRlYTtMmDdaoPrtd3cBAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emnapi/core": "1.11.3", + "@napi-rs/wasm-runtime": "1.2.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/check": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.10.tgz", + "integrity": "sha512-zgx/UQMozdjOa3bOxjgeCFdtpE3c9rRX6xHwa+2QXvy8z8Akifu2AtubHyv/zzC2znO8dl8fFWL4K+Ba9kS8HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/language-server": "^2.16.7", + "chokidar": "^4.0.3", + "kleur": "^4.1.5", + "yargs": "^18.0.0" + }, + "bin": { + "astro-check": "bin/astro-check.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@astrojs/check/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@astrojs/check/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@astrojs/compiler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", + "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", + "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.1.1", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/language-server": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.17.0.tgz", + "integrity": "sha512-ZvT7UEo7/jBdfjXD3nNYssziEezNKM8G6FUYRnGUidAs7FmowqsQhQ0Xwl5Q4tjqlOjhnpxQuoEb4jwESL+nKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/astro2tsx": "^0.1.0", + "@astrojs/yaml2ts": "^0.2.4", + "@volar/kit": "~2.4.28", + "@volar/language-core": "~2.4.28", + "@volar/language-server": "~2.4.28", + "@volar/language-service": "~2.4.28", + "muggle-string": "^0.4.1", + "tinyglobby": "^0.2.16", + "volar-service-css": "0.0.71", + "volar-service-emmet": "0.0.71", + "volar-service-html": "0.0.71", + "volar-service-prettier": "0.0.71", + "volar-service-typescript": "0.0.71", + "volar-service-typescript-twoslash-queries": "0.0.71", + "volar-service-yaml": "0.0.71", + "vscode-html-languageservice": "^5.6.2", + "vscode-uri": "^3.1.0" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-astro": ">=0.11.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + } + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", + "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/react": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-6.0.6.tgz", + "integrity": "sha512-TQ9G2m2tIx9c105GmvjRAcTk+kV0QER5MsEPKUbK1Jm8SBYKzIzR89xh4L2vXAxI2zMfTMmsR7vln1TOLZpckw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.11.0", + "@vitejs/plugin-react": "^5.2.0", + "devalue": "^5.8.1", + "ultrahtml": "^1.6.0", + "vite": "^8.0.13" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@astrojs/react/node_modules/@astrojs/internal-helpers": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.11.0.tgz", + "integrity": "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.3.0", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@astrojs/react/node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", + "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "is-wsl": "^3.1.1", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/yaml2ts": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.4.tgz", + "integrity": "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.8.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.1.tgz", + "integrity": "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.1.tgz", + "integrity": "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@emmetio/css-parser/-/css-parser-0.4.1.tgz", + "integrity": "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", + "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", + "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", + "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@heroui/react": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@heroui/react/-/react-3.2.6.tgz", + "integrity": "sha512-KBhUUUdX4571SSjwpuBPEFZTYAw/tmGezTIsTnjIOlXDZkjWACfhFe6ibtReO+xTkUcfxCV1r3cF00/3OPlhhw==", + "license": "MIT", + "dependencies": { + "@heroui/styles": "3.2.6", + "@radix-ui/react-avatar": "1.2.6", + "@react-stately/utils": "3.12.1", + "@react-types/color": "3.2.0", + "@react-types/shared": "3.36.1", + "input-otp": "1.5.0", + "tailwind-variants": "3.3.1" + }, + "peerDependencies": { + "@internationalized/date": "^3.12.4", + "@react-aria/ssr": "^3.10.1", + "@react-aria/utils": "^3.34.1", + "react": ">=19.0.0", + "react-aria": "^3.52.1", + "react-aria-components": "^1.21.1", + "react-dom": ">=19.0.0", + "tailwindcss": ">=4.0.0" + } + }, + "node_modules/@heroui/styles": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@heroui/styles/-/styles-3.2.6.tgz", + "integrity": "sha512-feKZUphVe5+QCXnUe8kpqT5T+pPeMI3LaQcSztx08PGVTkhz0dU0C6crKvA1G4HHIPFXXon68VeL8lPjvGHsWg==", + "license": "MIT", + "dependencies": { + "tailwind-variants": "3.3.1", + "tw-animate-css": "1.4.0" + }, + "peerDependencies": { + "tailwindcss": ">=4.0.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.4.tgz", + "integrity": "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.8.tgz", + "integrity": "sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.10.tgz", + "integrity": "sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-aria/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.2.1.tgz", + "integrity": "sha512-v/pZh1yBNeayQ5Kio0xRIX0gBf6uu+KjCRW4QSydTyRpmhvk6sMzHT9i9/vIMdaDHaYHBXlXF+Tp/mvS9CG4Zw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-aria": "^3.48.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.10.1.tgz", + "integrity": "sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-aria": "^3.48.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.34.1.tgz", + "integrity": "sha512-H6+rGZL+0f58bBNaUMfctEnT+NogqwAk+nHiB8sR3K+YlQ37GTuCijy2U/pPvQtFMS5mURrjZeBH5JNNXsx14A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-aria": "^3.48.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-spectrum/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@react-spectrum/color/-/color-3.2.1.tgz", + "integrity": "sha512-FbB+GaVufsN+obNJmUAe8XGZTeJjEWvEf/k/7nxDxoDCBdg/o6eEPONAZyID5Dj1f8BZKNym8VURxDZx7IIyuQ==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum": "^3.47.0", + "@swc/helpers": "^0.5.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-spectrum/provider": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-spectrum/provider/-/provider-3.11.1.tgz", + "integrity": "sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@adobe/react-spectrum": "^3.47.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/color": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz", + "integrity": "sha512-v4YlLa/oRQpPa/M9Yq1e0Bl8Z7xzGZdNDAIbfIMZuiJtacaOKriPW+AOgjjT/Ab0fNX2YDbwIYvkiMOJBnTi5A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.12.1.tgz", + "integrity": "sha512-NqKfzrknpfwiewx7R2vk1P+CneClInPDsIhw15+jOcUYSEfej0nta4cJywuKQJ2gsPwqX/ojDNixedCve9FWGw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-stately": "^3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.2.0.tgz", + "integrity": "sha512-beV3vz80nzZ1EuYUM7296Kyi3AHcMrbQw0qub/9yzHWVTKKc5sy/e4dCMKcWL/ArkeAyc7jDOiui190RQ4l0Fw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/color": "^3.2.0", + "@react-spectrum/color": "^3.2.0", + "@react-stately/color": "^3.10.0" + }, + "peerDependencies": { + "@react-spectrum/provider": "^3.0.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.36.1.tgz", + "integrity": "sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", + "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", + "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", + "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", + "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", + "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", + "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", + "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", + "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", + "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", + "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", + "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", + "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", + "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", + "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", + "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz", + "integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.2.0", + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz", + "integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz", + "integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz", + "integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz", + "integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz", + "integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz", + "integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@spectrum-icons/ui": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.2.tgz", + "integrity": "sha512-7IuSHO+Ck8gJUIGIXqDcDIU4R8RHRUZhlfz8AwN1rSdCEvdgp6DWYN0LDTMrWh7TN/H5hrJ7oDO/gUpR3TnBjw==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum-ui": "1.2.1", + "@babel/runtime": "^7.24.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "@adobe/react-spectrum": "^3.47.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@spectrum-icons/workflow": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.2.tgz", + "integrity": "sha512-UkVg8Kp9rInjfqtG27in9p7RWfN9JFSvZr7ZOpN2n9jkrQ1y88hZpK8dKce8lgVb7jALXtsi2p0rjyaWzO0QsA==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum-workflow": "2.3.5", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "@adobe/react-spectrum": "^3.47.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", + "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@volar/kit": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.28.tgz", + "integrity": "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/language-server": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.28.tgz", + "integrity": "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.28.tgz", + "integrity": "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz", + "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-i18n": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ajv-i18n/-/ajv-i18n-4.2.0.tgz", + "integrity": "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.0-beta.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astro": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.5.tgz", + "integrity": "sha512-0R7WLD1+WD/mTPcZxyKtcF0CztQi9ahFRtGM7oChJhxjNbr4lSBenxi+mk91k5bPTaUEoZ6edg1fDo2qP8bCwg==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^4.0.0", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", + "@astrojs/telemetry": "3.3.2", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^1.1.1", + "devalue": "^5.8.1", + "diff": "^8.0.3", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.27.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "jsonc-parser": "^3.3.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "obug": "^2.1.1", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.4", + "rehype": "^13.0.2", + "semver": "^7.7.4", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "svgo": "^4.0.1", + "tinyclip": "^0.1.12", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.5", + "vfile": "^6.0.3", + "vite": "^7.3.2", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.3.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.433", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.433.tgz", + "integrity": "sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA==", + "license": "ISC" + }, + "node_modules/emmet": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "./packages/scanner", + "./packages/abbreviation", + "./packages/css-abbreviation", + "./" + ], + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/input-otp": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.5.0.tgz", + "integrity": "sha512-3AcfdW1sNG0FmSA5hHMBXG7jNW5CdcdKs8ln8JOmn6S2SRkoXoJx+2UwC+ZvjflfnOdHJx0SfQ9/YjlxVYLtGQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", + "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.56", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz", + "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", + "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.8.tgz", + "integrity": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-aria": { + "version": "3.52.1", + "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.52.1.tgz", + "integrity": "sha512-fdZZruC9/x/joCg0mhKGs5aHpwrXLCSZ4GOJmhhYyiE0ffyEsk9MLFt9LCOCF9tn8ErTD+UDQP4oDUSlZkmpCg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@internationalized/number": "^3.6.8", + "@internationalized/string": "^3.2.10", + "@react-types/shared": "^3.36.1", + "@swc/helpers": "^0.5.0", + "aria-hidden": "^1.2.3", + "clsx": "^2.0.0", + "react-stately": "3.50.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-aria-components": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.21.1.tgz", + "integrity": "sha512-J88WflYY0z+EhLX0ce+GjFlQ3ag3ubIgfUTjpKSrBQBu92Xtl4Ub9o118HItUddtbk1Ido0jzyPy94/klZy1hg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@internationalized/string": "^3.2.10", + "@react-types/shared": "^3.36.1", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "react-aria": "3.52.1", + "react-stately": "3.50.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-stately": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.50.0.tgz", + "integrity": "sha512-TnckvpDQGU0672wEaLZqdzvhuSeXWjgW6vijMWxiKOxc8CosQUfPGISdcZyfHqjX3XMjEtRxj4w9HumvX4h4mw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.4", + "@internationalized/number": "^3.6.8", + "@internationalized/string": "^3.2.10", + "@react-types/shared": "^3.36.1", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rolldown": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", + "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.150.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.9", + "@rolldown/binding-android-arm64": "1.2.9", + "@rolldown/binding-darwin-arm64": "1.2.9", + "@rolldown/binding-darwin-x64": "1.2.9", + "@rolldown/binding-freebsd-x64": "1.2.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", + "@rolldown/binding-linux-arm64-gnu": "1.2.9", + "@rolldown/binding-linux-arm64-musl": "1.2.9", + "@rolldown/binding-linux-ppc64-gnu": "1.2.9", + "@rolldown/binding-linux-s390x-gnu": "1.2.9", + "@rolldown/binding-linux-x64-gnu": "1.2.9", + "@rolldown/binding-linux-x64-musl": "1.2.9", + "@rolldown/binding-openharmony-arm64": "1.2.9", + "@rolldown/binding-win32-arm64-msvc": "1.2.9", + "@rolldown/binding-win32-x64-msvc": "1.2.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz", + "integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.2.0", + "@shikijs/engine-javascript": "4.2.0", + "@shikijs/engine-oniguruma": "4.2.0", + "@shikijs/langs": "4.2.0", + "@shikijs/themes": "4.2.0", + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tailwind-variants": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.3.1.tgz", + "integrity": "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==", + "license": "MIT", + "engines": { + "node": ">=16.9.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.14.tgz", + "integrity": "sha512-F1oWdz8tjT17qe1d5JgDK6z03WGOhYYAN0lK3/D/fzNiy93xswLLEw7pk+3g05onhAy6Bsc6PLNUGhdgVjemMQ==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz", + "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", + "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.8" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", + "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/volar-service-css": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.71.tgz", + "integrity": "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.71.tgz", + "integrity": "sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/css-parser": "^0.4.1", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.71.tgz", + "integrity": "sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.71.tgz", + "integrity": "sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.71.tgz", + "integrity": "sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.71.tgz", + "integrity": "sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-yaml": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.71.tgz", + "integrity": "sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8", + "yaml-language-server": "~1.23.0" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-html-languageservice": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", + "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", + "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" + }, + "engines": { + "npm": ">=7.0.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.2.tgz", + "integrity": "sha512-SbQSV9yRemARxeXw6LU5sS6Zq0e9/DgCCX5yelH263ZQWukbTk8EF8fjTrr1dziasf4GwlJbvTwFnTrnQFWZXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.3.tgz", + "integrity": "sha512-DF49+WeV5py4zO5hhobp60jjsDSK0lAqA0OuKBLBvp423HPWQcCbhZz3JgyfIewsEz2f8U+X75xNIFHdiXZm2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.2", + "vscode-languageserver-types": "3.18.3" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.14.tgz", + "integrity": "sha512-EQyqJMi552E4ZTf46izQ4Fj6XquqxCySR3J5ZSD1SisMf6RfpeOWHxGBE8Gr6V0/3GHIGdAzDn8F8+1nTGCnoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.3.tgz", + "integrity": "sha512-XIlzJ7Qp/jzSI1ds7/FwPAWrPeTZA7pAtlW4hdJ1J6xXWJL6dR9QYnDhJOdLzdKhUQ5Mm6mvUMw+3DcOQQasPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-language-server": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.23.0.tgz", + "integrity": "sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-i18n": "^4.2.0", + "prettier": "^3.8.1", + "request-light": "^0.5.7", + "vscode-json-languageservice": "4.1.8", + "vscode-languageserver": "^9.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-uri": "^3.0.2", + "yaml": "2.8.3" + }, + "bin": { + "yaml-language-server": "bin/yaml-language-server" + } + }, + "node_modules/yaml-language-server/node_modules/request-light": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", + "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/services/recordly-share/worker/web/package.json b/services/recordly-share/worker/web/package.json new file mode 100644 index 000000000..2b4aac940 --- /dev/null +++ b/services/recordly-share/worker/web/package.json @@ -0,0 +1,31 @@ +{ + "name": "recordly-share-web", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "check": "astro check", + "test": "node --experimental-strip-types --test src/scripts/shareModel.node-test.ts" + }, + "dependencies": { + "@astrojs/react": "^6.0.6", + "@heroui/react": "^3.2.6", + "@heroui/styles": "^3.2.6", + "@phosphor-icons/react": "^2.1.10", + "@tailwindcss/vite": "^4.3.3", + "astro": "^6.4.5", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "tailwindcss": "^4.3.3" + }, + "devDependencies": { + "@astrojs/check": "^0.9.10", + "@types/node": "^24.13.6", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "typescript": "^6.0.3" + } +} diff --git a/services/recordly-share/worker/web/public/fonts/DMSans-Variable.ttf b/services/recordly-share/worker/web/public/fonts/DMSans-Variable.ttf new file mode 100644 index 000000000..c672f9806 Binary files /dev/null and b/services/recordly-share/worker/web/public/fonts/DMSans-Variable.ttf differ diff --git a/services/recordly-share/worker/web/public/fonts/OFL.txt b/services/recordly-share/worker/web/public/fonts/OFL.txt new file mode 100644 index 000000000..441171d68 --- /dev/null +++ b/services/recordly-share/worker/web/public/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/services/recordly-share/worker/web/public/icon-64.png b/services/recordly-share/worker/web/public/icon-64.png new file mode 100644 index 000000000..fc3c7a673 Binary files /dev/null and b/services/recordly-share/worker/web/public/icon-64.png differ diff --git a/services/recordly-share/worker/web/src/components/LibraryPage.tsx b/services/recordly-share/worker/web/src/components/LibraryPage.tsx new file mode 100644 index 000000000..60a673314 --- /dev/null +++ b/services/recordly-share/worker/web/src/components/LibraryPage.tsx @@ -0,0 +1,614 @@ +import ThemeToggle from "./ThemeToggle"; +import { + Button, + Card, + Dropdown, + Input, + Label, + Modal, + Skeleton, + TextField, + toast, +} from "@heroui/react"; +import { + CaretDownIcon, + CloudIcon, + DotsThreeIcon, + FolderSimpleIcon, + GearSixIcon, + LockKeyIcon, + MagnifyingGlassIcon, + PlusIcon, + SignOutIcon, + TrashIcon, +} from "@phosphor-icons/react"; +import { useCallback, useEffect, useState } from "react"; +import { + fetchVideos, + renewVideo, + deleteVideo, + parseUTC, + type LibraryVideo, +} from "../scripts/library"; +import { CenteredCard, Notice, Notifications } from "./ShareUI"; +import { LibraryCard, type LibraryFolder } from "./library/LibraryCard"; + +export function LibraryLogin() { + const [error, setError] = useState(false); + useEffect(() => setError(new URLSearchParams(location.search).has("error")), []); + return ( + + +

Your library

+

Enter your dashboard password to manage shared recordings.

+
+ + + + + {error && Incorrect password. Please try again.} + +
+
+ ); +} + +export default function LibraryPage() { + const [section, setSection] = useState("shared"); + const [query, setQuery] = useState(""); + const [period, setPeriod] = useState("all"); + const [sort, setSort] = useState("recent"); + const [videos, setVideos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [pending, setPending] = useState(null); + const [confirmDelete, setConfirmDelete] = useState(null); + const load = useCallback(async () => { + setLoading(true); + setError(""); + try { + setVideos(await fetchVideos()); + } catch { + setError("Could not load your recordings. Please try again."); + } finally { + setLoading(false); + } + }, []); + useEffect(() => { + void load(); + }, [load]); + async function renew(code: string) { + setPending(code); + try { + const expires = await renewVideo(code); + if (!expires) throw new Error(); + setVideos((current) => + current.map((video) => + video.share_code === code ? { ...video, expires_at: expires } : video, + ), + ); + toast.success("Share link renewed"); + } catch { + toast.danger("Could not renew this link. Try again."); + } finally { + setPending(null); + } + } + async function remove(code: string) { + setPending(code); + try { + if (!(await deleteVideo(code))) throw new Error(); + setVideos((current) => current.filter((video) => video.share_code !== code)); + toast.success("Recording deleted"); + return true; + } catch { + toast.danger("Could not delete this recording. Try again."); + return false; + } finally { + setPending(null); + } + } + const [folders, setFolders] = useState([]); + const [editingFolder, setEditingFolder] = useState(null); + const [folderName, setFolderName] = useState(""); + const [help, setHelp] = useState(false); + const [selecting, setSelecting] = useState(false); + const [selected, setSelected] = useState([]); + const [deleting, setDeleting] = useState(false); + useEffect(() => { + try { + const saved: unknown = JSON.parse( + localStorage.getItem("recordly-cloud-folders") || "[]", + ); + if (Array.isArray(saved)) + setFolders( + saved.filter( + (f): f is LibraryFolder => + !!f && + typeof f.id === "string" && + typeof f.name === "string" && + Array.isArray(f.codes) && + f.codes.every((code: unknown) => typeof code === "string"), + ), + ); + } catch { + /* An unavailable or invalid local cache starts with no folders. */ + } + }, []); + function saveFolders(next: LibraryFolder[]) { + setFolders(next); + try { + localStorage.setItem("recordly-cloud-folders", JSON.stringify(next)); + } catch { + toast.danger("Folders could not be saved in this browser."); + } + } + function assignFolder(code: string, id: string) { + saveFolders( + folders.map((folder) => + folder.id === id + ? { + ...folder, + codes: folder.codes.includes(code) + ? folder.codes.filter((item) => item !== code) + : [...folder.codes, code], + } + : folder, + ), + ); + } + function finishFolder() { + if (folderName.trim()) + saveFolders( + folders.map((folder) => + folder.id === editingFolder ? { ...folder, name: folderName.trim() } : folder, + ), + ); + setEditingFolder(null); + } + const visible = videos + .filter((video) => { + const age = Date.now() - parseUTC(video.created_at).getTime(); + const folder = folders.find((folder) => folder.id === section); + return ( + video.title.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()) && + (!folder || folder.codes.includes(video.share_code)) && + (period === "all" || age <= (period === "week" ? 7 : 30) * 86400000) + ); + }) + .sort((a, b) => + sort === "name" + ? a.title.localeCompare(b.title) + : parseUTC(b.created_at).getTime() - parseUTC(a.created_at).getTime(), + ); + const busy = pending !== null || deleting; + const navClass = (active: boolean) => + `h-10 w-full justify-start gap-3 px-3 text-[13px] ${active ? "bg-default/50 font-medium" : "text-muted-foreground"}`; + async function copy(code: string) { + try { + await navigator.clipboard.writeText(`${location.origin}/s/${code}`); + toast.success("Link copied"); + } catch { + toast.danger("Could not copy the link."); + } + } + return ( + <> + +
+ + + {section !== "settings" && ( + <> +
+
+ + setQuery(e.target.value)} + className="h-11 w-full border-0 bg-default/30 pl-10 shadow-none" + /> +
+
+
+
+ {[ + ["all", "All"], + ["week", "Last 7 days"], + ["month", "Last 30 days"], + ].map(([id, label]) => ( + + ))} + +
+
+ + + + + setSort("recent")} + > + Last created + + setSort("name")} + > + Name + + + + +
+
+ {selecting && ( +
+ + {selected.length} selected + + +
+ )} + + )} +
+ {section === "settings" ? ( +
+

Library settings

+

+ Folders are saved in this browser. Recordings and share links + are stored by your sharing service. +

+ +
+ ) : ( + <> + {error && {error}} +
    + {loading + ? [0, 1, 2].map((i) => ( +
  • + +
  • + )) + : visible.map((video) => ( + + setSelected((current) => + current.includes(video.share_code) + ? current.filter( + (code) => + code !== + video.share_code, + ) + : [...current, video.share_code], + ) + } + busy={busy} + copy={() => void copy(video.share_code)} + renew={() => void renew(video.share_code)} + remove={() => + setConfirmDelete([video.share_code]) + } + /> + ))} +
+ {!loading && !error && !visible.length && ( +
+ +

+ {videos.length + ? "No recordings match this view." + : "No shared recordings yet."} +

+ +
+ )} + + )} +
+
+
+ + + + + + Share a recording + + +

+ Open a project in Recordly, choose Export, then Create share + link. Your shared recording will appear here. +

+
+ + + +
+
+
+
+ { + if (!open && !busy) setConfirmDelete(null); + }} + > + + + + + + Delete{" "} + {confirmDelete?.length === 1 ? "recording" : "recordings"}? + + + +

+ This permanently removes {confirmDelete?.length} recording(s) + and their share links. +

+
+ + + + +
+
+
+
+ + ); +} diff --git a/services/recordly-share/worker/web/src/components/PagedPanel.tsx b/services/recordly-share/worker/web/src/components/PagedPanel.tsx new file mode 100644 index 000000000..22ecfac4e --- /dev/null +++ b/services/recordly-share/worker/web/src/components/PagedPanel.tsx @@ -0,0 +1,89 @@ +import { Button } from '@heroui/react'; +import { CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react'; +import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'; + +/** Flow long content into viewport-sized pages instead of a scroll container. */ +export default function PagedPanel({ children, label }: { children: ReactNode; label: string }) { + const viewport = useRef(null); + const flow = useRef(null); + const [page, setPage] = useState(0); + const [pages, setPages] = useState(1); + const [stride, setStride] = useState(0); + + useLayoutEffect(() => { + const frame = viewport.current; + const content = flow.current; + if (!frame || !content) return; + const measure = () => { + const width = frame.clientWidth; + if (!width) return; + const step = width + 24; + const count = Math.max(1, Math.ceil((content.scrollWidth + 24 - 1) / step)); + setStride(step); + setPages(count); + setPage((current) => Math.min(current, count - 1)); + }; + const resize = new ResizeObserver(measure); + const mutation = new MutationObserver(measure); + resize.observe(frame); + mutation.observe(content, { childList: true, subtree: true, characterData: true }); + measure(); + return () => { + resize.disconnect(); + mutation.disconnect(); + }; + }, []); + + return ( +
+
{ + if (!stride || !viewport.current) return; + const frame = viewport.current; + const offset = + (event.target as HTMLElement).getBoundingClientRect().left - + frame.getBoundingClientRect().left + + page * stride + + frame.scrollLeft; + setPage(Math.max(0, Math.min(pages - 1, Math.floor((offset + 1) / stride)))); + frame.scrollLeft = 0; + }} + > +
+ {children} +
+
+ +
+ ); +} diff --git a/services/recordly-share/worker/web/src/components/ShareFeedback.tsx b/services/recordly-share/worker/web/src/components/ShareFeedback.tsx new file mode 100644 index 000000000..6c7d100c0 --- /dev/null +++ b/services/recordly-share/worker/web/src/components/ShareFeedback.tsx @@ -0,0 +1,376 @@ +import { + Avatar, + Button, + Card, + Chip, + Input, + Label, + Spinner, + Tabs, + TextArea, + TextField, + toast, +} from '@heroui/react'; +import { + ChatCircleIcon, + ClockIcon, + LinkSimpleIcon, + PaperPlaneTiltIcon, +} from '@phosphor-icons/react'; +import { useEffect, useState, type Dispatch, type SetStateAction, type RefObject } from 'react'; +import { + fetchComments, + formatDate, + formatTimestamp, + postComment, + type Comment, + type ShareData, +} from '../scripts/api'; +import { timestampParts } from '../scripts/shareModel'; +import { copyLink, Notice } from './ShareUI'; +import PagedPanel from './PagedPanel'; + +interface Props { + data: ShareData; + time: number; + duration: number; + comments: Comment[]; + setComments: Dispatch>; + seek: (time: number) => void; + composerRef: RefObject; + selectedTab: string; + setSelectedTab: (tab: string) => void; +} + +export default function ShareFeedback({ + data, + time, + duration, + comments, + setComments, + seek, + composerRef, + selectedTab, + setSelectedTab, +}: Props) { + const [name, setName] = useState(''); + const [text, setText] = useState(''); + const [lockedTime, setLockedTime] = useState(null); + const [posting, setPosting] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + const [moreLoading, setMoreLoading] = useState(false); + const activeIndex = comments.reduce( + (found, comment, i) => (comment.timestamp <= time + 0.3 ? i : found), + -1, + ); + + useEffect(() => { + try { + setName(localStorage.getItem('recordly-comment-name') || ''); + } catch { + /* Storage can be disabled. */ + } + let cancelled = false; + fetchComments(data.shareCode) + .then((result) => { + if (cancelled) return; + setComments([...result.comments].sort((a, b) => a.timestamp - b.timestamp)); + setTotal(result.total); + setLoading(false); + }) + .catch(() => { + if (!cancelled) { + setError('Comments could not be loaded. Reload the page to try again.'); + setLoading(false); + } + }); + return () => { + cancelled = true; + }; + }, [data.shareCode, setComments]); + + async function loadMore() { + setMoreLoading(true); + try { + const result = await fetchComments(data.shareCode, page + 1); + setComments((current) => [...new Map([...current, ...result.comments].map((comment) => [comment.id, comment])).values()].sort((a, b) => a.timestamp - b.timestamp || a.id - b.id)); + setTotal(result.total); + setPage(page + 1); + } catch { + toast.danger('Could not load more comments. Try again.'); + } finally { + setMoreLoading(false); + } + } + + async function submit() { + if (posting || !name.trim() || !text.trim()) return; + const timestamp = lockedTime ?? time; + setPosting(true); + setError(''); + try { + const id = await postComment(data.shareCode, timestamp, name.trim(), text.trim()); + setComments((current) => + [ + ...current, + { + id, + timestamp, + author_name: name.trim(), + text: text.trim(), + created_at: new Date().toISOString().slice(0, 19), + }, + ].sort((a, b) => a.timestamp - b.timestamp), + ); + setTotal(total + 1); + setText(''); + setLockedTime(null); + try { + localStorage.setItem('recordly-comment-name', name.trim()); + } catch { + /* Posting does not require storage. */ + } + toast.success(`Comment posted at ${formatTimestamp(timestamp)}`); + } catch { + setError('Could not post your comment. Your draft is saved here—please try again.'); + } finally { + setPosting(false); + } + } + + return ( + + setSelectedTab(String(key))} + className="feedback-tabs" + > + + + + Comments + {total > 0 && ( + + {total} + + )} + + + {data.segments.length > 0 && ( + + Transcript + + + )} + {(data.video.summary || data.chapters.length > 0) && ( + + Details + + + )} + + + + + {loading ? ( +
+ +
+ ) : !comments.length ? ( +
+ +

Start the conversation

+

Leave feedback at the exact moment you’re watching.

+
+ ) : ( + comments.map((comment, i) => ( +
+
+ + + {comment.author_name.slice(0, 2).toUpperCase()} + + + {comment.author_name} + +
+

+ {timestampParts(comment.text, duration).map((part, n) => + part.time === undefined ? ( + part.text + ) : ( + + ), + )} +

+ {formatDate(comment.created_at)} +
+ )) + )} + {comments.length < total && ( + + )} +
+
+ {data.segments.length > 0 && ( + + + {data.segments.map((segment, i) => ( +
= segment.start_time && time < segment.end_time} + > +
+ + {segment.speaker && {segment.speaker}} + +
+ +
+ ))} +
+
+ )} + {(data.video.summary || data.chapters.length > 0) && ( + + +
+

Recording

+

{data.video.title}

+
+ {data.video.summary && ( +
+

Summary

+

{data.video.summary}

+
+ )} + {data.chapters.length > 0 && ( +
+

Chapters

+
+ {data.chapters.map((chapter, index) => ( + + ))} +
+
+ )} +
+
+ )} +
+